mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-10 04:13:27 +03:00
Compare commits
1 Commits
feat/proxy
...
bugfix/ovp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27e15eeecf |
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -10,10 +10,10 @@ env:
|
||||
|
||||
jobs:
|
||||
Build-Linux-Ubuntu:
|
||||
runs-on: android-runner
|
||||
runs-on: 4-core
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.8.3
|
||||
QIF_VERSION: 4.7
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||
sudo apt-get install libxkbcommon-x11-0
|
||||
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
|
||||
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
|
||||
bash deploy/build_linux.sh
|
||||
@@ -537,7 +537,7 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-Android:
|
||||
runs-on: android-runner
|
||||
runs-on: 4-core
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-36
|
||||
|
||||
2
.github/workflows/tag-upload.yml
vendored
2
.github/workflows/tag-upload.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Verify git tag
|
||||
run: |
|
||||
TAG_NAME=${{ inputs.RELEASE_VERSION }}
|
||||
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
|
||||
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
|
||||
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
|
||||
else
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -140,6 +140,3 @@ ios-ne-build.sh
|
||||
macos-ne-build.sh
|
||||
macos-signed-build.sh
|
||||
macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -14,7 +14,3 @@
|
||||
[submodule "client/3rd/QSimpleCrypto"]
|
||||
path = client/3rd/QSimpleCrypto
|
||||
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
||||
[submodule "client/3rd/qtgamepad"]
|
||||
path = client/3rd/qtgamepad
|
||||
url = https://github.com/amnezia-vpn/qtgamepad.git
|
||||
branch = 6.6
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.13.0)
|
||||
set(AMNEZIAVPN_VERSION 4.8.12.8)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2106)
|
||||
set(APP_ANDROID_VERSION_CODE 2104)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
Submodule client/3rd-prebuilt updated: b8c229288d...adf5eb920f
1
client/3rd/qtgamepad
vendored
1
client/3rd/qtgamepad
vendored
Submodule client/3rd/qtgamepad deleted from f72b3e0c62
@@ -59,6 +59,7 @@ target_include_directories(${PROJECT} PUBLIC
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
|
||||
endif()
|
||||
|
||||
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
|
||||
@@ -227,13 +228,4 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
endif()
|
||||
|
||||
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||
|
||||
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||
if(COMMAND qt_import_qml_plugins)
|
||||
qt_import_qml_plugins(${PROJECT})
|
||||
endif()
|
||||
if(COMMAND qt_finalize_executable)
|
||||
qt_finalize_executable(${PROJECT})
|
||||
else()
|
||||
qt_finalize_target(${PROJECT})
|
||||
endif()
|
||||
qt_finalize_target(${PROJECT})
|
||||
|
||||
@@ -26,8 +26,6 @@ import android.os.ParcelFileDescriptor
|
||||
import android.os.SystemClock
|
||||
import android.provider.OpenableColumns
|
||||
import android.provider.Settings
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -90,10 +88,6 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||
|
||||
private var isActivityResumed = false
|
||||
private var hasWindowFocus = false
|
||||
private val resumeHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
@@ -266,10 +260,6 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
isActivityResumed = false
|
||||
hasWindowFocus = false
|
||||
// Cancel all pending operations when activity stops
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
Log.d(TAG, "Stop Amnezia activity")
|
||||
doUnbindService()
|
||||
mainScope.launch {
|
||||
@@ -281,91 +271,35 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
hasWindowFocus = hasFocus
|
||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||
|
||||
// Cancel pending operations if window loses focus
|
||||
if (!hasFocus) {
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
val deviceId = event.deviceId
|
||||
val keyCode = event.keyCode
|
||||
val pressed = event.action == KeyEvent.ACTION_DOWN
|
||||
val source = event.source
|
||||
|
||||
if (deviceId < 0 && pressed) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_B,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BUTTON_START,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
nativeGamepadKeyEvent(0, keyCode, true)
|
||||
nativeGamepadKeyEvent(0, keyCode, false)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Real gamepad events (deviceId >= 0)
|
||||
if (deviceId >= 0) {
|
||||
val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
|
||||
val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
||||
val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
|
||||
if (isGamepad || isJoystick || isDpad) {
|
||||
nativeGamepadKeyEvent(deviceId, keyCode, pressed)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
Log.d(TAG, "Pause Amnezia activity")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
window.decorView.apply {
|
||||
invalidate()
|
||||
|
||||
resumeHandler.postDelayed({
|
||||
// Check if activity is still resumed and has focus before executing
|
||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||
sendTouch(1f, 1f)
|
||||
}
|
||||
postDelayed({
|
||||
sendTouch(1f, 1f)
|
||||
}, 100)
|
||||
|
||||
resumeHandler.postDelayed({
|
||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||
sendTouch(2f, 2f)
|
||||
}
|
||||
postDelayed({
|
||||
sendTouch(2f, 2f)
|
||||
}, 200)
|
||||
|
||||
resumeHandler.postDelayed({
|
||||
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
postDelayed({
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
} */
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
}
|
||||
|
||||
private fun configureWindowForEdgeToEdge() {
|
||||
@@ -428,10 +362,6 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
isActivityResumed = false
|
||||
hasWindowFocus = false
|
||||
// Cancel all pending operations when activity is destroyed
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
Log.d(TAG, "Destroy Amnezia activity")
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
notificationStateReceiver = null
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -14,25 +11,7 @@ private const val TAG = "TvFilePicker"
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||
}
|
||||
if (activitiesToResolveIntent.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||
}) {
|
||||
throw ActivityNotFoundException()
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}) {
|
||||
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
setResult(RESULT_OK, Intent().apply { data = it })
|
||||
finish()
|
||||
}
|
||||
@@ -52,7 +31,7 @@ class TvFilePicker : ComponentActivity() {
|
||||
private fun getFile() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||
fileChooseResultLauncher.launch("*/*")
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
|
||||
@@ -83,26 +83,6 @@ add_compile_definitions(_WINSOCKAPI_)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
|
||||
|
||||
if(ANDROID)
|
||||
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
||||
# Only if Qt6CorePrivate is available (required by qtgamepad)
|
||||
find_package(Qt6CorePrivate CONFIG QUIET)
|
||||
if(Qt6CorePrivate_FOUND)
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
|
||||
# Link both the C++ module and QML plugin
|
||||
if(TARGET GamepadLegacy)
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
|
||||
endif()
|
||||
if(TARGET GamepadLegacyQuickPrivate)
|
||||
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
|
||||
endif()
|
||||
message(STATUS "Gamepad support enabled for Android")
|
||||
else()
|
||||
message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(LIBS ${LIBS} qt6keychain)
|
||||
|
||||
include_directories(
|
||||
|
||||
@@ -181,6 +181,7 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/ipcclient.h
|
||||
${CLIENT_ROOT_DIR}/core/privileged_process.h
|
||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
|
||||
@@ -193,6 +194,7 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
|
||||
${CLIENT_ROOT_DIR}/core/privileged_process.cpp
|
||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
|
||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
|
||||
|
||||
@@ -298,14 +298,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
}
|
||||
|
||||
#elif defined(MACOS_NE)
|
||||
// macOS build using Network Extension – hide OpenVPN-based containers
|
||||
// macOS build using Network Extension – allow OpenVPN for parity with iOS.
|
||||
switch (c) {
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::Cloak:
|
||||
case DockerContainer::ShadowSocks:
|
||||
return false;
|
||||
|
||||
@@ -46,25 +46,12 @@ namespace
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
|
||||
QStringList splitUrls(const QString &urls)
|
||||
{
|
||||
QStringList parsedUrls = urls.split(",", Qt::SkipEmptyParts);
|
||||
for (QString &url : parsedUrls) {
|
||||
url = url.trimmed();
|
||||
}
|
||||
parsedUrls.removeAll("");
|
||||
return parsedUrls;
|
||||
}
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, const QString &proxyStorageOverride,
|
||||
const QString &proxyUrlOverride, QObject *parent)
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_proxyStorageOverride(proxyStorageOverride),
|
||||
m_proxyUrlOverride(proxyUrlOverride),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
@@ -84,8 +71,7 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
||||
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
||||
QString selectedProxyUrl = m_proxyUrlOverride.isEmpty() ? m_proxyUrl : m_proxyUrlOverride;
|
||||
encRequestData.request.setUrl(endpoint.arg(selectedProxyUrl.isEmpty() ? m_gatewayEndpoint : selectedProxyUrl));
|
||||
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
@@ -297,9 +283,9 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
baseUrls = m_proxyStorageOverride.isEmpty() ? splitUrls(DEV_S3_ENDPOINT) : splitUrls(m_proxyStorageOverride);
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
baseUrls = splitUrls(PROD_S3_ENDPOINT);
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
@@ -347,9 +333,9 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
baseUrls = m_proxyStorageOverride.isEmpty() ? splitUrls(DEV_S3_ENDPOINT) : splitUrls(m_proxyStorageOverride);
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
baseUrls = splitUrls(PROD_S3_ENDPOINT);
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
@@ -498,9 +484,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
return result;
|
||||
};
|
||||
|
||||
QString selectedProxyUrl = m_proxyUrlOverride.isEmpty() ? m_proxyUrl : m_proxyUrlOverride;
|
||||
|
||||
if (selectedProxyUrl.isEmpty()) {
|
||||
if (m_proxyUrl.isEmpty()) {
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(1000);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
@@ -522,7 +506,6 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
|
||||
m_proxyUrl = proxyUrl;
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
selectedProxyUrl = m_proxyUrl;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -531,8 +514,8 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedProxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, selectedProxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -618,11 +601,6 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
{
|
||||
if (!m_proxyUrlOverride.isEmpty()) {
|
||||
onComplete(m_proxyUrlOverride);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete("");
|
||||
return;
|
||||
|
||||
@@ -20,8 +20,7 @@ class GatewayController : public QObject
|
||||
|
||||
public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, const QString &proxyStorageOverride = "",
|
||||
const QString &proxyUrlOverride = "", QObject *parent = nullptr);
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
@@ -62,8 +61,6 @@ private:
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
QString m_proxyStorageOverride;
|
||||
QString m_proxyUrlOverride;
|
||||
bool m_isDevEnvironment = false;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
|
||||
|
||||
@@ -419,18 +419,6 @@ ErrorCode ServerController::installDockerWorker(const ServerCredentials &credent
|
||||
cbReadStdOut, cbReadStdErr);
|
||||
|
||||
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
|
||||
if (container == DockerContainer::Awg2) {
|
||||
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = regex.match(stdOut);
|
||||
if (match.hasMatch()) {
|
||||
int majorVersion = match.captured(1).toInt();
|
||||
int minorVersion = match.captured(2).toInt();
|
||||
|
||||
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
|
||||
return ErrorCode::ServerLinuxKernelTooOld;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stdOut.contains("lock"))
|
||||
return ErrorCode::ServerPacketManagerError;
|
||||
if (stdOut.contains("command not found"))
|
||||
|
||||
@@ -61,7 +61,6 @@ namespace amnezia
|
||||
ServerDockerOnCgroupsV2 = 211,
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
|
||||
@@ -29,7 +29,6 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
|
||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
|
||||
@@ -7,6 +7,7 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
|
||||
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
|
||||
m_tun2socks.reset(m_node.acquire<IpcProcessTun2SocksReplica>());
|
||||
}
|
||||
|
||||
IpcClient& IpcClient::Instance()
|
||||
@@ -32,43 +33,68 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
|
||||
return rep;
|
||||
}
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> IpcClient::CreatePrivilegedProcess()
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
|
||||
{
|
||||
return withInterface([](QSharedPointer<IpcInterfaceReplica> &iface) -> QSharedPointer<IpcProcessInterfaceReplica> {
|
||||
auto createPrivilegedProcess = iface->createPrivilegedProcess();
|
||||
if (!createPrivilegedProcess.waitForFinished()) {
|
||||
qCritical() << "Failed to create privileged process";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int pid = createPrivilegedProcess.returnValue();
|
||||
|
||||
auto* node = new QRemoteObjectNode();
|
||||
node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid))));
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> rep(
|
||||
node->acquire<IpcProcessInterfaceReplica>(),
|
||||
[node] (IpcProcessInterfaceReplica *ptr) {
|
||||
delete ptr;
|
||||
node->deleteLater();
|
||||
}
|
||||
);
|
||||
if (rep.isNull()) {
|
||||
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->waitForSource()) {
|
||||
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->isReplicaValid()) {
|
||||
qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return rep;
|
||||
},
|
||||
[]() -> QSharedPointer<IpcProcessInterfaceReplica> {
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
|
||||
if (rep.isNull()) {
|
||||
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
|
||||
return nullptr;
|
||||
});
|
||||
}
|
||||
if (!rep->waitForSource(1000)) {
|
||||
qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->isReplicaValid()) {
|
||||
qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid";
|
||||
}
|
||||
return rep;
|
||||
}
|
||||
|
||||
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
|
||||
{
|
||||
QSharedPointer<IpcInterfaceReplica> rep = Interface();
|
||||
if (!rep) {
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QRemoteObjectPendingReply<int> pidReply = rep->createPrivilegedProcess();
|
||||
if (!pidReply.waitForFinished(5000)){
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int pid = pidReply.returnValue();
|
||||
QSharedPointer<ProcessDescriptor> pd(new ProcessDescriptor());
|
||||
|
||||
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
|
||||
|
||||
connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() {
|
||||
pd->replicaNode->addClientSideConnection(pd->localSocket.data());
|
||||
|
||||
IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire<IpcProcessInterfaceReplica>();
|
||||
// TODO: rework the unsafe cast below
|
||||
PrivilegedProcess *priv = static_cast<PrivilegedProcess *>(repl);
|
||||
pd->ipcProcess.reset(priv);
|
||||
if (!pd->ipcProcess) {
|
||||
qWarning() << "Acquire PrivilegedProcess failed";
|
||||
} else {
|
||||
pd->ipcProcess->waitForSource(1000);
|
||||
if (!pd->ipcProcess->isReplicaValid()) {
|
||||
qWarning() << "PrivilegedProcess replica is not connected!";
|
||||
}
|
||||
|
||||
QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(),
|
||||
[pd]() { pd->replicaNode->deleteLater(); });
|
||||
}
|
||||
});
|
||||
|
||||
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
|
||||
if (!pd->localSocket->waitForConnected()) {
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
|
||||
return processReplica;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#include <QObject>
|
||||
|
||||
#include "rep_ipc_interface_replica.h"
|
||||
#include "rep_ipc_process_interface_replica.h"
|
||||
#include "rep_ipc_process_tun2socks_replica.h"
|
||||
|
||||
#include "privileged_process.h"
|
||||
|
||||
class IpcClient : public QObject
|
||||
{
|
||||
@@ -16,7 +18,8 @@ public:
|
||||
static IpcClient& Instance();
|
||||
|
||||
static QSharedPointer<IpcInterfaceReplica> Interface();
|
||||
static QSharedPointer<IpcProcessInterfaceReplica> CreatePrivilegedProcess();
|
||||
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
|
||||
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
|
||||
|
||||
template <typename Func>
|
||||
static auto withInterface(Func func)
|
||||
@@ -51,6 +54,18 @@ signals:
|
||||
private:
|
||||
QRemoteObjectNode m_node;
|
||||
QSharedPointer<IpcInterfaceReplica> m_interface;
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
|
||||
|
||||
struct ProcessDescriptor {
|
||||
ProcessDescriptor () {
|
||||
replicaNode = QSharedPointer<QRemoteObjectNode>(new QRemoteObjectNode());
|
||||
ipcProcess = QSharedPointer<PrivilegedProcess>();
|
||||
localSocket = QSharedPointer<QLocalSocket>();
|
||||
}
|
||||
QSharedPointer<PrivilegedProcess> ipcProcess;
|
||||
QSharedPointer<QRemoteObjectNode> replicaNode;
|
||||
QSharedPointer<QLocalSocket> localSocket;
|
||||
};
|
||||
};
|
||||
|
||||
#endif // IPCCLIENT_H
|
||||
|
||||
27
client/core/privileged_process.cpp
Normal file
27
client/core/privileged_process.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "privileged_process.h"
|
||||
|
||||
PrivilegedProcess::PrivilegedProcess() :
|
||||
IpcProcessInterfaceReplica()
|
||||
{
|
||||
}
|
||||
|
||||
PrivilegedProcess::~PrivilegedProcess()
|
||||
{
|
||||
qDebug() << "PrivilegedProcess::~PrivilegedProcess()";
|
||||
}
|
||||
|
||||
void PrivilegedProcess::waitForFinished(int msecs)
|
||||
{
|
||||
QSharedPointer<QEventLoop> loop(new QEventLoop);
|
||||
connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{
|
||||
loop->quit();
|
||||
loop.clear();
|
||||
});
|
||||
|
||||
QTimer::singleShot(msecs, this, [this, loop]() mutable {
|
||||
loop->quit();
|
||||
loop.clear();
|
||||
});
|
||||
|
||||
loop->exec();
|
||||
}
|
||||
24
client/core/privileged_process.h
Normal file
24
client/core/privileged_process.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef PRIVILEGED_PROCESS_H
|
||||
#define PRIVILEGED_PROCESS_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "rep_ipc_process_interface_replica.h"
|
||||
// This class is dangerous - instance of this class casted from base class,
|
||||
// so it support only functions
|
||||
// Do not add any members into it
|
||||
//
|
||||
class PrivilegedProcess : public IpcProcessInterfaceReplica
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PrivilegedProcess();
|
||||
~PrivilegedProcess() override;
|
||||
|
||||
void waitForFinished(int msecs);
|
||||
|
||||
};
|
||||
|
||||
#endif // PRIVILEGED_PROCESS_H
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
|
||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
||||
|
||||
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<string>AmneziaVPNNetworkExtension</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.amnezia.AmneziaVPN.network-extension</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@@ -16,9 +16,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${APPLE_PROJECT_VERSION}</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
@@ -41,6 +41,6 @@
|
||||
<string>group.org.amnezia.AmneziaVPN</string>
|
||||
|
||||
<key>com.wireguard.macos.app_group_id</key>
|
||||
<string>${BUILD_VPN_DEVELOPMENT_TEAM}.group.org.amnezia.AmneziaVPN</string>
|
||||
<string>$(DEVELOPMENT_TEAM).group.org.amnezia.AmneziaVPN</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -270,7 +270,12 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) {
|
||||
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()) {
|
||||
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
|
||||
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
|
||||
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
|
||||
|
||||
@@ -15,6 +15,12 @@ struct OpenVPNConfig: Decodable {
|
||||
|
||||
extension PacketTunnelProvider {
|
||||
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
||||
// Reset session-derived state so reconnects never reuse stale gateway/address data.
|
||||
openVpnGatewayAddress = nil
|
||||
openVpnLocalAddress = nil
|
||||
openVpnLocalMask = nil
|
||||
lastOpenVPNSettings = nil
|
||||
|
||||
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||
@@ -26,6 +32,20 @@ extension PacketTunnelProvider {
|
||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||
splitTunnelType = openVPNConfig.splitTunnelType
|
||||
splitTunnelSites = openVPNConfig.splitTunnelSites
|
||||
openVpnDnsServers = Self.extractDnsServers(from: openVPNConfig.config)
|
||||
openVpnRemoteAddress = Self.extractRemoteHost(from: openVPNConfig.config)
|
||||
openVpnRedirectGatewayDef1 = Self.hasRedirectGatewayDef1(in: openVPNConfig.config)
|
||||
if let openVpnRemoteAddress {
|
||||
ovpnLog(.info, title: "Remote", message: "host=\(openVpnRemoteAddress)")
|
||||
}
|
||||
if !openVpnDnsServers.isEmpty {
|
||||
ovpnLog(.info, title: "DNS", message: "servers=\(openVpnDnsServers)")
|
||||
}
|
||||
if openVpnRedirectGatewayDef1 {
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "redirect-gateway def1 detected")
|
||||
}
|
||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||
} catch {
|
||||
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||
@@ -73,6 +93,11 @@ extension PacketTunnelProvider {
|
||||
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
||||
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
||||
|
||||
let hasCertTag = configString.contains("<cert>") && configString.contains("</cert>")
|
||||
let hasKeyTag = configString.contains("<key>") && configString.contains("</key>")
|
||||
let hasAuthUserPass = configString.contains("auth-user-pass")
|
||||
ovpnLog(.info, title: "ConfigCreds", message: "inlineCert=\(hasCertTag) inlineKey=\(hasKeyTag) authUserPass=\(hasAuthUserPass)")
|
||||
|
||||
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
||||
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
||||
@@ -83,27 +108,98 @@ extension PacketTunnelProvider {
|
||||
ovpnLog(.debug, title: "ConfigHead", message: head)
|
||||
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
||||
|
||||
if let start = configString.range(of: "<tls-auth>"),
|
||||
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
|
||||
let keyBody = String(configString[start.upperBound..<end.lowerBound])
|
||||
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
|
||||
let sanitizedLines = keyBody
|
||||
.split(whereSeparator: { $0.isNewline })
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.filter { !$0.hasPrefix("#") }
|
||||
|
||||
let sanitizedKey = sanitizedLines.joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
|
||||
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
|
||||
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
|
||||
if hasTlsAuthOpen && hasTlsAuthClose {
|
||||
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
||||
}
|
||||
|
||||
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
var normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
normalizedConfig = Self.normalizeInlineBlock(
|
||||
in: normalizedConfig,
|
||||
tag: "ca",
|
||||
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
||||
endMarkers: ["-----END CERTIFICATE-----"]
|
||||
)
|
||||
normalizedConfig = Self.normalizeInlineBlock(
|
||||
in: normalizedConfig,
|
||||
tag: "cert",
|
||||
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
||||
endMarkers: ["-----END CERTIFICATE-----"]
|
||||
)
|
||||
normalizedConfig = Self.normalizeInlineBlock(
|
||||
in: normalizedConfig,
|
||||
tag: "key",
|
||||
beginMarkers: [
|
||||
"-----BEGIN PRIVATE KEY-----",
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
"-----BEGIN EC PRIVATE KEY-----",
|
||||
"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
||||
],
|
||||
endMarkers: [
|
||||
"-----END PRIVATE KEY-----",
|
||||
"-----END RSA PRIVATE KEY-----",
|
||||
"-----END EC PRIVATE KEY-----",
|
||||
"-----END ENCRYPTED PRIVATE KEY-----"
|
||||
]
|
||||
)
|
||||
normalizedConfig = Self.normalizeInlineBlock(
|
||||
in: normalizedConfig,
|
||||
tag: "tls-auth",
|
||||
beginMarkers: ["-----BEGIN OpenVPN Static key V1-----"],
|
||||
endMarkers: ["-----END OpenVPN Static key V1-----"]
|
||||
)
|
||||
normalizedConfig = Self.stripUnsupportedOptions(forOpenVPNAdapter: normalizedConfig)
|
||||
if !normalizedConfig.hasSuffix("\n") {
|
||||
normalizedConfig.append("\n")
|
||||
}
|
||||
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
|
||||
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
|
||||
let redirectLines = normalizedLines
|
||||
.map(String.init)
|
||||
.filter { $0.lowercased().contains("redirect-gateway") }
|
||||
if !redirectLines.isEmpty {
|
||||
ovpnLog(.info, title: "ConfigRedirect", message: redirectLines.joined(separator: " | "))
|
||||
}
|
||||
let controlScalars = normalizedConfig.unicodeScalars.filter {
|
||||
($0.value < 0x20 && $0 != "\n" && $0 != "\r" && $0 != "\t")
|
||||
}
|
||||
if !controlScalars.isEmpty {
|
||||
ovpnLog(.error, title: "ConfigChars", message: "nonPrintableControlCount=\(controlScalars.count)")
|
||||
}
|
||||
#if os(macOS)
|
||||
let dumpBaseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||
?? FileManager.default.temporaryDirectory
|
||||
let dumpURL = dumpBaseURL.appendingPathComponent("amnezia_ovpn_adapter_config.conf")
|
||||
do {
|
||||
try normalizedConfig.write(to: dumpURL, atomically: true, encoding: .utf8)
|
||||
ovpnLog(.info, title: "ConfigDump", message: "path=\(dumpURL.path) bytes=\(normalizedConfig.utf8.count)")
|
||||
} catch {
|
||||
ovpnLog(.error, title: "ConfigDump", message: "write failed: \(error.localizedDescription)")
|
||||
}
|
||||
#endif
|
||||
let sanitizedData = Data(normalizedConfig.utf8)
|
||||
|
||||
let configuration = OpenVPNConfiguration()
|
||||
configuration.fileContent = sanitizedData
|
||||
// Be explicit: enum default is 0 (enabled), we need stubs-only behavior.
|
||||
configuration.compressionMode = .disabled
|
||||
// A-012: emulate OpenVPN2 CLI capability advertisement as closely as possible.
|
||||
configuration.peerInfo = [
|
||||
"IV_VER": "2.6.10",
|
||||
"IV_PLAT": "mac",
|
||||
"IV_TCPNL": "1",
|
||||
"IV_MTU": "1600",
|
||||
"IV_NCP": "2",
|
||||
"IV_CIPHERS": "AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305",
|
||||
"IV_PROTO": "990",
|
||||
"IV_LZO_STUB": "1",
|
||||
"IV_COMP_STUB": "1",
|
||||
"IV_COMP_STUBv2": "1"
|
||||
]
|
||||
if let peerInfo = configuration.peerInfo {
|
||||
let peerInfoSummary = peerInfo.keys.sorted().map { "\($0)=\(peerInfo[$0] ?? "")" }.joined(separator: " ")
|
||||
ovpnLog(.info, title: "PeerInfoOverride", message: peerInfoSummary)
|
||||
}
|
||||
if configString.contains("cloak") {
|
||||
configuration.setPTCloak()
|
||||
}
|
||||
@@ -124,11 +220,16 @@ extension PacketTunnelProvider {
|
||||
if evaluation?.autologin == false {
|
||||
ovpnLog(.info, message: "Implement login with user credentials")
|
||||
}
|
||||
if let evaluation {
|
||||
ovpnLog(.info, title: "ConfigEval", message: "autologin=\(evaluation.autologin) externalPki=\(evaluation.externalPki)")
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
vpnReachability.startTracking { [weak self] status in
|
||||
guard status == .reachableViaWiFi else { return }
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
||||
}
|
||||
#endif
|
||||
|
||||
startHandler = completionHandler
|
||||
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||
@@ -144,6 +245,8 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
|
||||
|
||||
let response: [String: Any] = [
|
||||
"rx_bytes": bytesin,
|
||||
"tx_bytes": bytesout
|
||||
@@ -156,6 +259,10 @@ extension PacketTunnelProvider {
|
||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||
|
||||
stopHandler = completionHandler
|
||||
openVpnGatewayAddress = nil
|
||||
openVpnLocalAddress = nil
|
||||
openVpnLocalMask = nil
|
||||
lastOpenVPNSettings = nil
|
||||
if vpnReachability.isTracking {
|
||||
vpnReachability.stopTracking()
|
||||
}
|
||||
@@ -175,11 +282,99 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
||||
completionHandler: @escaping (Error?) -> Void
|
||||
) {
|
||||
guard var effectiveSettings = networkSettings else {
|
||||
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "nil settings; skipping update")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let splitType = splitTunnelType ?? 0
|
||||
|
||||
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||
openVpnLocalAddress = ipv4Settings.addresses.first
|
||||
openVpnLocalMask = ipv4Settings.subnetMasks.first
|
||||
}
|
||||
|
||||
let serverIP = openVPNAdapter.connectionInformation?.serverIP
|
||||
let configRemote = openVpnRemoteAddress
|
||||
let serverEndpoint: String? = {
|
||||
if let ip = serverIP, Self.isIPv4Address(ip) { return ip }
|
||||
if let ip = configRemote, Self.isIPv4Address(ip) { return ip }
|
||||
return effectiveSettings.tunnelRemoteAddress
|
||||
}()
|
||||
|
||||
if let serverEndpoint,
|
||||
Self.isIPv4Address(serverEndpoint),
|
||||
effectiveSettings.tunnelRemoteAddress != serverEndpoint {
|
||||
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: serverEndpoint)
|
||||
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
||||
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
||||
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
||||
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
||||
updatedSettings.mtu = effectiveSettings.mtu
|
||||
effectiveSettings = updatedSettings
|
||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to server=\(serverEndpoint)")
|
||||
} else if let serverEndpoint, !Self.isIPv4Address(serverEndpoint) {
|
||||
ovpnLog(.info, title: "Remote", message: "skip tunnelRemoteAddress override; non-ip serverEndpoint=\(serverEndpoint)")
|
||||
}
|
||||
|
||||
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
||||
// send empty string to NEDNSSettings.matchDomains
|
||||
networkSettings?.dnsSettings?.matchDomains = [""]
|
||||
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||
if dnsSettings.servers.isEmpty, !openVpnDnsServers.isEmpty {
|
||||
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
||||
newSettings.matchDomains = dnsSettings.matchDomains
|
||||
effectiveSettings.dnsSettings = newSettings
|
||||
}
|
||||
} else if !openVpnDnsServers.isEmpty {
|
||||
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
||||
effectiveSettings.dnsSettings = newSettings
|
||||
}
|
||||
|
||||
if splitTunnelType == 1 {
|
||||
effectiveSettings.dnsSettings?.matchDomains = [""]
|
||||
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||
let servers = dnsSettings.servers.joined(separator: ",")
|
||||
let domains = dnsSettings.matchDomains?.joined(separator: ",") ?? ""
|
||||
ovpnLog(.info, title: "DNS", message: "servers=[\(servers)] matchDomains=[\(domains)]")
|
||||
} else {
|
||||
ovpnLog(.error, title: "DNS", message: "dnsSettings is nil")
|
||||
}
|
||||
|
||||
let tunnelRemote = effectiveSettings.tunnelRemoteAddress
|
||||
if !tunnelRemote.isEmpty {
|
||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress=\(tunnelRemote)")
|
||||
} else if let remoteAddress = openVpnRemoteAddress {
|
||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress is empty, configRemote=\(remoteAddress)")
|
||||
}
|
||||
|
||||
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||
let included = (ipv4Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
||||
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
||||
let router: String
|
||||
#if os(macOS)
|
||||
if #available(macOS 13.0, *) {
|
||||
router = ipv4Settings.router ?? ""
|
||||
} else {
|
||||
router = ""
|
||||
}
|
||||
#else
|
||||
router = ""
|
||||
#endif
|
||||
ovpnLog(.info, title: "IPv4RoutesPre", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
||||
} else {
|
||||
ovpnLog(.error, title: "IPv4RoutesPre", message: "ipv4Settings is nil")
|
||||
}
|
||||
|
||||
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
||||
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
||||
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
||||
ovpnLog(.info, title: "IPv6RoutesPre", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
||||
}
|
||||
|
||||
if splitType == 1 {
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
|
||||
guard let splitTunnelSites else {
|
||||
@@ -195,9 +390,8 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else {
|
||||
if splitTunnelType == 2 {
|
||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else if splitType == 2 {
|
||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||
@@ -225,14 +419,418 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
destinationAddress: "\(allIPv6.address)",
|
||||
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
||||
}
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
effectiveSettings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||
} else {
|
||||
// Full tunnel: rely on adapter-provided routes.
|
||||
}
|
||||
|
||||
if let serverEndpoint,
|
||||
Self.isIPv4Address(serverEndpoint),
|
||||
let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||
let hostMask = "255.255.255.255"
|
||||
var excluded = ipv4Settings.excludedRoutes ?? []
|
||||
let alreadyExcluded = excluded.contains {
|
||||
$0.destinationAddress == serverEndpoint && $0.destinationSubnetMask == hostMask
|
||||
}
|
||||
if !alreadyExcluded {
|
||||
excluded.append(NEIPv4Route(destinationAddress: serverEndpoint, subnetMask: hostMask))
|
||||
ipv4Settings.excludedRoutes = excluded
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "excluded remoteAddress=\(serverEndpoint)")
|
||||
}
|
||||
} else if let serverEndpoint {
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "skip explicit remote exclude; non-ip server=\(serverEndpoint)")
|
||||
}
|
||||
|
||||
let localAddr = openVpnLocalAddress
|
||||
var net30Gateway: String?
|
||||
if let localAddr, let mask = openVpnLocalMask {
|
||||
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
||||
}
|
||||
var gateway = net30Gateway
|
||||
if let adapterGateway = openVPNAdapter.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
||||
if let localAddr, adapterGateway == localAddr {
|
||||
ovpnLog(.info, title: "IPv4Gateway", message: "ignore adapter gateway equal to local address=\(adapterGateway)")
|
||||
} else if let net30Gateway, net30Gateway != adapterGateway {
|
||||
ovpnLog(.info, title: "IPv4Gateway", message: "ignore mismatched adapter gateway=\(adapterGateway), using net30 peer=\(net30Gateway)")
|
||||
} else {
|
||||
gateway = adapterGateway
|
||||
}
|
||||
}
|
||||
|
||||
openVpnGatewayAddress = gateway
|
||||
if let gateway, !gateway.isEmpty {
|
||||
ovpnLog(.info, title: "IPv4Gateway", message: "gateway=\(gateway)")
|
||||
}
|
||||
#if os(macOS)
|
||||
if splitType == 0, let gateway, !gateway.isEmpty, effectiveSettings.tunnelRemoteAddress != gateway {
|
||||
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: gateway)
|
||||
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
||||
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
||||
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
||||
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
||||
updatedSettings.mtu = effectiveSettings.mtu
|
||||
effectiveSettings = updatedSettings
|
||||
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to gateway=\(gateway) on macOS full-tunnel")
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
if var ipv4Settings = effectiveSettings.ipv4Settings {
|
||||
if splitType == 0 {
|
||||
let hasNet30Mask = ipv4Settings.subnetMasks.contains("255.255.255.252")
|
||||
if hasNet30Mask {
|
||||
let normalizedMasks = Array(repeating: "255.255.255.255",
|
||||
count: ipv4Settings.subnetMasks.count)
|
||||
let normalized = NEIPv4Settings(addresses: ipv4Settings.addresses,
|
||||
subnetMasks: normalizedMasks)
|
||||
normalized.includedRoutes = ipv4Settings.includedRoutes
|
||||
normalized.excludedRoutes = ipv4Settings.excludedRoutes
|
||||
if #available(macOS 13.0, *) {
|
||||
normalized.router = ipv4Settings.router
|
||||
}
|
||||
ipv4Settings = normalized
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "normalized net30 /30 masks to /32 on macOS full-tunnel")
|
||||
}
|
||||
|
||||
if let gateway, !gateway.isEmpty {
|
||||
if #available(macOS 13.0, *) {
|
||||
ipv4Settings.router = gateway
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "set ipv4 router=\(gateway) on macOS full-tunnel")
|
||||
}
|
||||
}
|
||||
|
||||
var included = ipv4Settings.includedRoutes ?? []
|
||||
let hasDefault = included.contains {
|
||||
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
||||
}
|
||||
if hasDefault {
|
||||
included.removeAll {
|
||||
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
||||
}
|
||||
}
|
||||
let hasDef1Low = included.contains {
|
||||
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
||||
}
|
||||
let hasDef1High = included.contains {
|
||||
$0.destinationAddress == "128.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
||||
}
|
||||
if (hasDefault || openVpnRedirectGatewayDef1) && !(hasDef1Low && hasDef1High) {
|
||||
if !hasDef1Low {
|
||||
let route = NEIPv4Route(destinationAddress: "0.0.0.0", subnetMask: "128.0.0.0")
|
||||
if let gateway, !gateway.isEmpty {
|
||||
route.gatewayAddress = gateway
|
||||
}
|
||||
included.append(route)
|
||||
}
|
||||
if !hasDef1High {
|
||||
let route = NEIPv4Route(destinationAddress: "128.0.0.0", subnetMask: "128.0.0.0")
|
||||
if let gateway, !gateway.isEmpty {
|
||||
route.gatewayAddress = gateway
|
||||
}
|
||||
included.append(route)
|
||||
}
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "ensured def1 routes (/1 + /1) on macOS full-tunnel")
|
||||
}
|
||||
if let gateway, !gateway.isEmpty {
|
||||
included = included.map { route in
|
||||
let isDef1 =
|
||||
(route.destinationAddress == "0.0.0.0" && route.destinationSubnetMask == "128.0.0.0") ||
|
||||
(route.destinationAddress == "128.0.0.0" && route.destinationSubnetMask == "128.0.0.0")
|
||||
guard isDef1 else { return route }
|
||||
if route.gatewayAddress == gateway {
|
||||
return route
|
||||
}
|
||||
let updatedRoute = NEIPv4Route(destinationAddress: route.destinationAddress,
|
||||
subnetMask: route.destinationSubnetMask)
|
||||
updatedRoute.gatewayAddress = gateway
|
||||
return updatedRoute
|
||||
}
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "set gateway=\(gateway) on macOS def1 routes")
|
||||
}
|
||||
ipv4Settings.includedRoutes = included
|
||||
effectiveSettings.ipv4Settings = ipv4Settings
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||
let included = (ipv4Settings.includedRoutes ?? []).map {
|
||||
let gw = $0.gatewayAddress ?? ""
|
||||
return "\($0.destinationAddress)/\($0.destinationSubnetMask) gw=\(gw)"
|
||||
}
|
||||
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
||||
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
||||
let router: String
|
||||
#if os(macOS)
|
||||
if #available(macOS 13.0, *) {
|
||||
router = ipv4Settings.router ?? ""
|
||||
} else {
|
||||
router = ""
|
||||
}
|
||||
#else
|
||||
router = ""
|
||||
#endif
|
||||
ovpnLog(.info, title: "IPv4Routes", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
||||
} else {
|
||||
ovpnLog(.error, title: "IPv4Routes", message: "ipv4Settings is nil")
|
||||
}
|
||||
|
||||
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
||||
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
||||
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
||||
ovpnLog(.info, title: "IPv6Routes", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
||||
}
|
||||
#if os(macOS)
|
||||
if effectiveSettings.ipv6Settings != nil {
|
||||
effectiveSettings.ipv6Settings = nil
|
||||
ovpnLog(.info, title: "IPv6", message: "cleared ipv6Settings on macOS")
|
||||
}
|
||||
#endif
|
||||
|
||||
lastOpenVPNSettings = effectiveSettings
|
||||
|
||||
// Set the network settings for the current tunneling session.
|
||||
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
||||
setTunnelNetworkSettings(effectiveSettings) { error in
|
||||
if let error {
|
||||
ovpnLog(.error, title: "SetTunnelNetworkSettings", message: error.localizedDescription)
|
||||
} else {
|
||||
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "ok")
|
||||
}
|
||||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func extractDnsServers(from config: String) -> [String] {
|
||||
let lines = config.split(whereSeparator: \.isNewline)
|
||||
var servers: [String] = []
|
||||
for line in lines {
|
||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.hasPrefix("dhcp-option DNS ") {
|
||||
let parts = trimmed.split(separator: " ")
|
||||
if let last = parts.last {
|
||||
servers.append(String(last))
|
||||
}
|
||||
}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
private static func extractRemoteHost(from config: String) -> String? {
|
||||
let lines = config.split(whereSeparator: \.isNewline)
|
||||
for line in lines {
|
||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.hasPrefix("remote ") {
|
||||
let parts = trimmed.split(separator: " ")
|
||||
if parts.count >= 2 {
|
||||
return String(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func hasRedirectGatewayDef1(in config: String) -> Bool {
|
||||
let lines = config.split(whereSeparator: \.isNewline)
|
||||
for line in lines {
|
||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.hasPrefix("redirect-gateway") {
|
||||
return trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" }).contains("def1")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static func net30Peer(for address: String, mask: String) -> String? {
|
||||
guard mask == "255.255.255.252" else { return nil }
|
||||
let parts = address.split(separator: ".")
|
||||
guard parts.count == 4 else { return nil }
|
||||
var octets: [Int] = []
|
||||
for part in parts {
|
||||
guard let num = Int(part), num >= 0 && num <= 255 else { return nil }
|
||||
octets.append(num)
|
||||
}
|
||||
let ip = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
|
||||
let network = ip & ~3
|
||||
let host = ip - network
|
||||
let peerHost: Int
|
||||
switch host {
|
||||
case 1: peerHost = 2
|
||||
case 2: peerHost = 1
|
||||
default: return nil
|
||||
}
|
||||
let peerIP = network + peerHost
|
||||
return "\((peerIP >> 24) & 0xff).\((peerIP >> 16) & 0xff).\((peerIP >> 8) & 0xff).\(peerIP & 0xff)"
|
||||
}
|
||||
|
||||
private func logOpenVPNConnectionInfo() {
|
||||
guard let info = ovpnAdapter?.connectionInformation else { return }
|
||||
let message = "vpnIPv4=\(info.vpnIPv4 ?? "") gatewayIPv4=\(info.gatewayIPv4 ?? "") serverIP=\(info.serverIP ?? "") tun=\(info.tunName ?? "")"
|
||||
ovpnLog(.info, title: "ConnInfo", message: message)
|
||||
}
|
||||
|
||||
private static func normalizeInlineBlock(
|
||||
in config: String,
|
||||
tag: String,
|
||||
beginMarkers: [String],
|
||||
endMarkers: [String]
|
||||
) -> String {
|
||||
guard !beginMarkers.isEmpty, !endMarkers.isEmpty else { return config }
|
||||
|
||||
var normalizedConfig = config
|
||||
let openTag = "<\(tag)>"
|
||||
let closeTag = "</\(tag)>"
|
||||
var searchStart = normalizedConfig.startIndex
|
||||
|
||||
while let openRange = normalizedConfig.range(of: openTag, range: searchStart..<normalizedConfig.endIndex),
|
||||
let closeRange = normalizedConfig.range(of: closeTag, range: openRange.upperBound..<normalizedConfig.endIndex) {
|
||||
let rawBody = String(normalizedConfig[openRange.upperBound..<closeRange.lowerBound])
|
||||
let lines = rawBody
|
||||
.split(whereSeparator: \.isNewline)
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
var beginIndex: Int?
|
||||
var endIndex: Int?
|
||||
for (idx, line) in lines.enumerated() {
|
||||
if beginIndex == nil,
|
||||
beginMarkers.contains(where: { line.contains($0) }) {
|
||||
beginIndex = idx
|
||||
}
|
||||
if beginIndex != nil,
|
||||
endMarkers.contains(where: { line.contains($0) }) {
|
||||
endIndex = idx
|
||||
}
|
||||
}
|
||||
|
||||
if let beginIndex,
|
||||
let endIndex,
|
||||
endIndex >= beginIndex {
|
||||
let extracted = lines[beginIndex...endIndex].joined(separator: "\n")
|
||||
let replacement = "<\(tag)>\n\(extracted)\n</\(tag)>"
|
||||
normalizedConfig.replaceSubrange(openRange.lowerBound..<closeRange.upperBound, with: replacement)
|
||||
ovpnLog(.info, title: "ConfigInline", message: "tag=<\(tag)> linesIn=\(lines.count) linesOut=\(endIndex - beginIndex + 1)")
|
||||
searchStart = normalizedConfig.index(openRange.lowerBound, offsetBy: replacement.count)
|
||||
} else {
|
||||
ovpnLog(.error, title: "ConfigInline", message: "tag=<\(tag)> missing markers, keeping original body")
|
||||
searchStart = closeRange.upperBound
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedConfig
|
||||
}
|
||||
|
||||
|
||||
private static func stripUnsupportedOptions(forOpenVPNAdapter config: String) -> String {
|
||||
let unsupportedTokens: Set<String> = [
|
||||
"block-ipv6",
|
||||
"script-security",
|
||||
"up",
|
||||
"down",
|
||||
"resolv-retry",
|
||||
"persist-key",
|
||||
"persist-tun",
|
||||
"compat-mode",
|
||||
"disable-dco"
|
||||
]
|
||||
let inlineBlockTags: Set<String> = [
|
||||
"ca",
|
||||
"cert",
|
||||
"key",
|
||||
"pkcs12",
|
||||
"tls-auth",
|
||||
"tls-crypt",
|
||||
"tls-crypt-v2",
|
||||
"secret",
|
||||
"crl-verify",
|
||||
"extra-certs"
|
||||
]
|
||||
|
||||
var removed: [String: Int] = [:]
|
||||
var normalized: [String: Int] = [:]
|
||||
var output: [String] = []
|
||||
var activeInlineTag: String?
|
||||
|
||||
for rawLine in config.split(whereSeparator: \.isNewline) {
|
||||
let line = String(rawLine)
|
||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
output.append(line)
|
||||
continue
|
||||
}
|
||||
|
||||
let trimmedLowercased = trimmed.lowercased()
|
||||
|
||||
if let currentInlineTag = activeInlineTag {
|
||||
output.append(line)
|
||||
if trimmedLowercased == "</\(currentInlineTag)>" {
|
||||
activeInlineTag = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if trimmedLowercased.hasPrefix("<"),
|
||||
trimmedLowercased.hasSuffix(">"),
|
||||
!trimmedLowercased.hasPrefix("</") {
|
||||
let tagContent = String(trimmedLowercased.dropFirst().dropLast())
|
||||
let tagName = tagContent
|
||||
.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
||||
.first
|
||||
.map(String.init) ?? ""
|
||||
if inlineBlockTags.contains(tagName) {
|
||||
activeInlineTag = tagName
|
||||
output.append(line)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if trimmed.hasPrefix("#") || trimmed.hasPrefix(";") {
|
||||
output.append(line)
|
||||
continue
|
||||
}
|
||||
|
||||
let parts = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
||||
let token = parts.first.map(String.init)?.lowercased() ?? ""
|
||||
if trimmedLowercased.hasPrefix("redirect-gateway") || token.hasPrefix("redirect-gateway") {
|
||||
let hasDef1 = parts.dropFirst().contains { String($0).lowercased().hasPrefix("def1") }
|
||||
if hasDef1 {
|
||||
output.append("redirect-gateway def1")
|
||||
normalized["redirect-gateway", default: 0] += 1
|
||||
} else {
|
||||
removed["redirect-gateway", default: 0] += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if let matchedUnsupported = unsupportedTokens.first(where: { token.hasPrefix($0) }) {
|
||||
removed[matchedUnsupported, default: 0] += 1
|
||||
continue
|
||||
}
|
||||
|
||||
output.append(line)
|
||||
}
|
||||
|
||||
if !removed.isEmpty {
|
||||
let summary = removed.keys.sorted().map { "\($0)=\(removed[$0] ?? 0)" }.joined(separator: " ")
|
||||
ovpnLog(.info, title: "ConfigStrip", message: summary)
|
||||
}
|
||||
if !normalized.isEmpty {
|
||||
let summary = normalized.keys.sorted().map { "\($0)=\(normalized[$0] ?? 0)" }.joined(separator: " ")
|
||||
ovpnLog(.info, title: "ConfigNormalize", message: summary)
|
||||
}
|
||||
|
||||
return output.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func isIPv4Address(_ value: String) -> Bool {
|
||||
let parts = value.split(separator: ".")
|
||||
if parts.count != 4 { return false }
|
||||
for part in parts {
|
||||
guard let num = Int(part), num >= 0 && num <= 255 else { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Process events returned by the OpenVPN library
|
||||
@@ -250,6 +848,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
|
||||
startHandler(nil)
|
||||
self.startHandler = nil
|
||||
|
||||
logOpenVPNConnectionInfo()
|
||||
refreshOpenVPNSettingsAfterConnect()
|
||||
case .disconnected:
|
||||
guard let stopHandler = stopHandler else { return }
|
||||
|
||||
@@ -292,4 +893,41 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
// Handle log messages
|
||||
ovpnLog(.info, message: logMessage)
|
||||
}
|
||||
|
||||
func openVPNAdapterDidReceiveClockTick(_ openVPNAdapter: OpenVPNAdapter) {
|
||||
let now = Date()
|
||||
if now.timeIntervalSince(lastOpenVPNStatsLogTime) < 5 {
|
||||
return
|
||||
}
|
||||
lastOpenVPNStatsLogTime = now
|
||||
|
||||
let transport = openVPNAdapter.transportStatistics
|
||||
let iface = openVPNAdapter.interfaceStatistics
|
||||
let transportLine = "transport bytesIn=\(transport.bytesIn) bytesOut=\(transport.bytesOut) packetsIn=\(transport.packetsIn) packetsOut=\(transport.packetsOut)"
|
||||
let ifaceLine = "iface bytesIn=\(iface.bytesIn) bytesOut=\(iface.bytesOut) packetsIn=\(iface.packetsIn) packetsOut=\(iface.packetsOut) errorsIn=\(iface.errorsIn) errorsOut=\(iface.errorsOut)"
|
||||
ovpnLog(.info, title: "Stats", message: "\(transportLine) | \(ifaceLine)")
|
||||
}
|
||||
|
||||
private func refreshOpenVPNSettingsAfterConnect() {
|
||||
let localAddr = openVpnLocalAddress
|
||||
var net30Gateway: String?
|
||||
if let localAddr, let mask = openVpnLocalMask {
|
||||
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
||||
}
|
||||
var gateway = net30Gateway
|
||||
if let adapterGateway = ovpnAdapter?.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
||||
if let localAddr, adapterGateway == localAddr {
|
||||
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect ignoring adapter gateway equal to local address=\(adapterGateway)")
|
||||
} else if let net30Gateway, net30Gateway != adapterGateway {
|
||||
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect keeping net30 peer=\(net30Gateway), adapter gateway=\(adapterGateway)")
|
||||
} else {
|
||||
gateway = adapterGateway
|
||||
}
|
||||
}
|
||||
|
||||
guard let gateway, !gateway.isEmpty else { return }
|
||||
openVpnGatewayAddress = gateway
|
||||
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect gateway=\(gateway)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,6 +48,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
var splitTunnelType: Int?
|
||||
var splitTunnelSites: [String]?
|
||||
var openVpnDnsServers: [String] = []
|
||||
var openVpnRemoteAddress: String?
|
||||
var openVpnRedirectGatewayDef1 = false
|
||||
var openVpnLocalAddress: String?
|
||||
var openVpnLocalMask: String?
|
||||
var openVpnGatewayAddress: String?
|
||||
var lastOpenVPNSettings: NEPacketTunnelNetworkSettings?
|
||||
var lastOpenVPNStatsLogTime = Date.distantPast
|
||||
|
||||
let vpnReachability = OpenVPNReachability()
|
||||
|
||||
@@ -78,8 +86,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||
|
||||
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
||||
if proto == .wireguard {
|
||||
// WireGuard/AWG and OpenVPN manage network changes internally; avoid restarting the tunnel here.
|
||||
if proto == .wireguard || proto == .openvpn {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,6 +187,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||
|
||||
neLog(.info, message: "Start tunnel")
|
||||
if let vpnProto = protocolConfiguration as? NEVPNProtocol {
|
||||
if #available(iOS 14.0, macOS 11.0, *) {
|
||||
var details = "includeAllNetworks=\(vpnProto.includeAllNetworks)"
|
||||
if #available(iOS 14.2, macOS 11.0, *) {
|
||||
details += " excludeLocalNetworks=\(vpnProto.excludeLocalNetworks)"
|
||||
}
|
||||
neLog(.info, title: "Protocol", message: details)
|
||||
}
|
||||
}
|
||||
|
||||
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration
|
||||
@@ -323,6 +340,8 @@ extension WireGuardLogLevel {
|
||||
|
||||
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
private let flow: NEPacketTunnelFlow
|
||||
private var readLogCounter = 0
|
||||
private var writeLogCounter = 0
|
||||
|
||||
init(flow: NEPacketTunnelFlow) {
|
||||
self.flow = flow
|
||||
@@ -331,15 +350,98 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
|
||||
@objc(readPacketsWithCompletionHandler:)
|
||||
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||
flow.readPackets(completionHandler: completionHandler)
|
||||
flow.readPackets { packets, protocols in
|
||||
#if os(macOS)
|
||||
if self.readLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
||||
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||
let header = Self.describePacketHeader(firstPacket)
|
||||
ovpnLog(.info, title: "FlowRead", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
||||
self.readLogCounter += 1
|
||||
}
|
||||
#endif
|
||||
completionHandler(packets, protocols)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(writePackets:withProtocols:)
|
||||
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||
flow.writePackets(packets, withProtocols: protocols)
|
||||
#if os(macOS)
|
||||
if writeLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
||||
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||
let header = Self.describePacketHeader(firstPacket)
|
||||
ovpnLog(.info, title: "FlowWrite", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
||||
writeLogCounter += 1
|
||||
}
|
||||
#endif
|
||||
return flow.writePackets(packets, withProtocols: protocols)
|
||||
}
|
||||
|
||||
private static func describePacketHeader(_ packet: Data) -> String {
|
||||
guard let versionNibble = packet.first.map({ Int($0 >> 4) }) else {
|
||||
return "ip=unknown"
|
||||
}
|
||||
|
||||
if versionNibble == 4, packet.count >= 20 {
|
||||
let ihl = Int(packet[0] & 0x0f) * 4
|
||||
guard ihl >= 20, packet.count >= ihl else {
|
||||
return "ip=ipv4 malformed"
|
||||
}
|
||||
|
||||
let proto = packet[9]
|
||||
let src = "\(packet[12]).\(packet[13]).\(packet[14]).\(packet[15])"
|
||||
let dst = "\(packet[16]).\(packet[17]).\(packet[18]).\(packet[19])"
|
||||
let l4Offset = ihl
|
||||
let ports: String
|
||||
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
||||
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
||||
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
||||
ports = "sport=\(srcPort) dport=\(dstPort)"
|
||||
} else {
|
||||
ports = "sport=- dport=-"
|
||||
}
|
||||
let protoName: String
|
||||
switch proto {
|
||||
case 1: protoName = "ICMP"
|
||||
case 6: protoName = "TCP"
|
||||
case 17: protoName = "UDP"
|
||||
default: protoName = "P\(proto)"
|
||||
}
|
||||
return "ip=ipv4 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
||||
}
|
||||
|
||||
if versionNibble == 6, packet.count >= 40 {
|
||||
let proto = packet[6]
|
||||
func hex16(_ start: Int) -> String {
|
||||
let value = (UInt16(packet[start]) << 8) | UInt16(packet[start + 1])
|
||||
return String(format: "%x", value)
|
||||
}
|
||||
let src = stride(from: 8, to: 24, by: 2).map(hex16).joined(separator: ":")
|
||||
let dst = stride(from: 24, to: 40, by: 2).map(hex16).joined(separator: ":")
|
||||
let l4Offset = 40
|
||||
let ports: String
|
||||
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
||||
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
||||
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
||||
ports = "sport=\(srcPort) dport=\(dstPort)"
|
||||
} else {
|
||||
ports = "sport=- dport=-"
|
||||
}
|
||||
let protoName: String
|
||||
switch proto {
|
||||
case 58: protoName = "ICMPv6"
|
||||
case 6: protoName = "TCP"
|
||||
case 17: protoName = "UDP"
|
||||
default: protoName = "P\(proto)"
|
||||
}
|
||||
return "ip=ipv6 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
||||
}
|
||||
|
||||
return "ip=v\(versionNibble) len=\(packet.count)"
|
||||
}
|
||||
}
|
||||
|
||||
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
||||
|
||||
extension NEProviderStopReason {
|
||||
var amneziaDescription: String {
|
||||
switch self {
|
||||
|
||||
@@ -785,8 +785,38 @@ bool IosController::startOpenVPN(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
if (@available(iOS 14.0, macOS 11.0, *)) {
|
||||
int splitTunnelType = 0;
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
splitTunnelType = obj.value(config_key::splitTunnelType).toInt(0);
|
||||
}
|
||||
#if defined(MACOS_NE)
|
||||
// On macOS NE use route-based full tunnel. includeAllNetworks enables
|
||||
// policy-based drop-all mode and causes enforceRoutes to be ignored.
|
||||
tunnelProtocol.includeAllNetworks = NO;
|
||||
if (splitTunnelType == 0) {
|
||||
tunnelProtocol.enforceRoutes = YES;
|
||||
if (@available(iOS 14.2, macOS 11.0, *)) {
|
||||
tunnelProtocol.excludeLocalNetworks = YES;
|
||||
}
|
||||
}
|
||||
#else
|
||||
tunnelProtocol.includeAllNetworks = (splitTunnelType == 0);
|
||||
if (@available(iOS 14.2, macOS 11.0, *)) {
|
||||
// Keep existing iOS behavior.
|
||||
if (splitTunnelType == 0) {
|
||||
tunnelProtocol.excludeLocalNetworks = NO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
|
||||
@@ -799,7 +829,9 @@ bool IosController::startWireGuard(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
@@ -813,7 +845,9 @@ bool IosController::startXray(const QString &config)
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
||||
QByteArray configUtf8 = config.toUtf8();
|
||||
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
|
||||
tunnelProtocol.serverAddress = m_serverAddress;
|
||||
|
||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||
|
||||
@@ -62,9 +62,6 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
|
||||
}
|
||||
|
||||
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
|
||||
if (m_splitTunnelManager == nullptr)
|
||||
return;
|
||||
|
||||
if (config.m_vpnDisabledApps.length() > 0) {
|
||||
m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex);
|
||||
m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps);
|
||||
|
||||
@@ -232,6 +232,12 @@ ErrorCode OpenVpnProtocol::start()
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
}
|
||||
|
||||
m_openVpnProcess->waitForSource(5000);
|
||||
if (!m_openVpnProcess->isInitialized()) {
|
||||
qWarning() << "IpcProcess replica is not connected!";
|
||||
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
}
|
||||
m_openVpnProcess->setProgram(PermittedProcess::OpenVPN);
|
||||
QStringList arguments({
|
||||
"--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort),
|
||||
@@ -240,13 +246,13 @@ ErrorCode OpenVpnProtocol::start()
|
||||
m_openVpnProcess->setArguments(arguments);
|
||||
|
||||
qDebug() << arguments.join(" ");
|
||||
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred,
|
||||
connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred,
|
||||
[&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; });
|
||||
|
||||
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged,
|
||||
connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged,
|
||||
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
|
||||
|
||||
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this,
|
||||
connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this,
|
||||
[&]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
|
||||
|
||||
m_openVpnProcess->start();
|
||||
|
||||
@@ -53,7 +53,7 @@ private:
|
||||
void updateRouteGateway(QString line);
|
||||
void updateVpnGateway(const QString &line);
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> m_openVpnProcess;
|
||||
QSharedPointer<PrivilegedProcess> m_openVpnProcess;
|
||||
};
|
||||
|
||||
#endif // OPENVPNPROTOCOL_H
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace amnezia
|
||||
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
|
||||
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
|
||||
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
|
||||
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
|
||||
constexpr char defaultSpecialJunk1[] = "<b 0x084481800001000300000000077469636b65747306776964676574096b696e6f706f69736b0272750000010001c00c0005000100000039001806776964676574077469636b6574730679616e646578c025c0390005000100000039002b1765787465726e616c2d7469636b6574732d776964676574066166697368610679616e646578036e657400c05d000100010000001c000457fafe25>";
|
||||
constexpr char defaultSpecialJunk2[] = "";
|
||||
constexpr char defaultSpecialJunk3[] = "";
|
||||
constexpr char defaultSpecialJunk4[] = "";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "xrayprotocol.h"
|
||||
|
||||
#include "core/ipcclient.h"
|
||||
#include "ipc.h"
|
||||
#include "utilities.h"
|
||||
#include "core/networkUtilities.h"
|
||||
|
||||
@@ -10,37 +9,14 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkInterface>
|
||||
#include <QJsonDocument>
|
||||
#include <QtCore/qlogging.h>
|
||||
#include <QtCore/qobjectdefs.h>
|
||||
#include <QtCore/qprocess.h>
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
static const QString tunName = "utun22";
|
||||
#else
|
||||
static const QString tunName = "tun2";
|
||||
#endif
|
||||
|
||||
XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent)
|
||||
{
|
||||
readXrayConfiguration(configuration);
|
||||
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
|
||||
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
|
||||
m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr;
|
||||
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
|
||||
|
||||
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
|
||||
m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::config_key::hostName).toString());
|
||||
|
||||
const QString primaryDns = configuration.value(amnezia::config_key::dns1).toString();
|
||||
m_dnsServers.push_back(QHostAddress(primaryDns));
|
||||
if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) {
|
||||
const QString secondaryDns = configuration.value(amnezia::config_key::dns2).toString();
|
||||
m_dnsServers.push_back(QHostAddress(secondaryDns));
|
||||
}
|
||||
|
||||
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
|
||||
if (xrayConfiguration.isEmpty()) {
|
||||
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
|
||||
}
|
||||
m_xrayConfig = xrayConfiguration;
|
||||
m_t2sProcess = IpcClient::InterfaceTun2Socks();
|
||||
}
|
||||
|
||||
XrayProtocol::~XrayProtocol()
|
||||
@@ -52,197 +28,117 @@ XrayProtocol::~XrayProtocol()
|
||||
ErrorCode XrayProtocol::start()
|
||||
{
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
|
||||
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
return startTun2Socks();
|
||||
const ErrorCode err = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
|
||||
return ErrorCode::NoError;
|
||||
}, [] () {
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
});
|
||||
if (err != ErrorCode::NoError)
|
||||
return err;
|
||||
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
return startTun2Sock();
|
||||
}
|
||||
|
||||
ErrorCode XrayProtocol::startTun2Sock()
|
||||
{
|
||||
m_t2sProcess->start();
|
||||
|
||||
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this,
|
||||
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
|
||||
|
||||
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) {
|
||||
qDebug() << "PrivilegedProcess setConnectionState " << vpnState;
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
if (vpnState == Vpn::ConnectionState::Connected) {
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
QList<QHostAddress> dnsAddr;
|
||||
|
||||
dnsAddr.push_back(QHostAddress(m_primaryDNS));
|
||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||
if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) {
|
||||
dnsAddr.push_back(QHostAddress(m_secondaryDNS));
|
||||
}
|
||||
#ifdef Q_OS_WIN
|
||||
QThread::msleep(8000);
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
QThread::msleep(5000);
|
||||
iface->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr);
|
||||
iface->updateResolvers("utun22", dnsAddr);
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
QThread::msleep(1000);
|
||||
iface->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr);
|
||||
iface->updateResolvers("tun2", dnsAddr);
|
||||
#endif
|
||||
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
|
||||
iface->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1");
|
||||
}
|
||||
iface->StopRoutingIpv6();
|
||||
#ifdef Q_OS_WIN
|
||||
iface->updateResolvers("tun2", dnsAddr);
|
||||
#endif
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
}
|
||||
#if !defined(Q_OS_MACOS)
|
||||
if (vpnState == Vpn::ConnectionState::Disconnected) {
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
iface->deleteTun("tun2");
|
||||
iface->StartRoutingIpv6();
|
||||
iface->clearSavedRoutes();
|
||||
}
|
||||
#endif
|
||||
});
|
||||
});
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void XrayProtocol::stop()
|
||||
{
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
setConnectionState(Vpn::ConnectionState::Disconnecting);
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
qWarning() << "Failed to disable killswitch";
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
QRemoteObjectPendingReply<bool> StartRoutingIpv6Resp = iface->StartRoutingIpv6();
|
||||
if (!StartRoutingIpv6Resp.waitForFinished(1000)) {
|
||||
qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6";
|
||||
}
|
||||
|
||||
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
|
||||
if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue())
|
||||
qWarning() << "Failed to start routing ipv6";
|
||||
QRemoteObjectPendingReply<bool> restoreResolvers = iface->restoreResolvers();
|
||||
if (!restoreResolvers.waitForFinished(1000)) {
|
||||
qWarning() << "XrayProtocol::stop(): Failed to restore resolvers";
|
||||
}
|
||||
|
||||
auto restoreResolvers = iface->restoreResolvers();
|
||||
if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue())
|
||||
qWarning() << "Failed to restore resolvers";
|
||||
|
||||
auto deleteTun = iface->deleteTun(tunName);
|
||||
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
|
||||
qWarning() << "Failed to delete tun";
|
||||
|
||||
auto xrayStop = iface->xrayStop();
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
|
||||
qWarning() << "Failed to stop xray";
|
||||
#if !defined(Q_OS_MACOS)
|
||||
QRemoteObjectPendingReply<bool> deleteTunResp = iface->deleteTun("tun2");
|
||||
if (!deleteTunResp.waitForFinished(1000)) {
|
||||
qWarning() << "XrayProtocol::stop(): Failed to delete tun";
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
iface->xrayStop();
|
||||
});
|
||||
|
||||
if (m_tun2socksProcess) {
|
||||
m_tun2socksProcess->blockSignals(true);
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
m_tun2socksProcess->terminate();
|
||||
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
|
||||
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
|
||||
qWarning() << "Failed to terminate tun2socks. Killing the process...";
|
||||
m_tun2socksProcess->kill();
|
||||
}
|
||||
#else
|
||||
// terminate does not do anything useful on Windows
|
||||
// so just kill the process
|
||||
m_tun2socksProcess->kill();
|
||||
#endif
|
||||
|
||||
m_tun2socksProcess->close();
|
||||
m_tun2socksProcess.reset();
|
||||
if (m_t2sProcess) {
|
||||
m_t2sProcess->stop();
|
||||
QThread::msleep(200);
|
||||
}
|
||||
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
}
|
||||
|
||||
ErrorCode XrayProtocol::startTun2Socks()
|
||||
void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration)
|
||||
{
|
||||
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
|
||||
if (!m_tun2socksProcess->waitForSource()) {
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
|
||||
if (xrayConfiguration.isEmpty()) {
|
||||
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
|
||||
}
|
||||
|
||||
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
|
||||
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" });
|
||||
|
||||
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, [this]() {
|
||||
auto readAllStandardOutput = m_tun2socksProcess->readAllStandardOutput();
|
||||
if (!readAllStandardOutput.waitForFinished()) {
|
||||
qWarning() << "Failed to read output from tun2socks";
|
||||
return;
|
||||
}
|
||||
|
||||
const QString line = readAllStandardOutput.returnValue();
|
||||
|
||||
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
|
||||
qDebug() << "[tun2socks]:" << line;
|
||||
|
||||
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
|
||||
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
|
||||
|
||||
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
|
||||
stop();
|
||||
setLastError(res);
|
||||
} else {
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
}
|
||||
}
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
if (exitStatus == QProcess::ExitStatus::CrashExit) {
|
||||
qCritical() << "Tun2socks process crashed!";
|
||||
} else {
|
||||
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
|
||||
}
|
||||
stop();
|
||||
setLastError(ErrorCode::Tun2SockExecutableCrashed);
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
m_tun2socksProcess->start();
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode XrayProtocol::setupRouting() {
|
||||
return IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
|
||||
#ifdef Q_OS_WIN
|
||||
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
|
||||
#endif
|
||||
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
|
||||
if (!createTun.waitForFinished() || !createTun.returnValue()) {
|
||||
qCritical() << "Failed to assign IP address for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
|
||||
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
|
||||
qCritical() << "Failed to set DNS resolvers for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
int vpnAdapterIndex = -1;
|
||||
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
|
||||
for (auto& netInterface : netInterfaces) {
|
||||
for (auto& address : netInterface.addressEntries()) {
|
||||
if (m_vpnLocalAddress == address.ip().toString())
|
||||
vpnAdapterIndex = netInterface.index();
|
||||
}
|
||||
}
|
||||
#else
|
||||
static const int vpnAdapterIndex = 0;
|
||||
#endif
|
||||
const bool killSwitchEnabled = QVariant(m_rawConfig.value(config_key::killSwitchOption).toString()).toBool();
|
||||
if (killSwitchEnabled) {
|
||||
if (vpnAdapterIndex != -1) {
|
||||
QJsonObject config = m_rawConfig;
|
||||
config.insert("vpnServer", m_remoteAddress);
|
||||
|
||||
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
|
||||
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
|
||||
qCritical() << "Failed to enable killswitch";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
} else
|
||||
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
|
||||
}
|
||||
|
||||
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
|
||||
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
|
||||
|
||||
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
|
||||
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
|
||||
qCritical() << "Failed to set routes for TUN";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
|
||||
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
|
||||
qCritical() << "Failed to disable IPv6 routing";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
|
||||
QJsonObject config = m_rawConfig;
|
||||
config.insert("inetAdapterIndex", inetAdapterIndex);
|
||||
config.insert("vpnAdapterIndex", vpnAdapterIndex);
|
||||
config.insert("vpnGateway", m_vpnGateway);
|
||||
config.insert("vpnServer", m_remoteAddress);
|
||||
|
||||
auto enablePeerTraffic = iface->enablePeerTraffic(config);
|
||||
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
|
||||
qCritical() << "Failed to enable peer traffic";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
} else
|
||||
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
|
||||
#endif
|
||||
return ErrorCode::NoError;
|
||||
},
|
||||
[] () {
|
||||
return ErrorCode::AmneziaServiceConnectionFailed;
|
||||
});
|
||||
m_xrayConfig = xrayConfiguration;
|
||||
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
|
||||
m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString();
|
||||
m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "core/ipcclient.h"
|
||||
#include "vpnprotocol.h"
|
||||
#include "settings.h"
|
||||
#include <QtCore/qsharedpointer.h>
|
||||
|
||||
class XrayProtocol : public VpnProtocol
|
||||
{
|
||||
@@ -15,18 +14,19 @@ public:
|
||||
virtual ~XrayProtocol() override;
|
||||
|
||||
ErrorCode start() override;
|
||||
ErrorCode startTun2Sock();
|
||||
void stop() override;
|
||||
|
||||
private:
|
||||
ErrorCode setupRouting();
|
||||
ErrorCode startTun2Socks();
|
||||
|
||||
void readXrayConfiguration(const QJsonObject &configuration);
|
||||
|
||||
QJsonObject m_xrayConfig;
|
||||
Settings::RouteMode m_routeMode;
|
||||
QList<QHostAddress> m_dnsServers;
|
||||
QString m_remoteAddress;
|
||||
|
||||
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
|
||||
QString m_primaryDNS;
|
||||
QString m_secondaryDNS;
|
||||
#ifndef Q_OS_IOS
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> m_t2sProcess;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
<file>ui/qml/Components/AdLabel.qml</file>
|
||||
<file>ui/qml/Components/ConnectButton.qml</file>
|
||||
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
|
||||
<file>ui/qml/Components/GamepadLoader.qml</file>
|
||||
<file>ui/qml/Components/HomeContainersListView.qml</file>
|
||||
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
||||
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
||||
|
||||
@@ -21,5 +21,4 @@ if [ "$(systemctl is-active docker)" != "active" ]; then \
|
||||
sleep 5; sudo systemctl start docker; sleep 5;\
|
||||
fi;\
|
||||
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
|
||||
docker --version;\
|
||||
uname -sr
|
||||
docker --version
|
||||
|
||||
@@ -539,26 +539,6 @@ QString Settings::getGatewayEndpoint(bool isTestPurchase)
|
||||
return isTestPurchase ? DEV_AGW_ENDPOINT : m_gatewayEndpoint;
|
||||
}
|
||||
|
||||
QString Settings::getDevProxyStorageEndpoint() const
|
||||
{
|
||||
return value("Conf/devProxyStorageEndpoint", "").toString();
|
||||
}
|
||||
|
||||
void Settings::setDevProxyStorageEndpoint(const QString &endpoint)
|
||||
{
|
||||
setValue("Conf/devProxyStorageEndpoint", endpoint);
|
||||
}
|
||||
|
||||
QString Settings::getDevProxyUrl() const
|
||||
{
|
||||
return value("Conf/devProxyUrl", "").toString();
|
||||
}
|
||||
|
||||
void Settings::setDevProxyUrl(const QString &url)
|
||||
{
|
||||
setValue("Conf/devProxyUrl", url);
|
||||
}
|
||||
|
||||
bool Settings::isDevGatewayEnv(bool isTestPurchase)
|
||||
{
|
||||
return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();
|
||||
|
||||
@@ -94,15 +94,6 @@ public:
|
||||
setValue("Conf/startMinimized", enabled);
|
||||
}
|
||||
|
||||
bool isNewsNotifications() const
|
||||
{
|
||||
return value("Conf/newsNotifications", true).toBool();
|
||||
}
|
||||
void setNewsNotifications(bool enabled)
|
||||
{
|
||||
setValue("Conf/newsNotifications", enabled);
|
||||
}
|
||||
|
||||
bool isSaveLogs() const
|
||||
{
|
||||
return value("Conf/saveLogs", false).toBool();
|
||||
@@ -233,10 +224,6 @@ public:
|
||||
void setGatewayEndpoint(const QString &endpoint);
|
||||
void setDevGatewayEndpoint();
|
||||
QString getGatewayEndpoint(bool isTestPurchase = false);
|
||||
QString getDevProxyStorageEndpoint() const;
|
||||
void setDevProxyStorageEndpoint(const QString &endpoint);
|
||||
QString getDevProxyUrl() const;
|
||||
void setDevProxyUrl(const QString &url);
|
||||
bool isDevGatewayEnv(bool isTestPurchase = false);
|
||||
void toggleDevGatewayEnv(bool enabled);
|
||||
|
||||
|
||||
@@ -382,51 +382,6 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
}
|
||||
|
||||
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
QEventLoop waitProducts;
|
||||
bool productsFetched = false;
|
||||
QString productPrice;
|
||||
QString productCurrency;
|
||||
|
||||
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
|
||||
[&](const QList<QVariantMap> &products,
|
||||
const QStringList &invalidIds,
|
||||
const QString &errorString) {
|
||||
if (!errorString.isEmpty() || products.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
|
||||
} else {
|
||||
const auto &product = products.first();
|
||||
productPrice = product.value("price").toString();
|
||||
productCurrency = product.value("currencyCode").toString();
|
||||
productsFetched = true;
|
||||
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
|
||||
}
|
||||
waitProducts.quit();
|
||||
});
|
||||
waitProducts.exec();
|
||||
|
||||
if (productsFetched && !productPrice.isEmpty()) {
|
||||
QJsonArray services = data.value("services").toArray();
|
||||
for (int i = 0; i < services.size(); ++i) {
|
||||
QJsonObject service = services[i].toObject();
|
||||
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
|
||||
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
||||
QString formattedPrice = productPrice;
|
||||
if (!productCurrency.isEmpty()) {
|
||||
formattedPrice += " " + productCurrency;
|
||||
}
|
||||
serviceInfo["price"] = formattedPrice;
|
||||
service[configKey::serviceInfo] = serviceInfo;
|
||||
services[i] = service;
|
||||
data["services"] = services;
|
||||
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
m_apiServicesModel->updateModel(data);
|
||||
if (m_apiServicesModel->rowCount() > 0) {
|
||||
m_apiServicesModel->setServiceIndex(0);
|
||||
@@ -769,8 +724,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
||||
#endif
|
||||
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled(), m_settings->getDevProxyStorageEndpoint(),
|
||||
m_settings->getDevProxyUrl());
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||
auto installationUuid = m_settings->getInstallationUuid(true);
|
||||
@@ -995,7 +949,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
|
||||
bool isTestPurchase)
|
||||
{
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(),
|
||||
m_settings->getDevProxyStorageEndpoint(), m_settings->getDevProxyUrl());
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,7 @@ void ApiNewsController::fetchNews(bool showError)
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled(),
|
||||
m_settings->getDevProxyStorageEndpoint(),
|
||||
m_settings->getDevProxyUrl());
|
||||
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
||||
|
||||
|
||||
@@ -57,8 +57,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
|
||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(),
|
||||
m_settings->getDevProxyStorageEndpoint(), m_settings->getDevProxyUrl());
|
||||
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
|
||||
|
||||
@@ -291,8 +291,6 @@ void ImportController::processNativeWireGuardConfig()
|
||||
clientProtocolConfig[config_key::cookieReplyPacketJunkSize] = "0";
|
||||
clientProtocolConfig[config_key::transportPacketJunkSize] = "0";
|
||||
|
||||
clientProtocolConfig[config_key::specialJunk1] = protocols::awg::defaultSpecialJunk1;
|
||||
|
||||
clientProtocolConfig[config_key::isObfuscationEnabled] = true;
|
||||
|
||||
serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson());
|
||||
|
||||
@@ -308,15 +308,6 @@ void SettingsController::toggleStartMinimized(bool enable)
|
||||
emit startMinimizedChanged();
|
||||
}
|
||||
|
||||
bool SettingsController::isNewsNotificationsEnabled()
|
||||
{
|
||||
return m_settings->isNewsNotifications();
|
||||
}
|
||||
void SettingsController::toggleNewsNotificationsEnabled(bool enable)
|
||||
{
|
||||
m_settings->setNewsNotifications(enable);
|
||||
}
|
||||
|
||||
bool SettingsController::isScreenshotsEnabled()
|
||||
{
|
||||
return m_settings->isScreenshotsEnabled();
|
||||
@@ -426,28 +417,6 @@ QString SettingsController::getGatewayEndpoint()
|
||||
return m_settings->isDevGatewayEnv() ? "Dev endpoint" : m_settings->getGatewayEndpoint();
|
||||
}
|
||||
|
||||
void SettingsController::setDevProxyStorageEndpoint(const QString &endpoint)
|
||||
{
|
||||
m_settings->setDevProxyStorageEndpoint(endpoint);
|
||||
emit devProxyStorageEndpointChanged(endpoint);
|
||||
}
|
||||
|
||||
QString SettingsController::getDevProxyStorageEndpoint()
|
||||
{
|
||||
return m_settings->getDevProxyStorageEndpoint();
|
||||
}
|
||||
|
||||
void SettingsController::setDevProxyUrl(const QString &url)
|
||||
{
|
||||
m_settings->setDevProxyUrl(url);
|
||||
emit devProxyUrlChanged(url);
|
||||
}
|
||||
|
||||
QString SettingsController::getDevProxyUrl()
|
||||
{
|
||||
return m_settings->getDevProxyUrl();
|
||||
}
|
||||
|
||||
bool SettingsController::isDevGatewayEnv()
|
||||
{
|
||||
return m_settings->isDevGatewayEnv();
|
||||
|
||||
@@ -30,9 +30,6 @@ public:
|
||||
Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled)
|
||||
Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged)
|
||||
Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged)
|
||||
Q_PROPERTY(QString devProxyStorageEndpoint READ getDevProxyStorageEndpoint WRITE setDevProxyStorageEndpoint NOTIFY
|
||||
devProxyStorageEndpointChanged)
|
||||
Q_PROPERTY(QString devProxyUrl READ getDevProxyUrl WRITE setDevProxyUrl NOTIFY devProxyUrlChanged)
|
||||
|
||||
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
|
||||
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
|
||||
@@ -76,9 +73,6 @@ public slots:
|
||||
bool isStartMinimizedEnabled();
|
||||
void toggleStartMinimized(bool enable);
|
||||
|
||||
bool isNewsNotificationsEnabled();
|
||||
void toggleNewsNotificationsEnabled(bool enable);
|
||||
|
||||
bool isScreenshotsEnabled();
|
||||
void toggleScreenshotsEnabled(bool enable);
|
||||
|
||||
@@ -101,10 +95,6 @@ public slots:
|
||||
void resetGatewayEndpoint();
|
||||
void setGatewayEndpoint(const QString &endpoint);
|
||||
QString getGatewayEndpoint();
|
||||
void setDevProxyStorageEndpoint(const QString &endpoint);
|
||||
QString getDevProxyStorageEndpoint();
|
||||
void setDevProxyUrl(const QString &url);
|
||||
QString getDevProxyUrl();
|
||||
bool isDevGatewayEnv();
|
||||
void toggleDevGatewayEnv(bool enabled);
|
||||
|
||||
@@ -143,8 +133,6 @@ signals:
|
||||
void devModeEnabled();
|
||||
void gatewayEndpointChanged(const QString &endpoint);
|
||||
void devGatewayEnvChanged(bool enabled);
|
||||
void devProxyStorageEndpointChanged(const QString &endpoint);
|
||||
void devProxyUrlChanged(const QString &url);
|
||||
|
||||
void imeHeightChanged(int height);
|
||||
void safeAreaTopMarginChanged();
|
||||
|
||||
@@ -112,11 +112,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
if (price == "free") {
|
||||
return tr("Free");
|
||||
}
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
return tr("%1 $").arg(price);
|
||||
#else
|
||||
return tr("%1 $/month").arg(price);
|
||||
#endif
|
||||
}
|
||||
case EndDateRole: {
|
||||
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import QtQuick
|
||||
import QtGamepadLegacy
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias gamepad: gamepad
|
||||
property alias gamepadKeyNav: gamepadKeyNav
|
||||
|
||||
Gamepad {
|
||||
id: gamepad
|
||||
deviceId: GamepadManager.connectedGamepads.length > 0 ? GamepadManager.connectedGamepads[0] : -1
|
||||
|
||||
onButtonStartChanged: {
|
||||
if (buttonStart) {
|
||||
ServersModel.setProcessedServerIndex(ServersModel.defaultIndex)
|
||||
ConnectionController.connectButtonClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
gamepad: gamepad
|
||||
active: true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GamepadManager
|
||||
function onConnectedGamepadsChanged() {
|
||||
if (GamepadManager.connectedGamepads.length > 0) {
|
||||
gamepad.deviceId = GamepadManager.connectedGamepads[0]
|
||||
} else {
|
||||
gamepad.deviceId = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,11 +111,11 @@ Button {
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
if (root.pressed) {
|
||||
return root.pressedColor
|
||||
return pressedColor
|
||||
}
|
||||
return root.hovered ? root.hoveredColor : root.defaultColor
|
||||
return root.hovered ? hoveredColor : defaultColor
|
||||
} else {
|
||||
return root.disabledColor
|
||||
return disabledColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,55 +49,6 @@ Item {
|
||||
return drawerContent.state === stateName
|
||||
}
|
||||
|
||||
function isDrawerType2(obj) {
|
||||
return obj && typeof obj.drawerExpandedStateName !== "undefined" &&
|
||||
typeof obj.drawerCollapsedStateName !== "undefined"
|
||||
}
|
||||
|
||||
function isDescendantOfDrawer(obj) {
|
||||
var current = obj
|
||||
while (current && current !== root.parent) {
|
||||
if (isDrawerType2(current)) {
|
||||
return true
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function findComponent(obj, typeCtor) {
|
||||
if (!obj)
|
||||
return null
|
||||
|
||||
if (isDrawerType2(obj) || isDescendantOfDrawer(obj))
|
||||
return null
|
||||
|
||||
if (obj instanceof typeCtor)
|
||||
return obj
|
||||
|
||||
if (obj.children && obj.children.length > 0) {
|
||||
for (var i = 0; i < obj.children.length; i++) {
|
||||
var matchingChildren = findComponent(obj.children[i], typeCtor)
|
||||
if (matchingChildren) return matchingChildren
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contentItem) {
|
||||
var matchingContentItem = findComponent(obj.contentItem, typeCtor)
|
||||
if (matchingContentItem) return matchingContentItem
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function setParentInteractive(value) {
|
||||
var flickableType = findComponent(root.parent, Flickable)
|
||||
var listViewType = findComponent(root.parent, ListView)
|
||||
|
||||
if (flickableType) flickableType.interactive = value
|
||||
if (listViewType) listViewType.interactive = value
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Qt.application
|
||||
|
||||
@@ -142,8 +93,6 @@ Item {
|
||||
|
||||
aboutToHide()
|
||||
|
||||
setParentInteractive(true)
|
||||
|
||||
closed()
|
||||
}
|
||||
|
||||
@@ -169,8 +118,6 @@ Item {
|
||||
|
||||
root.aboutToShow()
|
||||
|
||||
setParentInteractive(false)
|
||||
|
||||
root.opened()
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,6 @@ Item {
|
||||
implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: root.enabled
|
||||
@@ -298,13 +296,13 @@ Item {
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
|
||||
if (clickedFunction && typeof clickedFunction === "function") {
|
||||
clickedFunction()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (!rightImageSource && clickedFunction && typeof clickedFunction === "function") {
|
||||
if (clickedFunction && typeof clickedFunction === "function") {
|
||||
clickedFunction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ Item {
|
||||
|
||||
property string buttonText
|
||||
property string buttonImageSource
|
||||
property string buttonImageColor: AmneziaStyle.color.midnightBlack
|
||||
property string buttonBackgroundColor: AmneziaStyle.color.paleGray
|
||||
property string buttonHoveredColor: AmneziaStyle.color.lightGray
|
||||
property var clickedFunc
|
||||
|
||||
property alias textField: textField
|
||||
@@ -67,7 +70,7 @@ Item {
|
||||
border.width: 1
|
||||
|
||||
Behavior on border.color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
PropertyAnimation { duration: 100 }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -121,7 +124,7 @@ Item {
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundDisabledColor
|
||||
color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
@@ -186,6 +189,14 @@ Item {
|
||||
focusPolicy: Qt.NoFocus
|
||||
text: root.buttonText
|
||||
leftImageSource: root.buttonImageSource
|
||||
leftImageColor: root.buttonImageColor
|
||||
|
||||
defaultColor: root.buttonBackgroundColor
|
||||
hoveredColor: root.buttonHoveredColor
|
||||
pressedColor: root.buttonHoveredColor
|
||||
disabledColor: AmneziaStyle.color.transparent
|
||||
|
||||
borderWidth: 0
|
||||
|
||||
anchors.top: content.top
|
||||
anchors.bottom: content.bottom
|
||||
@@ -193,7 +204,7 @@ Item {
|
||||
|
||||
height: content.implicitHeight
|
||||
width: content.implicitHeight
|
||||
squareLeftSide: true
|
||||
squareLeftSide: false
|
||||
|
||||
clickedFunc: function() {
|
||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||
|
||||
@@ -73,50 +73,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Proxy storage endpoint")
|
||||
textField.text: SettingsController.devProxyStorageEndpoint
|
||||
|
||||
buttonImageSource: textField.text !== "" ? "qrc:/images/controls/refresh-cw.svg" : ""
|
||||
|
||||
clickedFunc: function() {
|
||||
SettingsController.devProxyStorageEndpoint = ""
|
||||
}
|
||||
|
||||
textField.onEditingFinished: {
|
||||
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
|
||||
if (textField.text !== SettingsController.devProxyStorageEndpoint) {
|
||||
SettingsController.devProxyStorageEndpoint = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Proxy endpoint")
|
||||
textField.text: SettingsController.devProxyUrl
|
||||
|
||||
buttonImageSource: textField.text !== "" ? "qrc:/images/controls/refresh-cw.svg" : ""
|
||||
|
||||
clickedFunc: function() {
|
||||
SettingsController.devProxyUrl = ""
|
||||
}
|
||||
|
||||
textField.onEditingFinished: {
|
||||
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
|
||||
if (textField.text !== SettingsController.devProxyUrl) {
|
||||
SettingsController.devProxyUrl = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
|
||||
@@ -140,16 +140,6 @@ PageType {
|
||||
ListElement { name : "aes-128-gcm" }
|
||||
}
|
||||
|
||||
function updateSelectedIndex() {
|
||||
cipherDropDown.text = cipher
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipher) {
|
||||
selectedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clickedFunction: function() {
|
||||
cipherDropDown.text = selectedText
|
||||
cipher = cipherDropDown.text
|
||||
@@ -157,14 +147,13 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
cipherDropDown.text = cipher
|
||||
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
cipherListView.updateSelectedIndex()
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||
selectedIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,16 +192,6 @@ PageType {
|
||||
ListElement { name : qsTr("SHA1") }
|
||||
}
|
||||
|
||||
function updateSelectedIndex() {
|
||||
hashDropDown.text = hash
|
||||
for (var i = 0; i < hashListView.model.count; i++) {
|
||||
if (hashListView.model.get(i).name === hash) {
|
||||
selectedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clickedFunction: function() {
|
||||
hashDropDown.text = selectedText
|
||||
hash = hashDropDown.text
|
||||
@@ -209,14 +199,13 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
hashDropDown.text = hash
|
||||
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
hashListView.updateSelectedIndex()
|
||||
for (var i = 0; i < hashListView.model.count; i++) {
|
||||
if (hashListView.model.get(i).name === hashDropDown.text) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,16 +242,6 @@ PageType {
|
||||
ListElement { name : qsTr("none") }
|
||||
}
|
||||
|
||||
function updateSelectedIndex() {
|
||||
cipherDropDown.text = cipher
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipher) {
|
||||
selectedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clickedFunction: function() {
|
||||
cipherDropDown.text = selectedText
|
||||
cipher = cipherDropDown.text
|
||||
@@ -270,14 +249,13 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
cipherDropDown.text = cipher
|
||||
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
cipherListView.updateSelectedIndex()
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,16 +109,6 @@ PageType {
|
||||
ListElement { name : "aes-128-gcm" }
|
||||
}
|
||||
|
||||
function updateSelectedIndex() {
|
||||
cipherDropDown.text = cipher
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipher) {
|
||||
selectedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clickedFunction: function() {
|
||||
cipherDropDown.text = selectedText
|
||||
cipher = cipherDropDown.text
|
||||
@@ -126,14 +116,13 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateSelectedIndex()
|
||||
}
|
||||
}
|
||||
cipherDropDown.text = cipher
|
||||
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onDataChanged() {
|
||||
cipherListView.updateSelectedIndex()
|
||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ PageType {
|
||||
id: news
|
||||
|
||||
property string title: qsTr("News & Notifications")
|
||||
readonly property string leftImagePath: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled() ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||
property bool isVisible: ServersModel.hasServersFromGatewayApi
|
||||
readonly property var clickedHandler: function() {
|
||||
if (!ServersModel.hasServersFromGatewayApi) {
|
||||
|
||||
@@ -224,6 +224,7 @@ PageType {
|
||||
height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
|
||||
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
opacity: 0.8
|
||||
|
||||
RowLayout {
|
||||
id: addAppButton
|
||||
|
||||
@@ -168,29 +168,6 @@ PageType {
|
||||
DividerType {
|
||||
visible: !GC.isMobile()
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
id: switcherNewsNotificationEnabled
|
||||
|
||||
visible: ServersModel.hasServersFromGatewayApi
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
text: qsTr("News Notification")
|
||||
descriptionText: qsTr("Show notification icon when has unread news")
|
||||
|
||||
checked: SettingsController.isNewsNotificationsEnabled()
|
||||
onToggled: function() {
|
||||
if (checked !== SettingsController.isNewsNotificationsEnabled()) {
|
||||
SettingsController.toggleNewsNotificationsEnabled(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: !GC.isMobile()
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
|
||||
@@ -240,6 +240,7 @@ PageType {
|
||||
height: addSiteButton.implicitHeight + 48
|
||||
|
||||
color: AmneziaStyle.color.midnightBlack
|
||||
opacity: 0.8
|
||||
|
||||
RowLayout {
|
||||
id: addSiteButton
|
||||
|
||||
@@ -97,32 +97,16 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
||||
text: qsTr("Connect")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
@@ -137,37 +121,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
||||
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,6 @@ PageType {
|
||||
|
||||
onClicked: function() {
|
||||
isEasySetup = true
|
||||
checked = true
|
||||
var defaultContainerProto = ContainerProps.defaultProtocol(dockerContainer)
|
||||
|
||||
listView.dockerContainer = dockerContainer
|
||||
|
||||
@@ -383,7 +383,7 @@ PageType {
|
||||
objectName: "settingsTabButton"
|
||||
|
||||
isSelected: tabBar.currentIndex === 2
|
||||
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled()) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
|
||||
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
|
||||
Binding {
|
||||
target: settingsTabButton
|
||||
property: "defaultColor"
|
||||
|
||||
@@ -83,11 +83,6 @@ Window {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Qt.platform.os === "android"
|
||||
source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
objectName: "pageControllerConnections"
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
#include <configurators/shadowsocks_configurator.h>
|
||||
#include <configurators/wireguard_configurator.h>
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "core/controllers/serverController.h"
|
||||
#include "core/controllers/vpnConfigurationController.h"
|
||||
#endif
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/ipcclient.h"
|
||||
#include <protocols/wireguardprotocol.h>
|
||||
@@ -34,6 +39,54 @@
|
||||
#include "core/networkUtilities.h"
|
||||
#include "vpnconnection.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
static bool openVpnConfigHasInlineBlock(const QString &config, const QString &openTag, const QString &closeTag, const QString &marker)
|
||||
{
|
||||
const int start = config.indexOf(openTag);
|
||||
if (start < 0) {
|
||||
return false;
|
||||
}
|
||||
const int end = config.indexOf(closeTag, start + openTag.size());
|
||||
if (end < 0 || end <= start) {
|
||||
return false;
|
||||
}
|
||||
const QString block = config.mid(start + openTag.size(), end - (start + openTag.size()));
|
||||
return block.contains(marker);
|
||||
}
|
||||
|
||||
static bool openVpnConfigHasClientCredentials(const QString &config)
|
||||
{
|
||||
const bool hasCert = openVpnConfigHasInlineBlock(config, "<cert>", "</cert>", "BEGIN CERTIFICATE");
|
||||
const bool hasKey = openVpnConfigHasInlineBlock(config, "<key>", "</key>", "BEGIN PRIVATE KEY")
|
||||
|| openVpnConfigHasInlineBlock(config, "<key>", "</key>", "BEGIN RSA PRIVATE KEY");
|
||||
return hasCert && hasKey;
|
||||
}
|
||||
|
||||
static QString openVpnConfigFromProtocolConfig(const QJsonObject &protocolConfig)
|
||||
{
|
||||
const QString directConfig = protocolConfig.value(config_key::config).toString();
|
||||
if (!directConfig.isEmpty()) {
|
||||
return directConfig;
|
||||
}
|
||||
|
||||
const QString lastConfig = protocolConfig.value(config_key::last_config).toString();
|
||||
if (lastConfig.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QJsonDocument lastConfigDoc = QJsonDocument::fromJson(lastConfig.toUtf8());
|
||||
if (!lastConfigDoc.isObject()) {
|
||||
return {};
|
||||
}
|
||||
return lastConfigDoc.object().value(config_key::config).toString();
|
||||
}
|
||||
|
||||
static bool openVpnConfigUsesUserPass(const QString &config)
|
||||
{
|
||||
return config.contains("auth-user-pass");
|
||||
}
|
||||
#endif
|
||||
|
||||
VpnConnection::VpnConnection(std::shared_ptr<Settings> settings, QObject *parent)
|
||||
: QObject(parent), m_settings(settings), m_checkTimer(new QTimer(this))
|
||||
{
|
||||
@@ -99,7 +152,21 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container != DockerContainer::Ipsec) {
|
||||
if (startNetworkCheckIfReady()) {
|
||||
m_pendingNetworkCheck = false;
|
||||
} else {
|
||||
m_pendingNetworkCheck = true;
|
||||
qWarning() << "Deferring startNetworkCheck; missing gateway/local address"
|
||||
<< m_vpnProtocol->vpnGateway() << m_vpnProtocol->vpnLocalAddress();
|
||||
}
|
||||
} else {
|
||||
m_pendingNetworkCheck = false;
|
||||
}
|
||||
|
||||
} else if (state == Vpn::ConnectionState::Error) {
|
||||
m_pendingNetworkCheck = false;
|
||||
iface->flushDns();
|
||||
|
||||
if (m_settings->isSitesSplitTunnelingEnabled()) {
|
||||
@@ -107,6 +174,12 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
iface->clearSavedRoutes();
|
||||
}
|
||||
}
|
||||
} else if (state == Vpn::ConnectionState::Connecting) {
|
||||
|
||||
} else if (state == Vpn::ConnectionState::Disconnected) {
|
||||
m_pendingNetworkCheck = false;
|
||||
auto result = iface->stopNetworkCheck();
|
||||
result.waitForFinished(3000);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
@@ -245,7 +318,7 @@ ErrorCode VpnConnection::lastError() const
|
||||
void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container,
|
||||
const QJsonObject &vpnConfiguration)
|
||||
{
|
||||
qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is")
|
||||
qDebug() << QString("ConnectToVpn, Server index is %1, container is %2, route mode is")
|
||||
.arg(serverIndex)
|
||||
.arg(ContainerProps::containerToString(container))
|
||||
<< m_settings->routeMode();
|
||||
@@ -253,7 +326,11 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
|
||||
m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName);
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Connecting);
|
||||
|
||||
m_pendingNetworkCheck = false;
|
||||
m_vpnConfiguration = vpnConfiguration;
|
||||
m_serverIndex = serverIndex;
|
||||
m_serverCredentials = credentials;
|
||||
m_dockerContainer = container;
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_vpnProtocol) {
|
||||
@@ -279,6 +356,49 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
|
||||
|
||||
m_vpnProtocol.reset(androidVpnProtocol);
|
||||
#elif defined Q_OS_IOS || defined(MACOS_NE)
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
if (ContainerProps::protocolsForContainer(container).contains(Proto::OpenVpn)) {
|
||||
QJsonObject openVpnConfig = m_vpnConfiguration.value(ProtocolProps::key_proto_config_data(Proto::OpenVpn)).toObject();
|
||||
const bool isThirdParty = openVpnConfig.value(config_key::isThirdPartyConfig).toBool(false);
|
||||
const QString config = openVpnConfigFromProtocolConfig(openVpnConfig);
|
||||
const bool configHasCreds = !config.isEmpty() && openVpnConfigHasClientCredentials(config);
|
||||
const bool configAllowsUserPass = !config.isEmpty() && openVpnConfigUsesUserPass(config);
|
||||
|
||||
if (!isThirdParty && (!configHasCreds && !configAllowsUserPass)) {
|
||||
if (!credentials.isValid()) {
|
||||
qWarning() << "Missing OpenVPN client credentials and no valid server credentials to regenerate config";
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
|
||||
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
|
||||
QJsonObject containerConfig = m_settings->containerConfig(serverIndex, container);
|
||||
const QString storedConfig =
|
||||
openVpnConfigFromProtocolConfig(containerConfig.value(ProtocolProps::protoToString(Proto::OpenVpn)).toObject());
|
||||
const bool storedHasCreds = !storedConfig.isEmpty() && openVpnConfigHasClientCredentials(storedConfig);
|
||||
if (!storedHasCreds) {
|
||||
const ErrorCode regenError =
|
||||
vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig);
|
||||
if (regenError != ErrorCode::NoError) {
|
||||
qWarning() << "Failed to regenerate OpenVPN client config:" << regenError;
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_settings->setContainerConfig(serverIndex, container, containerConfig);
|
||||
|
||||
const QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
|
||||
const QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
|
||||
QJsonObject serverConfig = m_settings->server(serverIndex);
|
||||
m_vpnConfiguration = vpnConfigurationController.createVpnConfiguration({ dns1, dns2 },
|
||||
serverConfig,
|
||||
containerConfig,
|
||||
container);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Proto proto = ContainerProps::defaultProtocol(container);
|
||||
IosController::Instance()->connectVpn(proto, m_vpnConfiguration);
|
||||
connect(&m_checkTimer, &QTimer::timeout, IosController::Instance(), &IosController::checkStatus);
|
||||
@@ -292,12 +412,71 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
}
|
||||
|
||||
void VpnConnection::restartConnection()
|
||||
{
|
||||
// Only reconnect if VPN was connected before sleep/network change
|
||||
if (!m_wasConnectedBeforeSleep) {
|
||||
qDebug() << "VPN was not connected before sleep/network change, skipping reconnection";
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "VPN was connected before sleep/network change, attempting reconnection";
|
||||
this->disconnectFromVpn();
|
||||
#ifdef Q_OS_LINUX
|
||||
QThread::msleep(5000);
|
||||
#endif
|
||||
this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration);
|
||||
|
||||
// Reset the flag after reconnection attempt
|
||||
m_wasConnectedBeforeSleep = false;
|
||||
}
|
||||
|
||||
void VpnConnection::createProtocolConnections()
|
||||
{
|
||||
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
|
||||
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this,
|
||||
SLOT(onConnectionStateChanged(Vpn::ConnectionState)));
|
||||
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_connectionLoseHandle)
|
||||
disconnect(m_connectionLoseHandle);
|
||||
if (m_networkChangeHandle)
|
||||
disconnect(m_networkChangeHandle);
|
||||
m_connectionLoseHandle = QMetaObject::Connection();
|
||||
m_networkChangeHandle = QMetaObject::Connection();
|
||||
|
||||
// TODO: replace unsafe IpcClient::Interface() calls
|
||||
m_connectionLoseHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::connectionLose,
|
||||
this, [this]() {
|
||||
qDebug() << "Connection Lose";
|
||||
auto result = IpcClient::Interface()->stopNetworkCheck();
|
||||
result.waitForFinished(3000);
|
||||
// Track VPN state before connection loss
|
||||
m_wasConnectedBeforeSleep = isConnected();
|
||||
qDebug() << "VPN was connected before connection loss:" << m_wasConnectedBeforeSleep;
|
||||
this->restartConnection();
|
||||
});
|
||||
m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange,
|
||||
this, [this]() {
|
||||
qDebug() << "Network change";
|
||||
// Track VPN state before network change (including sleep/wake)
|
||||
m_wasConnectedBeforeSleep = isConnected();
|
||||
qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep;
|
||||
this->restartConnection();
|
||||
});
|
||||
connect(m_vpnProtocol.data(), &VpnProtocol::tunnelAddressesUpdated,
|
||||
this, [this](const QString& gateway, const QString& localAddress) {
|
||||
Q_UNUSED(gateway)
|
||||
Q_UNUSED(localAddress)
|
||||
if (connectionState() != Vpn::ConnectionState::Connected) {
|
||||
return;
|
||||
}
|
||||
if (startNetworkCheckIfReady()) {
|
||||
m_pendingNetworkCheck = false;
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void VpnConnection::appendKillSwitchConfig()
|
||||
@@ -399,13 +578,28 @@ void VpnConnection::appendSplitTunnelingConfig()
|
||||
|
||||
m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode);
|
||||
m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray);
|
||||
}
|
||||
|
||||
qDebug() << QString("Site split tunneling is %1, route mode is %2")
|
||||
.arg(m_settings->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled")
|
||||
.arg(routeMode);
|
||||
qDebug() << QString("App split tunneling is %1, route mode is %2")
|
||||
.arg(m_settings->isAppsSplitTunnelingEnabled() ? "enabled" : "disabled")
|
||||
.arg(appsRouteMode);
|
||||
bool VpnConnection::startNetworkCheckIfReady()
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (!m_vpnProtocol || m_dockerContainer == DockerContainer::Ipsec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString gateway = m_vpnProtocol->vpnGateway();
|
||||
const QString localAddress = m_vpnProtocol->vpnLocalAddress();
|
||||
if (gateway.isEmpty() || localAddress.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->startNetworkCheck(gateway, localAddress);
|
||||
return reply.waitForFinished() && reply.returnValue();
|
||||
});
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
@@ -52,6 +52,7 @@ public slots:
|
||||
const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
|
||||
|
||||
void disconnectFromVpn();
|
||||
void restartConnection();
|
||||
|
||||
void addRoutes(const QStringList &ips);
|
||||
void deleteRoutes(const QStringList &ips);
|
||||
@@ -72,6 +73,8 @@ protected slots:
|
||||
|
||||
protected:
|
||||
QSharedPointer<VpnProtocol> m_vpnProtocol;
|
||||
QMetaObject::Connection m_connectionLoseHandle;
|
||||
QMetaObject::Connection m_networkChangeHandle;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
@@ -79,6 +82,14 @@ private:
|
||||
QJsonObject m_routeMode;
|
||||
QString m_remoteAddress;
|
||||
|
||||
ServerCredentials m_serverCredentials;
|
||||
int m_serverIndex;
|
||||
DockerContainer m_dockerContainer;
|
||||
|
||||
// Track VPN state before sleep for smart reconnection
|
||||
bool m_wasConnectedBeforeSleep = false;
|
||||
bool m_pendingNetworkCheck = false;
|
||||
|
||||
// Only for iOS for now, check counters
|
||||
QTimer m_checkTimer;
|
||||
|
||||
@@ -93,6 +104,7 @@ private:
|
||||
|
||||
void appendSplitTunnelingConfig();
|
||||
void appendKillSwitchConfig();
|
||||
bool startNetworkCheckIfReady();
|
||||
};
|
||||
|
||||
#endif // VPNCONNECTION_H
|
||||
|
||||
@@ -31,7 +31,6 @@ set SCRIPT_DIR=%PROJECT_DIR:"=%\deploy
|
||||
set WORK_DIR=%SCRIPT_DIR:"=%\build_%BUILD_ARCH:"=%
|
||||
set APP_NAME=AmneziaVPN
|
||||
set APP_FILENAME=%APP_NAME:"=%.exe
|
||||
set SERVICE_FILENAME=%APP_NAME:"=%-service.exe
|
||||
set APP_DOMAIN=org.amneziavpn.package
|
||||
set OUT_APP_DIR=%WORK_DIR:"=%\client\release
|
||||
set PREBILT_DEPLOY_DATA_DIR=%PROJECT_DIR:"=%\client\3rd-prebuilt\deploy-prebuilt\windows\x%BUILD_ARCH:"=%
|
||||
@@ -44,7 +43,6 @@ set STAGE_DIR=%WORK_DIR:"=%\stage
|
||||
echo "Environment:"
|
||||
echo "WORK_DIR: %WORK_DIR%"
|
||||
echo "APP_FILENAME: %APP_FILENAME%"
|
||||
echo "SERVICE_FILENAME: %SERVICE_FILENAME%"
|
||||
echo "PROJECT_DIR: %PROJECT_DIR%"
|
||||
echo "SCRIPT_DIR: %SCRIPT_DIR%"
|
||||
echo "OUT_APP_DIR: %OUT_APP_DIR%"
|
||||
@@ -76,7 +74,7 @@ if %errorlevel% neq 0 exit /b %errorlevel%
|
||||
echo "Deploying..."
|
||||
|
||||
mkdir "%OUT_APP_DIR%"
|
||||
copy "%WORK_DIR%\service\server\release\%SERVICE_FILENAME%" "%OUT_APP_DIR%"
|
||||
copy "%WORK_DIR%\service\server\release\%APP_NAME%-service.exe" "%OUT_APP_DIR%"
|
||||
rem copy "%WORK_DIR%\client\%APP_FILENAME%" "%OUT_APP_DIR%"
|
||||
|
||||
copy /Y "%PROJECT_DIR%\client\images\app.ico" "%OUT_APP_DIR%\AmneziaVPN.ico" >nul
|
||||
@@ -85,8 +83,7 @@ echo "Signing exe"
|
||||
cd %OUT_APP_DIR%
|
||||
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.exe
|
||||
|
||||
"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations --force-openssl "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
|
||||
"%QT_BIN_DIR:"=%\windeployqt" --release "%OUT_APP_DIR:"=%\%SERVICE_FILENAME:"=%"
|
||||
"%QT_BIN_DIR:"=%\windeployqt" --release --qmldir "%PROJECT_DIR:"=%\client" --force --no-translations "%OUT_APP_DIR:"=%\%APP_FILENAME:"=%"
|
||||
|
||||
signtool sign /v /n "Privacy Technologies OU" /fd sha256 /tr http://timestamp.comodoca.com/?td=sha256 /td sha256 *.dll
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>Sockets</key>
|
||||
<dict>
|
||||
<key>Listeners</key>
|
||||
|
||||
@@ -7,54 +7,42 @@ LOG_FOLDER=/var/log/$APP_NAME
|
||||
LOG_FILE="$LOG_FOLDER/post-install.log"
|
||||
APP_PATH=/Applications/$APP_NAME.app
|
||||
|
||||
rm -rf "$LOG_FOLDER"
|
||||
mkdir -p "$LOG_FOLDER"
|
||||
echo "`date` Script started" > "$LOG_FILE"
|
||||
|
||||
log() {
|
||||
echo "`date` $*" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
log "CMD: $*"
|
||||
"$@" >> "$LOG_FILE" 2>&1
|
||||
local ec=$?
|
||||
log "EXIT: $ec"
|
||||
return $ec
|
||||
}
|
||||
|
||||
# Handle new installations unpacked into localized folder
|
||||
if [ -d "/Applications/${APP_NAME}.localized" ]; then
|
||||
log "Detected ${APP_NAME}.localized, migrating to standard path"
|
||||
run_cmd sudo rm -rf "$APP_PATH"
|
||||
run_cmd sudo mv "/Applications/${APP_NAME}.localized/${APP_NAME}.app" "$APP_PATH"
|
||||
run_cmd sudo rm -rf "/Applications/${APP_NAME}.localized"
|
||||
echo "`date` Detected ${APP_NAME}.localized, migrating to standard path" >> $LOG_FILE
|
||||
sudo rm -rf "$APP_PATH"
|
||||
sudo mv "/Applications/${APP_NAME}.localized/${APP_NAME}.app" "$APP_PATH"
|
||||
sudo rm -rf "/Applications/${APP_NAME}.localized"
|
||||
fi
|
||||
|
||||
run_cmd launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
run_cmd rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
if launchctl list "$APP_NAME-service" &> /dev/null; then
|
||||
launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
fi
|
||||
|
||||
run_cmd sudo chmod -R a-w "$APP_PATH/"
|
||||
run_cmd sudo chown -R root "$APP_PATH/"
|
||||
run_cmd sudo chgrp -R wheel "$APP_PATH/"
|
||||
sudo chmod -R a-w "$APP_PATH/"
|
||||
sudo chown -R root "$APP_PATH/"
|
||||
sudo chgrp -R wheel "$APP_PATH/"
|
||||
|
||||
log "Requesting ${APP_NAME} to quit gracefully"
|
||||
run_cmd osascript -e 'tell application "AmneziaVPN" to quit' || true
|
||||
rm -rf $LOG_FOLDER
|
||||
mkdir -p $LOG_FOLDER
|
||||
|
||||
echo "`date` Script started" > $LOG_FILE
|
||||
|
||||
echo "Requesting ${APP_NAME} to quit gracefully" >> "$LOG_FILE"
|
||||
osascript -e 'tell application "AmneziaVPN" to quit'
|
||||
|
||||
PLIST_SOURCE="$APP_PATH/Contents/Resources/$PLIST_NAME"
|
||||
if [ -f "$PLIST_SOURCE" ]; then
|
||||
run_cmd mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
mv -f "$PLIST_SOURCE" "$LAUNCH_DAEMONS_PLIST_NAME" 2>> $LOG_FILE
|
||||
else
|
||||
log "ERROR: service plist not found at $PLIST_SOURCE"
|
||||
echo "`date` ERROR: service plist not found at $PLIST_SOURCE" >> $LOG_FILE
|
||||
fi
|
||||
|
||||
run_cmd chown root:wheel "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
run_cmd chmod 644 "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
run_cmd launchctl bootstrap system "$LAUNCH_DAEMONS_PLIST_NAME" || run_cmd launchctl load "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
run_cmd launchctl enable "system/$APP_NAME-service" || true
|
||||
run_cmd launchctl kickstart -k "system/$APP_NAME-service" || true
|
||||
run_cmd launchctl print "system/$APP_NAME-service" || true
|
||||
log "Launching ${APP_NAME} application"
|
||||
run_cmd open -a "$APP_PATH" || true
|
||||
chown root:wheel "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
launchctl load "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
echo "`date` Launching ${APP_NAME} application" >> $LOG_FILE
|
||||
open -a "$APP_PATH" 2>> $LOG_FILE || true
|
||||
|
||||
log "Script finished"
|
||||
echo "`date` Service status: $?" >> $LOG_FILE
|
||||
echo "`date` Script finished" >> $LOG_FILE
|
||||
|
||||
@@ -29,7 +29,7 @@ fi
|
||||
|
||||
# Unload the service if loaded and remove its plist file regardless
|
||||
if launchctl list "${APP_NAME}-service" &> /dev/null; then
|
||||
sudo launchctl bootout system "$LAUNCH_DAEMONS_PLIST_NAME" || sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
fi
|
||||
sudo rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
if [[ $VERSION = '' ]]; then
|
||||
echo '::error::VERSION does not set. Exiting with error...'
|
||||
exit 1
|
||||
fi
|
||||
@@ -14,39 +14,25 @@ cd dist
|
||||
|
||||
echo $VERSION >> VERSION
|
||||
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG
|
||||
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE
|
||||
|
||||
if [[ $(cat CHANGELOG) = null ]]; then
|
||||
echo '::error::Release does not exists. Exiting with error...'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download files with error handling
|
||||
download_file() {
|
||||
local url=$1
|
||||
local filename=$(basename "$url")
|
||||
echo "Downloading $filename..."
|
||||
if ! wget -q "$url"; then
|
||||
echo "::error::Failed to download $filename from $url"
|
||||
exit 8
|
||||
fi
|
||||
echo "Successfully downloaded $filename"
|
||||
}
|
||||
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_x86.apk
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android9+_x86_64.apk
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux_x64.tar
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.pkg
|
||||
download_file https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_x64.exe
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_arm64-v8a.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_armeabi-v7a.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86_64.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_arm64-v8a.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_armeabi-v7a.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86_64.apk
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux_x64.tar.zip
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.dmg
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos_old.dmg
|
||||
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_windows_x64.exe
|
||||
|
||||
cd ../
|
||||
|
||||
echo "Syncing to R2..."
|
||||
if ! rclone sync ./dist/ r2:/updates/; then
|
||||
echo "::error::Failed to sync files to R2"
|
||||
exit 8
|
||||
fi
|
||||
|
||||
echo "Deployment completed successfully!"
|
||||
rclone sync ./dist/ r2:/updates/
|
||||
|
||||
66
ipc/ipc.h
66
ipc/ipc.h
@@ -11,7 +11,6 @@
|
||||
namespace amnezia {
|
||||
|
||||
enum PermittedProcess {
|
||||
Invalid,
|
||||
OpenVPN,
|
||||
Wireguard,
|
||||
Tun2Socks,
|
||||
@@ -20,18 +19,16 @@ enum PermittedProcess {
|
||||
|
||||
inline QString permittedProcessPath(PermittedProcess pid)
|
||||
{
|
||||
switch (pid) {
|
||||
case PermittedProcess::OpenVPN:
|
||||
return Utils::openVpnExecPath();
|
||||
case PermittedProcess::Wireguard:
|
||||
return Utils::wireguardExecPath();
|
||||
case PermittedProcess::CertUtil:
|
||||
return Utils::certUtilPath();
|
||||
case PermittedProcess::Tun2Socks:
|
||||
return Utils::tun2socksPath();
|
||||
default:
|
||||
return "";
|
||||
if (pid == PermittedProcess::OpenVPN) {
|
||||
return Utils::openVpnExecPath();
|
||||
} else if (pid == PermittedProcess::Wireguard) {
|
||||
return Utils::wireguardExecPath();
|
||||
} else if (pid == PermittedProcess::CertUtil) {
|
||||
return Utils::certUtilPath();
|
||||
} else if (pid == PermittedProcess::Tun2Socks) {
|
||||
return Utils::tun2socksPath();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -51,51 +48,6 @@ inline QString getIpcProcessUrl(int pid) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline QStringList sanitizeArguments(PermittedProcess proc, const QStringList &args) {
|
||||
using Validator = std::function<bool(const QString&)>;
|
||||
QMap<QString, Validator> namedArgs;
|
||||
QList<Validator> positionalArgs;
|
||||
|
||||
switch (proc) {
|
||||
case Tun2Socks:
|
||||
namedArgs["-device"] = [](const QString& v) { return v.startsWith("tun://"); };
|
||||
namedArgs["-proxy"] = [](const QString& v) { return v.startsWith("socks5://"); };
|
||||
break;
|
||||
default:
|
||||
//FIXME
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
QStringList sanitized;
|
||||
|
||||
for (int i = 0, pos = 0; i < args.size(); i++) {
|
||||
const auto& key = args[i];
|
||||
|
||||
if (const auto found = namedArgs.find(key); found != namedArgs.end()) {
|
||||
const auto validator = found.value();
|
||||
|
||||
if (validator) {
|
||||
if (i + 1 < args.size()) {
|
||||
const auto& value = args[i+1];
|
||||
if (validator(value)) {
|
||||
sanitized << key << value;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sanitized << key;
|
||||
}
|
||||
} else if (pos < positionalArgs.size()) {
|
||||
if (const auto validator = positionalArgs[pos]; validator && validator(key)) {
|
||||
sanitized << key;
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ class IpcInterface
|
||||
SLOT( bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) );
|
||||
SLOT( bool restoreResolvers() );
|
||||
|
||||
SLOT(bool xrayStart(const QString &config));
|
||||
SLOT(bool xrayStop());
|
||||
SLOT(void xrayStart(const QString &config));
|
||||
SLOT(void xrayStop());
|
||||
|
||||
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
|
||||
SLOT( bool stopNetworkCheck() );
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
class IpcProcessInterface
|
||||
{
|
||||
SLOT( start() );
|
||||
SLOT( terminate() );
|
||||
SLOT( kill() );
|
||||
SLOT( close() );
|
||||
|
||||
SLOT( setArguments(const QStringList &arguments) );
|
||||
@@ -19,11 +17,6 @@ class IpcProcessInterface
|
||||
SLOT( QByteArray readAllStandardError() );
|
||||
SLOT( QByteArray readAllStandardOutput() );
|
||||
|
||||
SLOT( bool waitForFinished() );
|
||||
SLOT( bool waitForFinished(int msecs) );
|
||||
SLOT( bool waitForStarted() );
|
||||
SLOT( bool waitForStarted(int msecs) );
|
||||
|
||||
|
||||
SIGNAL( errorOccurred(QProcess::ProcessError error) );
|
||||
SIGNAL( finished(int exitCode, QProcess::ExitStatus exitStatus) );
|
||||
|
||||
11
ipc/ipc_process_tun2socks.rep
Normal file
11
ipc/ipc_process_tun2socks.rep
Normal file
@@ -0,0 +1,11 @@
|
||||
#include <QtCore>
|
||||
#include <QString>
|
||||
|
||||
class IpcProcessTun2Socks
|
||||
{
|
||||
SLOT( start() );
|
||||
SLOT( stop() );
|
||||
|
||||
SIGNAL( setConnectionState(int state) );
|
||||
SIGNAL( stateChanged(QProcess::ProcessState newState) );
|
||||
};
|
||||
@@ -304,7 +304,7 @@ bool IpcServer::refreshKillSwitch(bool enabled)
|
||||
return KillSwitch::instance()->refresh(enabled);
|
||||
}
|
||||
|
||||
bool IpcServer::xrayStart(const QString& cfg)
|
||||
void IpcServer::xrayStart(const QString& cfg)
|
||||
{
|
||||
#ifdef MZ_DEBUG
|
||||
qDebug() << "IpcServer::xrayStart";
|
||||
@@ -313,7 +313,7 @@ bool IpcServer::xrayStart(const QString& cfg)
|
||||
return Xray::getInstance().startXray(cfg);
|
||||
}
|
||||
|
||||
bool IpcServer::xrayStop()
|
||||
void IpcServer::xrayStop()
|
||||
{
|
||||
#ifdef MZ_DEBUG
|
||||
qDebug() << "IpcServer::xrayStop";
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
|
||||
#include "ipc.h"
|
||||
#include "ipcserverprocess.h"
|
||||
#include "ipctun2socksprocess.h"
|
||||
|
||||
#include "rep_ipc_interface_source.h"
|
||||
#include "rep_ipc_process_tun2socks_source.h"
|
||||
|
||||
class IpcServer : public IpcInterfaceSource
|
||||
{
|
||||
@@ -42,8 +44,8 @@ public:
|
||||
virtual bool refreshKillSwitch( bool enabled ) override;
|
||||
virtual bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) override;
|
||||
virtual bool restoreResolvers() override;
|
||||
virtual bool xrayStart(const QString& cfg) override;
|
||||
virtual bool xrayStop() override;
|
||||
virtual void xrayStart(const QString& cfg) override;
|
||||
virtual void xrayStop() override;
|
||||
virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override;
|
||||
virtual bool stopNetworkCheck() override;
|
||||
|
||||
@@ -54,10 +56,12 @@ private:
|
||||
ProcessDescriptor (QObject *parent = nullptr) {
|
||||
serverNode = QSharedPointer<QRemoteObjectHost>(new QRemoteObjectHost(parent));
|
||||
ipcProcess = QSharedPointer<IpcServerProcess>(new IpcServerProcess(parent));
|
||||
tun2socksProcess = QSharedPointer<IpcProcessTun2Socks>(new IpcProcessTun2Socks(parent));
|
||||
localServer = QSharedPointer<QLocalServer>(new QLocalServer(parent));
|
||||
}
|
||||
|
||||
QSharedPointer<IpcServerProcess> ipcProcess;
|
||||
QSharedPointer<IpcProcessTun2Socks> tun2socksProcess;
|
||||
QSharedPointer<QRemoteObjectHost> serverNode;
|
||||
QSharedPointer<QLocalServer> localServer;
|
||||
};
|
||||
|
||||
@@ -40,14 +40,6 @@ void IpcServerProcess::start()
|
||||
m_process->waitForStarted();
|
||||
}
|
||||
|
||||
void IpcServerProcess::terminate() {
|
||||
m_process->terminate();
|
||||
}
|
||||
|
||||
void IpcServerProcess::kill() {
|
||||
m_process->kill();
|
||||
}
|
||||
|
||||
void IpcServerProcess::close()
|
||||
{
|
||||
m_process->close();
|
||||
@@ -55,7 +47,7 @@ void IpcServerProcess::close()
|
||||
|
||||
void IpcServerProcess::setArguments(const QStringList &arguments)
|
||||
{
|
||||
m_process->setArguments(amnezia::sanitizeArguments(m_program, arguments));
|
||||
m_process->setArguments(arguments);
|
||||
}
|
||||
|
||||
void IpcServerProcess::setInputChannelMode(QProcess::InputChannelMode mode)
|
||||
@@ -77,9 +69,7 @@ void IpcServerProcess::setProcessChannelMode(QProcess::ProcessChannelMode mode)
|
||||
|
||||
void IpcServerProcess::setProgram(int programId)
|
||||
{
|
||||
m_program = static_cast<amnezia::PermittedProcess>(programId);
|
||||
m_process->setProgram(amnezia::permittedProcessPath(m_program));
|
||||
m_process->setArguments({});
|
||||
m_process->setProgram(amnezia::permittedProcessPath(static_cast<amnezia::PermittedProcess>(programId)));
|
||||
}
|
||||
|
||||
void IpcServerProcess::setWorkingDirectory(const QString &dir)
|
||||
@@ -102,20 +92,4 @@ QByteArray IpcServerProcess::readAllStandardOutput()
|
||||
return m_process->readAllStandardOutput();
|
||||
}
|
||||
|
||||
bool IpcServerProcess::waitForStarted() {
|
||||
return m_process->waitForStarted();
|
||||
}
|
||||
|
||||
bool IpcServerProcess::waitForStarted(int msecs) {
|
||||
return m_process->waitForStarted(msecs);
|
||||
}
|
||||
|
||||
bool IpcServerProcess::waitForFinished() {
|
||||
return m_process->waitForFinished();
|
||||
}
|
||||
|
||||
bool IpcServerProcess::waitForFinished(int msecs) {
|
||||
return m_process->waitForFinished(msecs);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef IPCSERVERPROCESS_H
|
||||
#define IPCSERVERPROCESS_H
|
||||
|
||||
#include "ipc.h"
|
||||
#include <QObject>
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
@@ -15,8 +14,6 @@ public:
|
||||
virtual ~IpcServerProcess();
|
||||
|
||||
void start() override;
|
||||
void terminate() override;
|
||||
void kill() override;
|
||||
void close() override;
|
||||
|
||||
void setArguments(const QStringList &arguments) override;
|
||||
@@ -30,15 +27,9 @@ public:
|
||||
QByteArray readAllStandardError() override;
|
||||
QByteArray readAllStandardOutput() override;
|
||||
|
||||
bool waitForStarted() override;
|
||||
bool waitForStarted(int msecs) override;
|
||||
bool waitForFinished() override;
|
||||
bool waitForFinished(int msecs) override;
|
||||
|
||||
signals:
|
||||
|
||||
private:
|
||||
amnezia::PermittedProcess m_program = amnezia::PermittedProcess::Invalid;
|
||||
QSharedPointer<QProcess> m_process;
|
||||
};
|
||||
|
||||
|
||||
79
ipc/ipctun2socksprocess.cpp
Normal file
79
ipc/ipctun2socksprocess.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "ipctun2socksprocess.h"
|
||||
#include "ipc.h"
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
|
||||
#include "../protocols/protocols_defs.h"
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
|
||||
IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) :
|
||||
IpcProcessTun2SocksSource(parent),
|
||||
m_t2sProcess(QSharedPointer<QProcess>(new QProcess()))
|
||||
{
|
||||
qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()";
|
||||
|
||||
}
|
||||
|
||||
IpcProcessTun2Socks::~IpcProcessTun2Socks()
|
||||
{
|
||||
qDebug() << "IpcProcessTun2Socks::~IpcProcessTun2Socks()";
|
||||
}
|
||||
|
||||
void IpcProcessTun2Socks::start()
|
||||
{
|
||||
connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged);
|
||||
qDebug() << "IpcProcessTun2Socks::start()";
|
||||
m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast<amnezia::PermittedProcess>(amnezia::PermittedProcess::Tun2Socks)));
|
||||
|
||||
QString XrayConStr = "socks5://127.0.0.1:10808";
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QStringList arguments({"-device", "tun://tun2?guid={081A8A84-8D12-4DF5-B8C4-396D5B0053E4}", "-proxy", XrayConStr, "-tun-post-up",
|
||||
QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255")
|
||||
.arg(amnezia::protocols::xray::defaultLocalAddr)});
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr});
|
||||
#endif
|
||||
#ifdef Q_OS_MAC
|
||||
QStringList arguments({"-device", "utun22", "-proxy", XrayConStr});
|
||||
#endif
|
||||
|
||||
m_t2sProcess->setArguments(arguments);
|
||||
|
||||
if (Utils::processIsRunning(Utils::executable("tun2socks", false))) {
|
||||
qDebug().noquote() << "kill previos tun2socks";
|
||||
Utils::killProcessByName(Utils::executable("tun2socks", false));
|
||||
}
|
||||
|
||||
m_t2sProcess->start();
|
||||
|
||||
connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() {
|
||||
QString line = m_t2sProcess.data()->readAllStandardOutput();
|
||||
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
|
||||
emit setConnectionState(Vpn::ConnectionState::Connected);
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_t2sProcess.data(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus;
|
||||
emit setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) {
|
||||
emit setConnectionState(Vpn::ConnectionState::Error);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
m_t2sProcess->start();
|
||||
m_t2sProcess->waitForStarted();
|
||||
}
|
||||
|
||||
void IpcProcessTun2Socks::stop()
|
||||
{
|
||||
qDebug() << "IpcProcessTun2Socks::stop()";
|
||||
m_t2sProcess->disconnect();
|
||||
m_t2sProcess->kill();
|
||||
m_t2sProcess->waitForFinished(3000);
|
||||
}
|
||||
#endif
|
||||
52
ipc/ipctun2socksprocess.h
Normal file
52
ipc/ipctun2socksprocess.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef IPCTUN2SOCKSPROCESS_H
|
||||
#define IPCTUN2SOCKSPROCESS_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
#include "rep_ipc_process_tun2socks_source.h"
|
||||
|
||||
namespace Vpn
|
||||
{
|
||||
Q_NAMESPACE
|
||||
enum ConnectionState {
|
||||
Unknown,
|
||||
Disconnected,
|
||||
Preparing,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnecting,
|
||||
Reconnecting,
|
||||
Error
|
||||
};
|
||||
Q_ENUM_NS(ConnectionState)
|
||||
}
|
||||
|
||||
|
||||
class IpcProcessTun2Socks : public IpcProcessTun2SocksSource
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit IpcProcessTun2Socks(QObject *parent = nullptr);
|
||||
virtual ~IpcProcessTun2Socks();
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
signals:
|
||||
|
||||
private:
|
||||
QSharedPointer<QProcess> m_t2sProcess;
|
||||
};
|
||||
|
||||
#else
|
||||
class IpcProcessTun2Socks : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IpcProcessTun2Socks(QObject *parent = nullptr);
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif // IPCTUN2SOCKSPROCESS_H
|
||||
@@ -6,7 +6,7 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION})
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat Concurrent)
|
||||
find_package(Qt6 REQUIRED COMPONENTS DBus Core Network Widgets RemoteObjects Core5Compat)
|
||||
qt_standard_project_setup()
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ set(HEADERS
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/localserver.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/router.h
|
||||
@@ -96,6 +97,7 @@ set(SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/localserver.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/main.cpp
|
||||
@@ -351,7 +353,7 @@ include_directories(
|
||||
|
||||
|
||||
add_executable(${PROJECT} ${SOURCES} ${HEADERS} ${RESOURCES})
|
||||
target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus Qt6::Concurrent ${LIBS})
|
||||
target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus ${LIBS})
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -387,6 +389,7 @@ endif()
|
||||
|
||||
qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep)
|
||||
qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep)
|
||||
qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_tun2socks.rep)
|
||||
|
||||
# copy deploy artifacts required to run the application to the debug build folder
|
||||
if(WIN32)
|
||||
|
||||
@@ -40,6 +40,7 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
|
||||
if (!m_isRemotingEnabled) {
|
||||
m_isRemotingEnabled = true;
|
||||
m_serverNode.enableRemoting(&m_ipcServer);
|
||||
m_serverNode.enableRemoting(&m_tun2socks);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
~LocalServer();
|
||||
QSharedPointer<QLocalServer> m_server;
|
||||
IpcServer m_ipcServer;
|
||||
IpcProcessTun2Socks m_tun2socks;
|
||||
QRemoteObjectHost m_serverNode;
|
||||
bool m_isRemotingEnabled = false;
|
||||
|
||||
|
||||
@@ -66,9 +66,6 @@ void Router::resetIpStack()
|
||||
|
||||
bool Router::createTun(const QString &dev, const QString &subnet)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return RouterWin::Instance().createTun(dev, subnet);
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
return RouterLinux::Instance().createTun(dev, subnet);
|
||||
#endif
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <tchar.h>
|
||||
|
||||
#include <QProcess>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include <core/networkUtilities.h>
|
||||
|
||||
@@ -309,77 +308,6 @@ void RouterWin::resetIpStack()
|
||||
}
|
||||
}
|
||||
|
||||
bool RouterWin::createTun(const QString &dev, const QString &subnet)
|
||||
{
|
||||
NET_LUID luid;
|
||||
DWORD res = ConvertInterfaceAliasToLuid(reinterpret_cast<const wchar_t*>(dev.utf16()), &luid);
|
||||
if (res != NO_ERROR) {
|
||||
qCritical() << "Failed to convert luid: " << res;
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE hEvent = CreateEvent(nullptr, true, false, nullptr);
|
||||
if (!hEvent) {
|
||||
qCritical() << "Failed to allocate event object";
|
||||
return false;
|
||||
}
|
||||
auto _guardEvent = qScopeGuard([hEvent](){ CloseHandle(hEvent); });
|
||||
|
||||
struct {
|
||||
HANDLE hEvent;
|
||||
NET_LUID luid;
|
||||
const QString &subnet;
|
||||
bool found;
|
||||
} ctx = { .hEvent = hEvent, .luid = luid, .subnet = subnet, .found = false };
|
||||
|
||||
auto cb = [](void *priv, MIB_UNICASTIPADDRESS_ROW *row, MIB_NOTIFICATION_TYPE NotificationType) {
|
||||
auto* c = reinterpret_cast<decltype(ctx)*>(priv);
|
||||
if (row != nullptr && row->InterfaceLuid.Value == c->luid.Value && row->Address.si_family == AF_INET) {
|
||||
char ip[INET_ADDRSTRLEN];
|
||||
inet_ntop(row->Address.Ipv4.sin_family, &row->Address.Ipv4.sin_addr, ip, INET_ADDRSTRLEN);
|
||||
if (c->subnet == ip) {
|
||||
c->found = true;
|
||||
SetEvent(c->hEvent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
HANDLE hNotif;
|
||||
res = NotifyUnicastIpAddressChange(AF_INET, cb, &ctx, false, &hNotif);
|
||||
if (res != NO_ERROR) {
|
||||
qCritical() << "Failed to subscribe to interface change";
|
||||
return false;
|
||||
}
|
||||
auto _guardNotif = qScopeGuard([hNotif](){ CancelMibChangeNotify2(hNotif); });
|
||||
|
||||
MIB_UNICASTIPADDRESS_ROW row;
|
||||
InitializeUnicastIpAddressEntry(&row);
|
||||
|
||||
row.InterfaceLuid = luid;
|
||||
row.Address.si_family = AF_INET;
|
||||
|
||||
inet_pton(AF_INET, subnet.toStdString().c_str(), &row.Address.Ipv4.sin_addr);
|
||||
|
||||
row.OnLinkPrefixLength = 32;
|
||||
row.ValidLifetime = 0xffffffff;
|
||||
row.PreferredLifetime = 0xffffffff;
|
||||
row.DadState = IpDadStatePreferred;
|
||||
|
||||
res = CreateUnicastIpAddressEntry(&row);
|
||||
if (res != NO_ERROR && res != ERROR_OBJECT_ALREADY_EXISTS) {
|
||||
qDebug() << "Failed to create IP address:" << res;
|
||||
return false;
|
||||
}
|
||||
|
||||
res = WaitForSingleObject(hEvent, 10000);
|
||||
if (res == WAIT_TIMEOUT) {
|
||||
qCritical() << "Timeout of waiting for IP assignment for " << dev << " device";
|
||||
return false;
|
||||
}
|
||||
|
||||
return ctx.found;
|
||||
}
|
||||
|
||||
void RouterWin::suspendWcmSvc(bool suspend)
|
||||
{
|
||||
if (suspend == m_suspended) return;
|
||||
@@ -537,19 +465,11 @@ bool RouterWin::StopRoutingIpv6()
|
||||
qDebug() << "RouterWin::StopRoutingIpv6";
|
||||
|
||||
if (auto loopback = findLoopbackIface(); loopback.isValid()) {
|
||||
QFuture<bool> res = QtConcurrent::mappedReduced(kIpv6Subnets, [loopback](const QString &subnet) -> bool {
|
||||
int res = QProcess::execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" });
|
||||
return res == 0;
|
||||
},
|
||||
[](bool &result, bool success) {
|
||||
result = result && success;
|
||||
}, true);
|
||||
|
||||
res.waitForFinished();
|
||||
return res.result();
|
||||
for (auto subnet : kIpv6Subnets) {
|
||||
QProcess{}.execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" });
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RouterWin::StartRoutingIpv6()
|
||||
@@ -557,14 +477,9 @@ bool RouterWin::StartRoutingIpv6()
|
||||
qDebug() << "RouterWin::StartRoutingIpv6";
|
||||
|
||||
if (auto loopback = findLoopbackIface(); loopback.isValid()) {
|
||||
QFuture<bool> res = QtConcurrent::mappedReduced(kIpv6Subnets, [loopback](const QString &subnet) -> bool {
|
||||
int res = QProcess::execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) });
|
||||
return res == 0;
|
||||
},
|
||||
[](bool &result, bool success) {
|
||||
result = result && success;
|
||||
}, true);
|
||||
for (auto subnet : kIpv6Subnets) {
|
||||
QProcess{}.execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) });
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ public:
|
||||
bool StartRoutingIpv6();
|
||||
bool StopRoutingIpv6();
|
||||
|
||||
bool createTun(const QString &dev, const QString &subnet);
|
||||
void suspendWcmSvc(bool suspend);
|
||||
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
|
||||
bool restoreResolvers();
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
bool Xray::startXray(const QString &cfg)
|
||||
void Xray::startXray(const QString &cfg)
|
||||
{
|
||||
qDebug() << "Xray::startXray()";
|
||||
|
||||
@@ -40,38 +40,34 @@ bool Xray::startXray(const QString &cfg)
|
||||
|
||||
if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) {
|
||||
qDebug() << "[xray] sockopt failed: " << err;
|
||||
amnezia_xray_free(err);
|
||||
return false;
|
||||
free(err);
|
||||
return;
|
||||
}
|
||||
|
||||
amnezia_xray_setloghandler(ctxLogHandler, this);
|
||||
|
||||
QByteArray bytes = cfg.toUtf8();
|
||||
if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) {
|
||||
qDebug() << "[xray] configuration failed: " << err;
|
||||
amnezia_xray_free(err);
|
||||
return false;
|
||||
free(err);
|
||||
return;
|
||||
}
|
||||
|
||||
amnezia_xray_setloghandler(ctxLogHandler, this);
|
||||
|
||||
if (auto err = amnezia_xray_start(); err != nullptr) {
|
||||
qDebug() << "[xray] failed to start: " << err;
|
||||
amnezia_xray_free(err);
|
||||
return false;
|
||||
free(err);
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Xray::stopXray()
|
||||
void Xray::stopXray()
|
||||
{
|
||||
qDebug() << "Xray::stopXray()";
|
||||
if (auto err = amnezia_xray_stop(); err != nullptr) {
|
||||
qDebug() << "[xray] failed to stop: " << err;
|
||||
amnezia_xray_free(err);
|
||||
return false;
|
||||
free(err);
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Xray::logHandler(char* str)
|
||||
|
||||
@@ -12,8 +12,8 @@ public:
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool startXray(const QString& cfg);
|
||||
bool stopXray();
|
||||
void startXray(const QString& cfg);
|
||||
void stopXray();
|
||||
|
||||
private:
|
||||
static void ctxSockCallback(uintptr_t fd, void* ctx) {
|
||||
|
||||
Reference in New Issue
Block a user