Compare commits

..

27 Commits

Author SHA1 Message Date
yp
db054b2449 fix: trim SSH server IP and submit credentials on Enter 2026-08-06 12:13:45 +03:00
yp
d702dc0b2a fix: various fixes for Telemt, XRay (VLESS), Captcha (#2903)
* fix worker & fix captha

* fixed read config (xxd) snapshot

* remove comment

* fix snapshot

* fixed xray save

* move subtitleText -> hint (Xray)

* xray config apply via reinstall

* apply server config via container reinstall

* transport compatibility and xray-core v26 config

* seed reality defaults on fresh deploy

* server scripts for xray-core v26 (keys, udp, image)

* placeholder defaults and hint tooltip control

* wire placeholders and hints into settings pages

* gate flow and security by transport in UI

* encode raw tg link in QR

* remove comment

* restore telegram-proxy transport mode and domain on server re-scan

* remove drop SessionKey, add edit

* fixed xray/mtproxy/telemt
2026-08-06 13:44:00 +08:00
vkamn
2ad6721061 feat: awg3 support selfhosted (#2908)
* chore: bump version

* feat: add awg3 support to selfhosted

* chore: remove HeaderProtectionKey from ui

* fix: fixed regexp in configure_container.sh for awg

* chore: return server protocol version to config

* fix: fixed native wg obfuscation

* fix: fixed range validator for awg text field

* feat: add warning for awg 1/2

* fix: remove warning from other protocols
2026-08-06 10:54:26 +08:00
vkamn
0d116bfc67 fix: use native NSStatusItem for the macOS tray menu (#2759) (#2941)
* fix: use native NSStatusItem for the macOS tray menu (#2759)

On macOS 14+ the status-item menu is tracked out of process, so Qt's
QCocoaSystemTrayIcon menu-tracking observer (installed by setContextMenu)
fires asynchronously and calls emitActivated(), which reads
-[NSEvent clickCount] when NSApp.currentEvent is no longer a mouse event.
clickCount then raises NSInternalInconsistencyException, crashing the app
the first time the tray menu is opened.

Stop using QSystemTrayIcon on macOS and own the NSStatusItem ourselves
(MacOSStatusIcon), attaching the menu's backing native NSMenu via
QMenu::toNSMenu(). AppKit shows the menu natively - with menu-bar highlight
and correct anchoring - without registering the crashing observer, and Qt
keeps the NSMenu in sync with the QActions so the existing menu logic
(enable/disable, translations) is reused. The icon stays colored per state
and notifications go through UNUserNotificationCenter. Windows and Linux
keep QSystemTrayIcon unchanged.

This also wires up the previously-unused MacOSStatusIcon and removes the
orphaned neNotificationHandler.h.

* chore: return error messages

---------

Co-authored-by: Alexei <alexei.selivanov@gmail.com>
2026-08-06 00:13:05 +08:00
romanio427
25a18bdf97 fix: avoid reentrant DNS flush while resolving site routes (#2930)
Rebase onto dev after #2811 (multi-IP site routes). Keep multi-IP
resolution and addVpnSite(QStringList), but stop calling
waitForFinished() from QHostInfo callbacks: that nested event loop
re-entered the same callback path until stack overflow (0xc00000fd).

Track outstanding lookups and flush DNS once asynchronously via
QRemoteObjectPendingCallWatcher when any new routes were installed.
Capture gw/ip by value for the async routeAddList call.

Co-authored-by: romanio427 <romanio427@users.noreply.github.com>
2026-08-06 00:02:29 +08:00
aiamnezia
f73697d3f4 fix: enhance split tunneling IP handling (#2811)
- Refactored `VpnConnection` and `IpSplitTunnelingController` to support multiple IPs per site, improving the handling of split tunneling configurations.
- Adjusted methods to utilize `QStringList` for IP addresses, ensuring better management of site IPs.
- Enhanced the `SecureAppSettingsRepository` to normalize and manage IP lists effectively.
- Updated UI models to reflect changes in data structure for sites with multiple IPs.
2026-08-05 21:20:36 +08:00
cd-amn
53121f20a3 fix: OpenVPN binary crashes on connection (#2861)
* fix: set proper rpath's for openvpn on linux

* feat: run compilation on linux in max number of threads using deploy script

* feat: replace runpath for prebuilt openvpn

* fix: avoid double-installing of Qt libraries on linux

* fix: set proper rpath's for openvpn on macos

* fix: no DNS routes on Windows in blacklist split-tunnelling with OpenVPN
2026-08-05 21:18:53 +08:00
NickVs2015
d6cb9b930f chore: bump android version to 2141 (#2922) 2026-08-05 12:30:53 +08:00
vkamn
c4b3ccacc9 refactor: ios qt6.9 support (#2916)
* chore: remove configs from ios logs

* fix: fix scrolling on user list

* fix: fixed container installation after adding a new server

* refactor: qt6.9.3 support for ios
2026-08-05 12:30:02 +08:00
yp
e38a233904 fix: fix link RemoteObjects for linux (#2884) 2026-07-31 14:27:50 +08:00
Yaroslav Gurov
9b0495f5e1 fix: proper linking of openssl and other libraries on linux (#2910) 2026-07-31 10:18:20 +08:00
NickVs2015
30ec46a567 fix: openvpn runtime futex error (#2894)
* fix: pin go/1.23.12 for openvpn-pt-android to avoid android/arm crash

* chore: add fixme annotation

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-07-29 21:21:15 +08:00
NickVs2015
4055e1b05e fix: amnezia-libxray apply patch to all release (#2885)
* fix: amnezia-libxray apply patch to all releases

* chore: bump version to 2138
2026-07-29 21:20:42 +08:00
vkamn
4d28f81965 fix: set timeout for MarketplaceUpdateController 2026-07-26 15:20:53 +08:00
vkamn
5c035bb6cf chore: bump version 2026-07-25 23:38:25 +08:00
NickVs2015
26c22327af fix: netlink message lookup and buffer size 2026-07-25 19:31:37 +08:00
vkamn
5e9def4184 feat: add awg3 support 2026-07-25 19:29:54 +08:00
yp
a98c60d967 feat: add update dialog on mobile platforms (#2867)
* add force update

* add native UI & remove func force update
2026-07-25 19:29:24 +08:00
vkamn
0faf3e6d2f fix: pass keepalive from config to uapi 2026-07-25 00:34:38 +08:00
NickVs2015
a32a26bb67 fix: add 16kb page size support to amnezia-libxray android build (#2865) 2026-07-23 20:02:52 +08:00
vkamn
431ece7fff chore: update links in readme (#2860) 2026-07-23 19:24:15 +08:00
vkamn
5e82c6a558 fix: set macos keychain path (#2858) 2026-07-22 20:46:52 +08:00
vkamn
84a2d13b68 fix: various fixes (#2856)
* fix: fixed endless preparing

* fix: remove update drawer when open news
2026-07-22 00:25:37 +08:00
vkamn
324b09cca3 fix: various fixes (#2850)
* fix: return macos ne icon

* fix: return default sni to googletagmanager

* fix: context menu did not appear on ios
2026-07-21 21:37:13 +08:00
vkamn
e70563a071 chore: update links in readme (#2855) 2026-07-21 19:12:38 +08:00
Yaroslav Gurov
bc2a0ca649 fix: copy PF files to debug directory (#2849) 2026-07-20 16:37:59 +08:00
Yaroslav Gurov
3c6ac431b4 chore: update awg (#2845)
* chore: update awg

* chore: update xray

* chore: update awg-apple checksum

* chore: update awg-windows checksum
2026-07-20 15:38:02 +08:00
153 changed files with 3253 additions and 1791 deletions

View File

@@ -28,7 +28,7 @@ jobs:
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
@@ -63,7 +63,7 @@ jobs:
# ------------------------------------------------------
Build-Linux-Ubuntu:
runs-on: android-runner
runs-on: ubuntu-22.04
needs: Bake-Prebuilts-Linux
if: ${{ always() }}
@@ -501,7 +501,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.0'
xcode-version: '26.3.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v4
@@ -531,6 +531,8 @@ jobs:
QT_INSTALL_DIR: ${{ runner.temp }}
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
CODESIGN_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
CODESIGN_INSTALLER_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}

2
.gitignore vendored
View File

@@ -10,6 +10,8 @@ deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
.venv/
.cursor*
# Qt-es

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.4)
set(AMNEZIAVPN_VERSION 5.0.1.0)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -28,7 +28,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 2133)
set(APP_ANDROID_VERSION_CODE 2141)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -38,14 +38,14 @@
## Links
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Documentation | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Telegram support channel (English)
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Telegram support channel (Russian)
- [Get Premium for 6 or 12 months](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
## Tech

View File

@@ -35,14 +35,14 @@
## Ссылки
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Документация | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
- [Оформите Premium на 6 или 12 месяцев](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
## Технологии

View File

@@ -39,6 +39,7 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
endif()
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
find_package(OpenSSL REQUIRED)
set(LIBS ${LIBS}
Qt6::Core Qt6::Gui
@@ -174,6 +175,7 @@ endif()
if(LINUX AND NOT ANDROID)
set(LIBS ${LIBS} -static-libstdc++ -static-libgcc -ldl)
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
set(LIBS ${LIBS} -Wl,--no-as-needed $<TARGET_LINKER_FILE:Qt6::RemoteObjects> -Wl,--as-needed)
endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
@@ -231,7 +233,7 @@ install(RUNTIME_DEPENDENCY_SET client_deps
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^[Qq]t.*]]
[[^(lib)?[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]

View File

@@ -26,6 +26,9 @@
#include "version.h"
#include "platforms/ios/QRCodeReaderBase.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ioscontextmenu.h"
#endif
bool AmneziaApplication::m_forceQuit = false;
@@ -139,12 +142,19 @@ void AmneziaApplication::init()
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
#endif
#ifdef Q_OS_IOS
m_engine->rootContext()->setContextProperty("IosContextMenu", new IosContextMenu(this));
#endif
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
m_vpnConnectionThread.start();
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
m_marketplaceUpdateController.reset(new MarketplaceUpdateController());
m_marketplaceUpdateController->start();
m_engine->addImportPath("qrc:/ui/qml/Modules/");
if (m_parser.isSet(m_optImport)) {

View File

@@ -15,6 +15,7 @@
#include "core/controllers/coreController.h"
#include "secureQSettings.h"
#include "ui/controllers/marketplaceUpdateController.h"
#include "vpnConnection.h"
#include "ui/models/containerProps.h"
#include "ui/models/protocolProps.h"
@@ -56,6 +57,7 @@ private:
SecureQSettings* m_settings;
QScopedPointer<CoreController> m_coreController;
QScopedPointer<MarketplaceUpdateController> m_marketplaceUpdateController;
QSharedPointer<ContainerProps> m_containerProps;
QSharedPointer<ProtocolProps> m_protocolProps;

View File

@@ -3,6 +3,13 @@ package org.amnezia.vpn
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
@@ -100,6 +107,8 @@ class AmneziaActivity : QtActivity() {
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private var updateCoverDialog: Dialog? = null
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
@@ -487,6 +496,116 @@ class AmneziaActivity : QtActivity() {
super.onDestroy()
}
fun showUpdateCover() {
runOnUiThread {
if (isFinishing || isDestroyed || updateCoverDialog != null) return@runOnUiThread
val dialog = Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
dialog.setCancelable(false)
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(0xFF0E0E11.toInt())
}
dialog.setContentView(root)
dialog.show()
updateCoverDialog = dialog
}
}
fun hideUpdateCover() {
runOnUiThread {
updateCoverDialog?.dismiss()
updateCoverDialog = null
}
}
fun showUpdatePrompt(title: String, message: String, updateTitle: String, skipTitle: String, storeUrl: String) {
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
val dialog = updateCoverDialog ?: Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen).also {
it.setCancelable(false)
it.show()
updateCoverDialog = it
}
val density = resources.displayMetrics.density
fun dp(value: Int) = (value * density).toInt()
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(0xFF0E0E11.toInt())
setPadding(dp(32), dp(32), dp(32), dp(32))
}
val titleView = TextView(this).apply {
text = title
textSize = 22f
setTextColor(0xFFFFFFFF.toInt())
gravity = Gravity.CENTER
typeface = Typeface.create(typeface, Typeface.BOLD)
}
val messageView = TextView(this).apply {
text = message
textSize = 16f
setTextColor(0xFFC7C8CB.toInt())
gravity = Gravity.CENTER
setPadding(0, dp(16), 0, dp(28))
}
val updateButton = Button(this).apply {
text = updateTitle
isAllCaps = false
textSize = 17f
setTextColor(0xFF0E0E11.toInt())
stateListAnimator = null
background = GradientDrawable().apply {
cornerRadius = dp(12).toFloat()
setColor(0xFFFBB26A.toInt())
}
setOnClickListener {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)))
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "open store failed: ${e.message}")
}
hideUpdateCover()
}
}
val skipButton = Button(this).apply {
text = skipTitle
isAllCaps = false
textSize = 17f
setTextColor(0xFFD7D8DB.toInt())
stateListAnimator = null
background = GradientDrawable().apply {
cornerRadius = dp(12).toFloat()
setColor(0x00000000)
setStroke(dp(1), 0xFF2C2D30.toInt())
}
setOnClickListener { hideUpdateCover() }
}
val updateParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
).apply { topMargin = dp(8) }
val skipParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
).apply { topMargin = dp(12) }
root.addView(titleView)
root.addView(messageView)
root.addView(updateButton, updateParams)
root.addView(skipButton, skipParams)
dialog.setContentView(root)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")

View File

@@ -111,7 +111,7 @@ open class Wireguard : Protocol() {
configExtensionParameters(configData)
}
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it) }
configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
@@ -134,6 +134,20 @@ open class Wireguard : Protocol() {
configData.optStringOrNull("I3")?.let { setI3(it) }
configData.optStringOrNull("I4")?.let { setI4(it) }
configData.optStringOrNull("I5")?.let { setI5(it) }
configData.optStringOrNull("HeaderProtectionKey")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setHeaderProtectionKey(it.base64ToHex()) }
configData.optStringOrNull("ContentPaddingAddition")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setContentPaddingAddition(it) }
configData.optStringOrNull("RekeyAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setRekeyAfterTime(it) }
configData.optStringOrNull("RekeyTimeout")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setRekeyTimeout(it) }
configData.optStringOrNull("RejectAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setRejectAfterTime(it) }
configData.optStringOrNull("KeepaliveTimeout")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setKeepaliveTimeout(it) }
configData.optStringOrNull("MaxHandshakeAttempts")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setMaxHandshakeAttempts(it) }
}
private fun start(

View File

@@ -10,7 +10,7 @@ private const val WIREGUARD_DEFAULT_MTU = 1280
open class WireguardConfig protected constructor(
protocolConfigBuilder: ProtocolConfig.Builder,
val endpoint: InetEndpoint,
val persistentKeepalive: Int,
val persistentKeepalive: String?,
val publicKeyHex: String,
val preSharedKeyHex: String?,
val privateKeyHex: String,
@@ -31,6 +31,13 @@ open class WireguardConfig protected constructor(
var i3: String?,
var i4: String?,
var i5: String?,
val headerProtectionKeyHex: String?,
val contentPaddingAddition: String?,
val rekeyAfterTime: String?,
val rekeyTimeout: String?,
val rejectAfterTime: String?,
val keepaliveTimeout: String?,
val maxHandshakeAttempts: String?,
) : ProtocolConfig(protocolConfigBuilder) {
protected constructor(builder: Builder) : this(
@@ -57,6 +64,13 @@ open class WireguardConfig protected constructor(
builder.i3,
builder.i4,
builder.i5,
builder.headerProtectionKeyHex,
builder.contentPaddingAddition,
builder.rekeyAfterTime,
builder.rekeyTimeout,
builder.rejectAfterTime,
builder.keepaliveTimeout,
builder.maxHandshakeAttempts,
)
fun toWgUserspaceString(): String = with(StringBuilder()) {
@@ -87,6 +101,13 @@ open class WireguardConfig protected constructor(
i4?.let { appendLine("i4=$it") }
i5?.let { appendLine("i5=$it") }
}
headerProtectionKeyHex?.takeIf { it.isNotEmpty() }?.let { appendLine("header_protection_key=$it") }
contentPaddingAddition?.takeIf { it.isNotEmpty() }?.let { appendLine("content_padding_addition=$it") }
rekeyAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_after_time=$it") }
rekeyTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_timeout=$it") }
rejectAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("reject_after_time=$it") }
keepaliveTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("keepalive_timeout=$it") }
maxHandshakeAttempts?.takeIf { it.isNotEmpty() }?.let { appendLine("max_handshake_attempts=$it") }
}
private fun validateProtocolExtensionParameters() {
@@ -107,7 +128,7 @@ open class WireguardConfig protected constructor(
appendLine("allowed_ip=${route.inetNetwork}")
}
appendLine("endpoint=$endpoint")
if (persistentKeepalive != 0)
if (!persistentKeepalive.isNullOrEmpty() && persistentKeepalive != "0")
appendLine("persistent_keepalive_interval=$persistentKeepalive")
if (preSharedKeyHex != null)
appendLine("preshared_key=$preSharedKeyHex")
@@ -117,7 +138,7 @@ open class WireguardConfig protected constructor(
internal lateinit var endpoint: InetEndpoint
private set
internal var persistentKeepalive: Int = 0
internal var persistentKeepalive: String? = null
private set
internal lateinit var publicKeyHex: String
@@ -149,10 +170,17 @@ open class WireguardConfig protected constructor(
internal var i3: String? = null
internal var i4: String? = null
internal var i5: String? = null
internal var headerProtectionKeyHex: String? = null
internal var contentPaddingAddition: String? = null
internal var rekeyAfterTime: String? = null
internal var rekeyTimeout: String? = null
internal var rejectAfterTime: String? = null
internal var keepaliveTimeout: String? = null
internal var maxHandshakeAttempts: String? = null
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
fun setPersistentKeepalive(persistentKeepalive: String) = apply { this.persistentKeepalive = persistentKeepalive }
fun setPublicKeyHex(publicKeyHex: String) = apply { this.publicKeyHex = publicKeyHex }
@@ -178,6 +206,13 @@ open class WireguardConfig protected constructor(
fun setI3(i3: String) = apply { this.i3 = i3 }
fun setI4(i4: String) = apply { this.i4 = i4 }
fun setI5(i5: String) = apply { this.i5 = i5 }
fun setHeaderProtectionKey(headerProtectionKeyHex: String) = apply { this.headerProtectionKeyHex = headerProtectionKeyHex }
fun setContentPaddingAddition(contentPaddingAddition: String) = apply { this.contentPaddingAddition = contentPaddingAddition }
fun setRekeyAfterTime(rekeyAfterTime: String) = apply { this.rekeyAfterTime = rekeyAfterTime }
fun setRekeyTimeout(rekeyTimeout: String) = apply { this.rekeyTimeout = rekeyTimeout }
fun setRejectAfterTime(rejectAfterTime: String) = apply { this.rejectAfterTime = rejectAfterTime }
fun setKeepaliveTimeout(keepaliveTimeout: String) = apply { this.keepaliveTimeout = keepaliveTimeout }
fun setMaxHandshakeAttempts(maxHandshakeAttempts: String) = apply { this.maxHandshakeAttempts = maxHandshakeAttempts }
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
}

View File

@@ -31,6 +31,7 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -42,6 +43,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
@@ -49,6 +51,12 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
)
# The context menu helper uses ARC-only constructs (weak references); the
# rest of the Objective-C++ sources build with manual reference counting.
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
)
target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
@@ -69,7 +77,6 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
@@ -78,7 +85,7 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_EMBED_APP_EXTENSIONS networkextension
)
if(DEFINED DEPLOY)
if(DEPLOY)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"

View File

@@ -5,6 +5,7 @@ set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
enable_language(OBJC)
enable_language(OBJCXX)
enable_language(Swift)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Widgets)
@@ -33,7 +34,6 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -44,19 +44,12 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
)
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
set(MACOSX_BUNDLE_ICON_FILE app.icns)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})
target_include_directories(${PROJECT} PRIVATE
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
${Qt6Widgets_PRIVATE_INCLUDE_DIRS}
@@ -69,6 +62,7 @@ set_target_properties(${PROJECT} PROPERTIES
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
@@ -79,14 +73,10 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY "NO"
XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY "YES"
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks"
XCODE_EMBED_APP_EXTENSIONS AmneziaVPNNetworkExtension
)
@@ -133,6 +123,11 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy

View File

@@ -96,13 +96,17 @@ ProtocolConfig AwgConfigurator::createConfig(const ServerCredentials &credential
newClientConfig.specialJunk4 = configMap.value(configKey::specialJunk4);
newClientConfig.specialJunk5 = configMap.value(configKey::specialJunk5);
if (container == DockerContainer::Awg2) {
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
}
newClientConfig.isObfuscationEnabled = false;
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
newClientConfig.headerProtectionKey = configMap.value(configKey::headerProtectionKey);
newClientConfig.contentPaddingAddition = configMap.value(configKey::contentPaddingAddition);
newClientConfig.rekeyAfterTime = configMap.value(configKey::rekeyAfterTime);
newClientConfig.rekeyTimeout = configMap.value(configKey::rekeyTimeout);
newClientConfig.rejectAfterTime = configMap.value(configKey::rejectAfterTime);
newClientConfig.keepaliveTimeout = configMap.value(configKey::keepaliveTimeout);
newClientConfig.maxHandshakeAttempts = configMap.value(configKey::maxHandshakeAttempts);
protocolConfig.setClientConfig(newClientConfig);
return protocolConfig;

View File

@@ -228,11 +228,20 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
}
}
const bool isAwg3 = awgServerConfig && awgServerConfig->protocolVersion == protocols::awg::awgV3;
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
QString scriptData = amnezia::scriptData(m_configTemplate, container);
QString config = m_sshSession->replaceVars(scriptData, vars);
// The template lists every possible key, but each parameter is optional -
// drop the lines whose value came out empty
static const QRegularExpression emptyValueLine(R"(^\s*\S+\s*=\s*$)");
auto configTemplateLines = config.split("\n");
configTemplateLines.removeIf([](const QString &line) { return emptyValueLine.match(line).hasMatch(); });
config = configTemplateLines.join("\n");
ConnectionData connData = prepareWireguardConfig(credentials, container, wireguardServerConfig, awgServerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError) {
return WireGuardProtocolConfig{};
@@ -266,7 +275,8 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
clientConfig.presharedKey = connData.pskKey;
clientConfig.clientId = connData.clientPubKey;
clientConfig.allowedIps = QStringList { "0.0.0.0/0", "::/0" };
clientConfig.persistentKeepAlive = "25";
clientConfig.persistentKeepAlive = isAwg3 ? protocols::awg::defaultPersistentKeepAlive
: protocols::wireguard::defaultPersistentKeepAlive;
clientConfig.mtu = mtu;
clientConfig.isObfuscationEnabled = false;

View File

@@ -85,6 +85,12 @@ namespace {
return t.toLower();
}
// xray wants int ranges as "from-to" string, not a {from,to} object.
QString makeRangeString(const QString &minV, const QString &maxV)
{
return minV + QLatin1Char('-') + maxV;
}
void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax)
{
@@ -94,10 +100,23 @@ namespace {
minV = QString::fromLatin1(fallbackMin);
if (maxV.isEmpty())
maxV = QString::fromLatin1(fallbackMax);
QJsonObject r;
r[QStringLiteral("from")] = minV.toInt();
r[QStringLiteral("to")] = maxV.toInt();
obj[QString::fromUtf8(key)] = r;
obj[QString::fromUtf8(key)] = makeRangeString(minV, maxV);
}
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
{
const bool rawTransport = srv.transport.isEmpty() || srv.transport == QLatin1String("raw");
const bool secureFlow =
srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality");
return (rawTransport && secureFlow) ? srv.flow : QString();
}
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
{
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
return QStringLiteral("none");
}
return srv.security;
}
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
@@ -197,7 +216,7 @@ QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServ
{
QJsonObject streamSettings = buildStreamSettings(srv, QString());
if (srv.security != QLatin1String("reality")) {
if (effectiveSecurity(srv) != QLatin1String("reality")) {
return streamSettings;
}
@@ -244,10 +263,10 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
const QString flowValue = srv.flow;
const QString flowValue = effectiveClientFlow(srv);
QString realityPublicKey;
QString realityShortId;
if (srv.security == QLatin1String("reality")) {
if (effectiveSecurity(srv) == QLatin1String("reality")) {
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
@@ -363,6 +382,129 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
return ErrorCode::NoError;
}
ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, const ServerCredentials &credentials,
const QString &path, QString &out) const
{
out.clear();
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
out.replace(QLatin1Char('\n'), QString());
out.replace(QLatin1Char('\r'), QString());
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
}
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
{
Q_UNUSED(dnsSettings);
namespace px = amnezia::protocols::xray;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray writeServerConfigForSetup: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
if (srv.isThirdPartyConfig) {
logger.info() << "Xray writeServerConfigForSetup: skipped (third-party/native profile)";
return ErrorCode::NoError;
}
logger.info() << "Xray writeServerConfigForSetup: start container=" << static_cast<int>(container)
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port;
ErrorCode errorCode = ErrorCode::NoError;
QString clientId;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
const QString securityEff = effectiveSecurity(srv);
QString realityPrivateKey;
QString realityPublicKey;
QString realityShortId;
if (securityEff == QLatin1String("reality")) {
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
if (errorCode != ErrorCode::NoError)
return errorCode;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
if (errorCode != ErrorCode::NoError)
return errorCode;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
if (errorCode != ErrorCode::NoError)
return errorCode;
}
QJsonObject streamSettings = buildStreamSettings(srv, clientId);
if (securityEff == QLatin1String("reality")) {
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(px::defaultSite) : srv.site;
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
QJsonObject rs;
rs[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
rs[px::fingerprint] = fpEff;
rs[QStringLiteral("privateKey")] = realityPrivateKey;
rs[px::serverNames] = QJsonArray { sniEff };
rs[QStringLiteral("shortIds")] = QJsonArray { realityShortId };
streamSettings[px::realitySettings] = rs;
}
QJsonObject clientEntry;
clientEntry[px::id] = clientId;
const QString flowValue = effectiveClientFlow(srv);
if (!flowValue.isEmpty()) {
clientEntry[px::flow] = flowValue;
}
QJsonObject settings;
settings[px::clients] = QJsonArray { clientEntry };
settings[QStringLiteral("decryption")] = QStringLiteral("none");
QJsonObject inbound;
inbound[px::port] = srv.port.isEmpty() ? QString(px::defaultPort).toInt() : srv.port.toInt();
inbound[QStringLiteral("protocol")] = QStringLiteral("vless");
inbound[px::settings] = settings;
inbound[px::streamSettings] = streamSettings;
QJsonObject serverConfig;
serverConfig[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
serverConfig[px::inbounds] = QJsonArray { inbound };
serverConfig[px::outbounds] =
QJsonArray { QJsonObject { { QStringLiteral("protocol"), QStringLiteral("freedom") } } };
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
QString::fromLatin1(px::serverConfigPath),
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
containerConfig.protocolConfig = updated;
logger.info() << "Xray writeServerConfigForSetup: done, clientId=" << clientId;
return ErrorCode::NoError;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
@@ -389,7 +531,9 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
QString xrayPublicKey = prefetchedRealityPublicKey;
QString xrayShortId = prefetchedRealityShortId;
if (srv.security == QLatin1String("reality")) {
const QString securityEff = effectiveSecurity(srv);
if (securityEff == QLatin1String("reality")) {
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
if (errorCode != ErrorCode::NoError) {
@@ -401,8 +545,9 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = clientId;
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
const QString flowValue = effectiveClientFlow(srv);
if (!flowValue.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = flowValue;
}
QJsonObject vnextEntry;
@@ -419,7 +564,7 @@ XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCrede
outbound[amnezia::protocols::xray::settings] = outboundSettings;
QJsonObject streamObj = buildStreamSettings(srv, clientId);
if (srv.security == QLatin1String("reality")) {
if (securityEff == QLatin1String("reality")) {
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
rs[amnezia::protocols::xray::shortId] = xrayShortId;
@@ -468,18 +613,24 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
networkValue = QStringLiteral("kcp");
streamSettings[px::network] = networkValue;
streamSettings[px::security] = srv.security;
const QString securityEff = effectiveSecurity(srv);
streamSettings[px::security] = securityEff;
if (srv.security == QLatin1String("tls")) {
if (securityEff == QLatin1String("tls")) {
QJsonObject tlsSettings;
const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni;
tlsSettings[px::serverName] = sniEff;
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn;
QJsonArray alpnArray;
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
const QString t = a.trimmed();
if (!t.isEmpty())
alpnArray.append(t);
QString t = a.trimmed();
if (t.isEmpty())
continue;
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("h2");
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("http/1.1");
alpnArray.append(t);
}
if (!alpnArray.isEmpty())
tlsSettings[QStringLiteral("alpn")] = alpnArray;
@@ -488,7 +639,7 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
streamSettings[QStringLiteral("tlsSettings")] = tlsSettings;
}
if (srv.security == QLatin1String("reality")) {
if (securityEff == QLatin1String("reality")) {
QJsonObject realSettings;
const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint;
realSettings[px::fingerprint] = fpEff;
@@ -504,13 +655,13 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
xo[QStringLiteral("host")] = hostEff;
if (!xhttp.path.isEmpty())
xo[QStringLiteral("path")] = xhttp.path;
xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode);
if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) {
QJsonObject headers;
headers[QStringLiteral("Host")] = hostEff;
xo[QStringLiteral("headers")] = headers;
QString modeEff = normalizeXhttpMode(xhttp.mode);
if (modeEff == QLatin1String("auto") || modeEff == QLatin1String("packet-up")) {
modeEff = QStringLiteral("stream-one");
}
xo[QStringLiteral("mode")] = modeEff;
// No "Host" in headers: xray rejects it when the top-level "host" field is set.
const QString methodEff =
xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod;
@@ -521,27 +672,27 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement);
if (!sessPl.isEmpty())
xo[QStringLiteral("sessionPlacement")] = sessPl;
xo[QStringLiteral("sessionIDPlacement")] = sessPl;
const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement);
if (!seqPl.isEmpty())
xo[QStringLiteral("seqPlacement")] = seqPl;
if (!xhttp.sessionKey.isEmpty())
xo[QStringLiteral("sessionKey")] = xhttp.sessionKey;
xo[QStringLiteral("sessionIDKey")] = xhttp.sessionKey;
if (!xhttp.seqKey.isEmpty())
xo[QStringLiteral("seqKey")] = xhttp.seqKey;
xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
const QString uDataPl = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement);
const bool uDataNeedsPacketUp =
uDataPl == QLatin1String("header") || uDataPl == QLatin1String("cookie");
if (!(uDataNeedsPacketUp && modeEff != QLatin1String("packet-up")))
xo[QStringLiteral("uplinkDataPlacement")] = uDataPl;
if (!xhttp.uplinkDataKey.isEmpty())
xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey;
const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize)
: xhttp.uplinkChunkSize;
if (!ucs.isEmpty() && ucs != QLatin1String("0")) {
const int v = ucs.toInt();
QJsonObject chunkR;
chunkR[QStringLiteral("from")] = v;
chunkR[QStringLiteral("to")] = v;
xo[QStringLiteral("uplinkChunkSize")] = chunkR;
xo[QStringLiteral("uplinkChunkSize")] = ucs.toInt();
}
if (!xhttp.scMaxBufferedPosts.isEmpty())
@@ -558,17 +709,20 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode;
if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
QJsonObject br;
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
const int fromV = pad.bytesMin.isEmpty()
? QString::fromLatin1(px::defaultXPaddingBytesMin).toInt()
: pad.bytesMin.toInt();
int toV = pad.bytesMax.isEmpty()
? QString::fromLatin1(px::defaultXPaddingBytesMax).toInt()
: pad.bytesMax.toInt();
if (toV < fromV)
toV = fromV;
br[QStringLiteral("from")] = fromV;
br[QStringLiteral("to")] = toV;
xo[QStringLiteral("xPaddingBytes")] = br;
xo[QStringLiteral("xPaddingBytes")] = makeRangeString(QString::number(fromV), QString::number(toV));
}
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header;
xo[QStringLiteral("xPaddingKey")] =
pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
xo[QStringLiteral("xPaddingHeader")] =
pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement(
pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement);
xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod(
@@ -579,12 +733,14 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
if (xhttp.xmux.enabled) {
QJsonObject mux;
auto addMuxRange = [&](const char *key, const QString &a, const QString &b) {
if (a.isEmpty() && b.isEmpty())
// omit empty / 0-0 ranges (xray may reject "0-0")
const bool aZero = a.isEmpty() || a == QLatin1String("0");
const bool bZero = b.isEmpty() || b == QLatin1String("0");
if (aZero && bZero)
return;
QJsonObject r;
r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt();
r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt();
mux[QString::fromUtf8(key)] = r;
const QString aV = a.isEmpty() ? QStringLiteral("0") : a;
const QString bV = b.isEmpty() ? QStringLiteral("0") : b;
mux[QString::fromUtf8(key)] = makeRangeString(aV, bV);
};
addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);

View File

@@ -30,7 +30,16 @@ public:
bool appendNewClient,
QString *outClientId = nullptr);
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings);
private:
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
const QString &path, QString &out) const;
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);

View File

@@ -188,23 +188,14 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
// TODO looks like this block can be removed after v1 configs EOL
serverProtocolConfig[configKey::junkPacketCount] = clientProtocolConfig.value(configKey::junkPacketCount);
serverProtocolConfig[configKey::junkPacketMinSize] = clientProtocolConfig.value(configKey::junkPacketMinSize);
serverProtocolConfig[configKey::junkPacketMaxSize] = clientProtocolConfig.value(configKey::junkPacketMaxSize);
serverProtocolConfig[configKey::initPacketJunkSize] = clientProtocolConfig.value(configKey::initPacketJunkSize);
serverProtocolConfig[configKey::responsePacketJunkSize] = clientProtocolConfig.value(configKey::responsePacketJunkSize);
serverProtocolConfig[configKey::initPacketMagicHeader] = clientProtocolConfig.value(configKey::initPacketMagicHeader);
serverProtocolConfig[configKey::responsePacketMagicHeader] = clientProtocolConfig.value(configKey::responsePacketMagicHeader);
serverProtocolConfig[configKey::underloadPacketMagicHeader] = clientProtocolConfig.value(configKey::underloadPacketMagicHeader);
serverProtocolConfig[configKey::transportPacketMagicHeader] = clientProtocolConfig.value(configKey::transportPacketMagicHeader);
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
serverProtocolConfig[configKey::cookieReplyPacketJunkSize] = clientProtocolConfig.value(configKey::cookieReplyPacketJunkSize);
serverProtocolConfig[configKey::transportPacketJunkSize] = clientProtocolConfig.value(configKey::transportPacketJunkSize);
serverProtocolConfig[configKey::specialJunk1] = clientProtocolConfig.value(configKey::specialJunk1);
serverProtocolConfig[configKey::specialJunk2] = clientProtocolConfig.value(configKey::specialJunk2);
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = clientProtocolConfig.value(key);
if (value.isString() && !value.toString().isEmpty()) {
serverProtocolConfig[key] = value;
}
}
//

View File

@@ -31,7 +31,6 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::setConnectionStateRequested, m_vpnConnection, &VpnConnection::setConnectionState, Qt::QueuedConnection);
connect(this, &ConnectionController::killSwitchModeChangedRequested, m_vpnConnection, &VpnConnection::onKillSwitchModeChanged, Qt::QueuedConnection);
#ifdef Q_OS_ANDROID
connect(this, &ConnectionController::restoreConnectionRequested, m_vpnConnection, &VpnConnection::restoreConnection, Qt::QueuedConnection);
@@ -45,9 +44,7 @@ bool ConnectionController::isConnected() const
void ConnectionController::setConnectionState(Vpn::ConnectionState state)
{
if (m_vpnConnection) {
emit setConnectionStateRequested(state);
}
emit connectionStateChanged(state);
}
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const

View File

@@ -67,7 +67,6 @@ signals:
void connectionStateChanged(Vpn::ConnectionState state);
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
void closeConnectionRequested();
void setConnectionStateRequested(Vpn::ConnectionState state);
void killSwitchModeChangedRequested(bool enabled);
#ifdef Q_OS_ANDROID

View File

@@ -285,6 +285,10 @@ void CoreController::initSignalHandlers()
if (m_serversUiController->hasServersFromGatewayApi()) {
m_apiNewsUiController->fetchNews(false);
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
m_updateController->checkForUpdates();
#endif
}
void CoreController::updateTranslator(const QLocale &locale)

View File

@@ -440,9 +440,6 @@ void CoreSignalHandlers::initNotificationHandler()
void CoreSignalHandlers::initUpdateFoundHandler()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished, m_coreController->m_updateUiController,
&UpdateUiController::checkForUpdates);
connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
const QString version = m_coreController->m_updateUiController->getVersion();
const QString updateId = version.isEmpty() ? QStringLiteral("update") : QStringLiteral("update-%1").arg(version);

View File

@@ -1,6 +1,7 @@
#include "ipSplitTunnelingController.h"
#include "core/utils/networkUtilities.h"
#include <QJsonObject>
#include <QDebug>
IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent)
: QObject(parent),
@@ -14,47 +15,56 @@ IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsReposito
fillSites();
}
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QString &ip)
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QStringList &ips)
{
QVariantMap existing = m_appSettingsRepository->vpnSites(m_currentRouteMode);
if (existing.contains(hostname) && ip.isEmpty()) {
if (existing.contains(hostname) && ips.isEmpty()) {
return false;
}
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname && (m_sites[i].second.isEmpty() && !ip.isEmpty())) {
m_sites[i].second = ip;
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
if (m_sites[i].first == hostname) {
bool changed = false;
for (const QString &ip : ips) {
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
m_sites[i].second.append(ip);
changed = true;
}
}
if (!changed) {
return false;
}
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
return true;
} else if (m_sites[i].first == hostname && (m_sites[i].second == ip)) {
return false;
}
}
m_sites.append(qMakePair(hostname, ip));
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
m_sites.append(qMakePair(hostname, ips));
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
return true;
}
void IpSplitTunnelingController::addSites(const QMap<QString, QString> &sites, bool replaceExisting)
void IpSplitTunnelingController::addSites(const QMap<QString, QStringList> &sites, bool replaceExisting)
{
if (replaceExisting) {
m_sites.clear();
}
for (auto it = sites.constBegin(); it != sites.constEnd(); ++it) {
const QString &hostname = it.key();
const QString &ip = it.value();
const QStringList &ips = it.value();
bool found = false;
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname) {
if (!ip.isEmpty()) {
m_sites[i].second = ip;
for (const QString &ip : ips) {
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
m_sites[i].second.append(ip);
}
}
found = true;
break;
}
}
if (!found) {
m_sites.append(qMakePair(hostname, ip));
m_sites.append(qMakePair(hostname, ips));
}
}
if (replaceExisting) {
@@ -72,11 +82,11 @@ bool IpSplitTunnelingController::addSite(const QString &hostname)
}
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(normalizedHostname)) {
processSite(normalizedHostname, "");
processSite(normalizedHostname, {});
return true;
}
if (addSiteInternal(normalizedHostname, "")) {
if (addSiteInternal(normalizedHostname, {})) {
QHostInfo::lookupHost(normalizedHostname, this, SLOT(onHostResolved(QHostInfo)));
return true;
}
@@ -124,7 +134,7 @@ bool IpSplitTunnelingController::isSplitTunnelingEnabled() const
return m_appSettingsRepository->isSitesSplitTunnelingEnabled();
}
QVector<QPair<QString, QString>> IpSplitTunnelingController::getCurrentSites() const
QVector<QPair<QString, QStringList>> IpSplitTunnelingController::getCurrentSites() const
{
return m_sites;
}
@@ -134,7 +144,7 @@ void IpSplitTunnelingController::fillSites()
QVariantMap sitesMap = m_appSettingsRepository->vpnSites(m_currentRouteMode);
m_sites.clear();
for (auto it = sitesMap.begin(); it != sitesMap.end(); ++it) {
m_sites.append(qMakePair(it.key(), it.value().toString()));
m_sites.append(qMakePair(it.key(), SecureAppSettingsRepository::siteIpList(it.value())));
}
}
@@ -164,29 +174,40 @@ void IpSplitTunnelingController::onHostResolved(const QHostInfo &hostInfo)
{
const QList<QHostAddress> &addresses = hostInfo.addresses();
QString hostname = hostInfo.hostName();
QStringList allIpv4;
for (const QHostAddress &addr : addresses) {
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
processSiteAfterResolve(hostname, addr.toString());
break;
allIpv4.append(addr.toString());
}
}
allIpv4.removeDuplicates();
qDebug() << "[SplitTunneling] Host resolved:" << hostname
<< "-> adding all IPv4 addresses to list:" << allIpv4;
if (!allIpv4.isEmpty()) {
processSiteAfterResolve(hostname, allIpv4);
}
}
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QString &ip)
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QStringList &ips)
{
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname && m_sites[i].second.isEmpty()) {
m_sites[i].second = ip;
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
if (m_sites[i].first == hostname) {
for (const QString &ip : ips) {
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
m_sites[i].second.append(ip);
}
}
break;
}
}
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
}
void IpSplitTunnelingController::processSite(const QString &hostname, const QString &ip)
void IpSplitTunnelingController::processSite(const QString &hostname, const QStringList &ips)
{
addSiteInternal(hostname, ip);
addSiteInternal(hostname, ips);
}
bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage)
@@ -205,12 +226,25 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
}
QJsonArray jsonArray = jsonDocument.array();
QMap<QString, QString> sites;
QMap<QString, QStringList> sites;
for (auto jsonValue : jsonArray) {
QJsonObject jsonObject = jsonValue.toObject();
QString hostname = jsonObject.value("hostname").toString("");
QString ip = jsonObject.value("ip").toString("");
QStringList ips;
if (jsonObject.value("ips").isArray()) {
const QJsonArray ipsArray = jsonObject.value("ips").toArray();
for (const auto &ipValue : ipsArray) {
ips.append(ipValue.toString());
}
}
const QString singleIp = jsonObject.value("ip").toString("");
if (!singleIp.isEmpty()) {
ips.append(singleIp);
}
ips.removeAll(QString());
ips.removeDuplicates();
QString normalizedHostname = normalizeHostname(hostname);
@@ -219,7 +253,7 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
continue;
}
sites.insert(normalizedHostname, ip);
sites.insert(normalizedHostname, ips);
}
addSites(sites, replaceExisting);
@@ -229,13 +263,21 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
QByteArray IpSplitTunnelingController::exportSitesToJson() const
{
QVector<QPair<QString, QString>> sites = getCurrentSites();
QVector<QPair<QString, QStringList>> sites = getCurrentSites();
QJsonArray jsonArray;
for (const auto &site : sites) {
QJsonObject jsonObject;
jsonObject["hostname"] = site.first;
jsonObject["ip"] = site.second;
QJsonArray ipsArray;
for (const QString &ip : site.second) {
ipsArray.append(ip);
}
jsonObject["ips"] = ipsArray;
// Keep the legacy "ip" field (first address) for backward compatibility.
jsonObject["ip"] = site.second.isEmpty() ? QString() : site.second.first();
jsonArray.append(jsonObject);
}

View File

@@ -25,7 +25,7 @@ public:
explicit IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent = nullptr);
bool addSite(const QString &hostname);
void addSites(const QMap<QString, QString> &sites, bool replaceExisting);
void addSites(const QMap<QString, QStringList> &sites, bool replaceExisting);
bool removeSite(const QString &hostname);
void removeSites();
void setRouteMode(RouteMode routeMode);
@@ -33,7 +33,7 @@ public:
RouteMode getRouteMode() const;
bool isSplitTunnelingEnabled() const;
QVector<QPair<QString, QString>> getCurrentSites() const;
QVector<QPair<QString, QStringList>> getCurrentSites() const;
bool importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage);
QByteArray exportSitesToJson() const;
@@ -43,15 +43,15 @@ private slots:
private:
void fillSites();
bool addSiteInternal(const QString &hostname, const QString &ip);
bool addSiteInternal(const QString &hostname, const QStringList &ips);
QString normalizeHostname(const QString &hostname) const;
bool validateHostname(const QString &hostname) const;
void processSiteAfterResolve(const QString &hostname, const QString &ip);
void processSite(const QString &hostname, const QString &ip);
void processSiteAfterResolve(const QString &hostname, const QStringList &ips);
void processSite(const QString &hostname, const QStringList &ips);
SecureAppSettingsRepository* m_appSettingsRepository;
RouteMode m_currentRouteMode;
QVector<QPair<QString, QString>> m_sites;
QVector<QPair<QString, QStringList>> m_sites;
};
#endif // IPSPLITTUNNELINGCONTROLLER_H

View File

@@ -572,47 +572,19 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
QString protocolName = configKey::wireguard;
QString protocolVersion;
ConfigTypes detectedType = ConfigTypes::WireGuard;
const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize,
configKey::junkPacketMaxSize, configKey::initPacketJunkSize,
configKey::responsePacketJunkSize, configKey::initPacketMagicHeader,
configKey::responsePacketMagicHeader, configKey::underloadPacketMagicHeader,
configKey::transportPacketMagicHeader };
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
configKey::transportPacketJunkSize,
configKey::specialJunk1, configKey::specialJunk2, configKey::specialJunk3,
configKey::specialJunk4, configKey::specialJunk5
};
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAllRequiredFields) {
for (const QString &field : requiredJunkFields) {
lastConfig[field] = configMap.value(field);
}
for (const QString &field : optionalJunkFields) {
if (!configMap.value(field).isEmpty()) {
lastConfig[field] = configMap.value(field);
bool hasAwgKeys = std::any_of(awgProtocolKeys.begin(), awgProtocolKeys.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAwgKeys) {
for (const QString &key : awgProtocolKeys) {
if (!configMap.value(key).isEmpty()) {
lastConfig[key] = configMap.value(key);
}
}
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
!configMap.value(configKey::specialJunk2).isEmpty() ||
!configMap.value(configKey::specialJunk3).isEmpty() ||
!configMap.value(configKey::specialJunk4).isEmpty() ||
!configMap.value(configKey::specialJunk5).isEmpty();
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
protocolVersion = "2";
} else if (hasSpecialJunk && !hasCookieReplyPacketJunkSize && !hasTransportPacketJunkSize) {
protocolVersion = "1.5";
}
protocolName = configKey::awg;
detectedType = ConfigTypes::Awg;
}
@@ -630,9 +602,6 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
wireguardConfig[configKey::isThirdPartyConfig] = true;
wireguardConfig[configKey::port] = port;
wireguardConfig[configKey::transportProto] = protocols::openvpn::defaultTransportProto;
if (protocolName == configKey::awg && !protocolVersion.isEmpty()) {
wireguardConfig[configKey::protocolVersion] = protocolVersion;
}
QJsonObject containers;
QString containerName = (protocolName == configKey::awg) ? configKey::amneziaAwg : configKey::amneziaWireguard;

View File

@@ -11,6 +11,7 @@
#include <QtConcurrent>
#include "core/configurators/configuratorBase.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -152,6 +153,15 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e;
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
}
setupServerFirewall(credentials, sshSession);
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
@@ -191,11 +201,21 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
SshSession sshSession;
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
reinstallRequired = true;
}
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
errorCode = setupContainer(credentials, container, newConfig, true);
// Reinstall pulls the latest container image, so the server runs the latest protocol version
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
if (auto* awgConfig = newConfig.getAwgProtocolConfig()) {
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
}
}
} else {
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
if (errorCode == ErrorCode::NoError) {
@@ -397,6 +417,11 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
}
if (ContainerUtils::containerService(container) != ServiceType::Other) {
if ((container == DockerContainer::Xray || container == DockerContainer::SSXray)
&& containerConfig.protocolConfig.hasClientConfig()) {
return ErrorCode::NoError;
}
Proto protocol = ContainerUtils::defaultProtocol(container);
DnsSettings dnsSettings = {
@@ -484,6 +509,12 @@ ErrorCode InstallController::buildContainerWorker(const ServerCredentials &crede
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
return ErrorCode::DockerPullRateLimit;
if (stdOut.contains("returned a non-zero code")
|| stdOut.contains("failed to solve")
|| stdOut.contains("Unable to find image")
|| stdOut.contains("Couldn't connect to server"))
return ErrorCode::ServerDockerFailedError;
return error;
}
@@ -508,6 +539,8 @@ ErrorCode InstallController::runContainerWorker(const ServerCredentials &credent
return ErrorCode::ServerPortAlreadyAllocatedError;
if (stdOut.contains("invalid publish"))
return ErrorCode::ServerDockerFailedError;
if (stdOut.contains("Unable to find image") || stdOut.contains("No such image"))
return ErrorCode::ServerDockerFailedError;
return e;
}

View File

@@ -1,11 +1,10 @@
#include "awgInstaller.h"
#include <QPair>
#include <QRandomGenerator>
#include <QSet>
#include <QStringList>
#include <QVector>
#include "core/configurators/wireguardConfigurator.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -28,109 +27,57 @@ AwgInstaller::AwgInstaller(QObject *parent)
ContainerConfig AwgInstaller::generateConfig(DockerContainer container, int port, TransportProto transportProto)
{
ContainerConfig config = createBaseConfig(container, port, transportProto);
bool isAwg2 = (container == DockerContainer::Awg2);
if (auto* awgConfig = config.getAwgProtocolConfig()) {
generateAwgParameters(awgConfig->serverConfig, isAwg2);
if (isAwg2) {
awgConfig->serverConfig.protocolVersion = "2";
}
generateAwgParameters(awgConfig->serverConfig);
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
}
return config;
}
void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig, bool isAwg2)
void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig)
{
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7));
QString junkPacketMinSize = QString::number(10);
QString junkPacketMaxSize = QString::number(50);
int s1 = QRandomGenerator::global()->bounded(15, 150);
int s2 = QRandomGenerator::global()->bounded(15, 150);
int s3 = QRandomGenerator::global()->bounded(0, 64);
int s4 = QRandomGenerator::global()->bounded(0, 20);
int s1 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::initPacketJunkSizeMax);
int s2 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::responsePacketJunkSizeMax);
int s3 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::cookieReplyPacketJunkSizeMax);
int s4 = protocols::awg::defaultTransportPacketJunkSize;
// Ensure all values are unique and don't create equal packet sizes
QSet<int> usedValues;
usedValues.insert(s1);
QSet<int> usedValues { s1, s4 };
while (usedValues.contains(s2) || s1 + amnezia::AwgConstant::messageInitiationSize == s2 + amnezia::AwgConstant::messageResponseSize) {
s2 = QRandomGenerator::global()->bounded(15, 150);
s2 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::responsePacketJunkSizeMax);
}
usedValues.insert(s2);
while (usedValues.contains(s3) || s1 + amnezia::AwgConstant::messageInitiationSize == s3 + amnezia::AwgConstant::messageCookieReplySize
|| s2 + amnezia::AwgConstant::messageResponseSize == s3 + amnezia::AwgConstant::messageCookieReplySize) {
s3 = QRandomGenerator::global()->bounded(0, 64);
}
usedValues.insert(s3);
while (usedValues.contains(s4)) {
s4 = QRandomGenerator::global()->bounded(0, 20);
}
QString initPacketJunkSize = QString::number(s1);
QString responsePacketJunkSize = QString::number(s2);
QString cookieReplyPacketJunkSize = QString::number(s3);
QString transportPacketJunkSize = QString::number(s4);
QString initPacketMagicHeader;
QString responsePacketMagicHeader;
QString underloadPacketMagicHeader;
QString transportPacketMagicHeader;
if (isAwg2) {
// AWG 2.0: use range format for magic headers
QVector<QPair<QString, QString>> headersValue;
int min = 5;
auto max = (std::numeric_limits<qint32>::max)();
while (headersValue.size() != 4) {
auto first = QRandomGenerator::global()->bounded(min, max);
auto second = QRandomGenerator::global()->bounded(first, max);
min = second;
headersValue.push_back(QPair<QString, QString>(QString::number(first), QString::number(second)));
}
initPacketMagicHeader = headersValue.at(0).first + "-" + headersValue.at(0).second;
responsePacketMagicHeader = headersValue.at(1).first + "-" + headersValue.at(1).second;
underloadPacketMagicHeader = headersValue.at(2).first + "-" + headersValue.at(2).second;
transportPacketMagicHeader = headersValue.at(3).first + "-" + headersValue.at(3).second;
} else {
// AWG legacy: use single values for magic headers
QSet<QString> headersValue;
while (headersValue.size() != 4) {
auto max = (std::numeric_limits<qint32>::max)();
headersValue.insert(QString::number(QRandomGenerator::global()->bounded(5, max)));
}
auto headersValueList = headersValue.values();
initPacketMagicHeader = headersValueList.at(0);
responsePacketMagicHeader = headersValueList.at(1);
underloadPacketMagicHeader = headersValueList.at(2);
transportPacketMagicHeader = headersValueList.at(3);
s3 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::cookieReplyPacketJunkSizeMax);
}
serverConfig.junkPacketCount = junkPacketCount;
serverConfig.junkPacketMinSize = junkPacketMinSize;
serverConfig.junkPacketMaxSize = junkPacketMaxSize;
serverConfig.initPacketJunkSize = initPacketJunkSize;
serverConfig.responsePacketJunkSize = responsePacketJunkSize;
serverConfig.initPacketMagicHeader = initPacketMagicHeader;
serverConfig.responsePacketMagicHeader = responsePacketMagicHeader;
serverConfig.underloadPacketMagicHeader = underloadPacketMagicHeader;
serverConfig.transportPacketMagicHeader = transportPacketMagicHeader;
serverConfig.initPacketJunkSize = QString::number(s1);
serverConfig.responsePacketJunkSize = QString::number(s2);
serverConfig.cookieReplyPacketJunkSize = QString::number(s3);
serverConfig.transportPacketJunkSize = QString::number(s4);
serverConfig.cookieReplyPacketJunkSize = cookieReplyPacketJunkSize;
serverConfig.transportPacketJunkSize = transportPacketJunkSize;
serverConfig.initPacketMagicHeader = protocols::awg::defaultInitPacketMagicHeader;
serverConfig.responsePacketMagicHeader = protocols::awg::defaultResponsePacketMagicHeader;
serverConfig.underloadPacketMagicHeader = protocols::awg::defaultUnderloadPacketMagicHeader;
serverConfig.transportPacketMagicHeader = protocols::awg::defaultTransportPacketMagicHeader;
serverConfig.specialJunk1 = protocols::awg::defaultSpecialJunk1;
serverConfig.specialJunk2 = protocols::awg::defaultSpecialJunk2;
serverConfig.specialJunk3 = protocols::awg::defaultSpecialJunk3;
serverConfig.specialJunk4 = protocols::awg::defaultSpecialJunk4;
serverConfig.specialJunk5 = protocols::awg::defaultSpecialJunk5;
serverConfig.headerProtectionKey = WireguardConfigurator::genClientKeys().clientPrivKey;
serverConfig.contentPaddingAddition = protocols::awg::defaultContentPaddingAddition;
serverConfig.rekeyAfterTime = protocols::awg::defaultRekeyAfterTime;
serverConfig.rekeyTimeout = protocols::awg::defaultRekeyTimeout;
serverConfig.rejectAfterTime = protocols::awg::defaultRejectAfterTime;
serverConfig.keepaliveTimeout = protocols::awg::defaultKeepaliveTimeout;
serverConfig.maxHandshakeAttempts = protocols::awg::defaultMaxHandshakeAttempts;
}
ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
@@ -187,14 +134,20 @@ ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, co
awgConfig->serverConfig.specialJunk4 = serverConfigMap.value(QString("# ") + configKey::specialJunk4);
awgConfig->serverConfig.specialJunk5 = serverConfigMap.value(QString("# ") + configKey::specialJunk5);
// AWG 2.0 specific fields
if (container == DockerContainer::Awg2) {
awgConfig->serverConfig.protocolVersion = "2";
awgConfig->serverConfig.cookieReplyPacketJunkSize = serverConfigMap.value(configKey::cookieReplyPacketJunkSize);
awgConfig->serverConfig.transportPacketJunkSize = serverConfigMap.value(configKey::transportPacketJunkSize);
}
awgConfig->serverConfig.cookieReplyPacketJunkSize = serverConfigMap.value(configKey::cookieReplyPacketJunkSize);
awgConfig->serverConfig.transportPacketJunkSize = serverConfigMap.value(configKey::transportPacketJunkSize);
awgConfig->serverConfig.headerProtectionKey = serverConfigMap.value(configKey::headerProtectionKey);
awgConfig->serverConfig.contentPaddingAddition = serverConfigMap.value(configKey::contentPaddingAddition);
awgConfig->serverConfig.rekeyAfterTime = serverConfigMap.value(configKey::rekeyAfterTime);
awgConfig->serverConfig.rekeyTimeout = serverConfigMap.value(configKey::rekeyTimeout);
awgConfig->serverConfig.rejectAfterTime = serverConfigMap.value(configKey::rejectAfterTime);
awgConfig->serverConfig.keepaliveTimeout = serverConfigMap.value(configKey::keepaliveTimeout);
awgConfig->serverConfig.maxHandshakeAttempts = serverConfigMap.value(configKey::maxHandshakeAttempts);
awgConfig->serverConfig.protocolVersion = awgConfig->serverProtocolVersion();
}
return ErrorCode::NoError;
}

View File

@@ -14,7 +14,7 @@ public:
SshSession* serverController, amnezia::ContainerConfig &config) override;
private:
void generateAwgParameters(amnezia::AwgServerConfig &serverConfig, bool isAwg2 = false);
void generateAwgParameters(amnezia::AwgServerConfig &serverConfig);
};
#endif // AWGINSTALLER_H

View File

@@ -56,6 +56,7 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
AwgProtocolConfig awgConfig;
awgConfig.serverConfig.port = portStr;
awgConfig.serverConfig.transportProto = transportProtoStr;
awgConfig.serverConfig.subnetAddress = protocols::wireguard::defaultSubnetAddress;
config.protocolConfig = awgConfig;
break;
}
@@ -63,6 +64,7 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
WireGuardProtocolConfig wgConfig;
wgConfig.serverConfig.port = portStr;
wgConfig.serverConfig.transportProto = transportProtoStr;
wgConfig.serverConfig.subnetAddress = protocols::wireguard::defaultSubnetAddress;
config.protocolConfig = wgConfig;
break;
}
@@ -76,8 +78,16 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
case Proto::Xray:
case Proto::SSXray: {
XrayProtocolConfig xrayConfig;
xrayConfig.serverConfig.port = portStr;
xrayConfig.serverConfig.transportProto = transportProtoStr;
XrayServerConfig &srv = xrayConfig.serverConfig;
srv.port = portStr;
srv.transportProto = transportProtoStr;
srv.transport = protocols::xray::defaultTransport;
srv.security = protocols::xray::defaultSecurity;
srv.flow = protocols::xray::defaultFlow;
srv.site = protocols::xray::defaultSite;
srv.sni = protocols::xray::defaultSni;
srv.fingerprint = protocols::xray::defaultFingerprint;
srv.alpn = protocols::xray::defaultAlpn;
config.protocolConfig = xrayConfig;
break;
}

View File

@@ -2,6 +2,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
@@ -20,6 +21,8 @@ namespace {
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxySecretPath("/data/secret");
constexpr QLatin1String kMtProxyMetaPath("/data/mtproxy-meta");
constexpr QLatin1String kMtProxyStartScriptPath("/opt/amnezia/start.sh");
}
MtProxyInstaller::MtProxyInstaller(QObject *parent)
@@ -53,14 +56,98 @@ ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container
}
}
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
const auto addExtra = [&](const QString &s) {
if (hex32.match(s).hasMatch() && !mt->additionalSecrets.contains(s)) {
mt->additionalSecrets.append(s);
}
};
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
mt->secret = sec;
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
mt->secret = sec;
}
bool metaRestored = false;
ErrorCode metaErr = ErrorCode::NoError;
const QByteArray metaRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyMetaPath), metaErr);
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
QString mode, domain, workersMode, workers, natInternal, natExternal;
bool natEnabled = false;
const QList<QByteArray> lines = metaRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq);
const QString val = line.mid(eq + 1).trimmed();
if (key == QLatin1String("mode")) mode = val;
else if (key == QLatin1String("domain")) domain = val;
else if (key == QLatin1String("tag")) { if (mt->tag.isEmpty()) mt->tag = val; }
else if (key == QLatin1String("additional")) {
for (const QString &s : val.split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
else if (key == QLatin1String("workers_mode")) workersMode = val;
else if (key == QLatin1String("workers")) workers = val;
else if (key == QLatin1String("nat_enabled")) natEnabled = (val == QLatin1String("1"));
else if (key == QLatin1String("nat_internal")) natInternal = val;
else if (key == QLatin1String("nat_external")) natExternal = val;
else if (key == QLatin1String("public_host")) { if (mt->publicHost.isEmpty()) mt->publicHost = val; }
}
if (!mode.isEmpty()) {
mt->transportMode = mode;
if (!domain.isEmpty()) mt->tlsDomain = domain;
if (!workersMode.isEmpty()) mt->workersMode = workersMode;
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual) && !workers.isEmpty()) {
mt->workers = workers;
}
if (natEnabled) {
mt->natEnabled = true;
mt->natInternalIp = natInternal;
mt->natExternalIp = natExternal;
}
metaRestored = true;
}
}
if (!metaRestored) {
ErrorCode startErr = ErrorCode::NoError;
const QByteArray startRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyStartScriptPath), startErr);
if (startErr == ErrorCode::NoError && !startRaw.trimmed().isEmpty()) {
const QString start = QString::fromUtf8(startRaw);
static const QRegularExpression modeRe(QStringLiteral("\\[ \"(standard|faketls)\" = \"faketls\" \\]"));
const QRegularExpressionMatch m = modeRe.match(start);
if (m.hasMatch()) {
mt->transportMode = m.captured(1);
if (m.captured(1) == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) {
static const QRegularExpression domRe(QStringLiteral("--domain ([A-Za-z0-9.\\-]+)"));
const QRegularExpressionMatch dm = domRe.match(start);
if (dm.hasMatch()) mt->tlsDomain = dm.captured(1);
}
}
static const QRegularExpression tagRe(QStringLiteral("-P ([0-9a-fA-F]{32})"));
const QRegularExpressionMatch tm = tagRe.match(start);
if (tm.hasMatch() && mt->tag.isEmpty()) mt->tag = tm.captured(1);
static const QRegularExpression addRe(QStringLiteral("echo \"([0-9a-fA-F,]+)\" \\| tr ',' ' '"));
const QRegularExpressionMatch am = addRe.match(start);
if (am.hasMatch()) {
for (const QString &s : am.captured(1).split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
static const QRegularExpression natRe(QStringLiteral("NAT_VALUE=\"([0-9.]+):([0-9.]+)\""));
const QRegularExpressionMatch nm = natRe.match(start);
if (nm.hasMatch()) {
mt->natEnabled = true;
mt->natInternalIp = nm.captured(1);
mt->natExternalIp = nm.captured(2);
}
}
}

View File

@@ -2,6 +2,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
@@ -19,6 +20,7 @@ namespace {
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtSecretPath("/data/secret");
constexpr QLatin1String kTelemtConfigTomlPath("/data/config.toml");
}
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
@@ -54,10 +56,83 @@ ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container,
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
tc->secret = sec;
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
tc->secret = sec;
}
ErrorCode tomlErr = ErrorCode::NoError;
const QByteArray tomlRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtConfigTomlPath), tomlErr);
if (tomlErr == ErrorCode::NoError && !tomlRaw.trimmed().isEmpty()) {
QString section;
bool userNameSet = false;
const QList<QByteArray> lines = tomlRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
if (line.isEmpty() || line.startsWith('#')) {
continue;
}
if (line.startsWith('[')) {
section = line;
continue;
}
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq).trimmed();
QString val = line.mid(eq + 1).trimmed();
if (val.startsWith('"')) {
const int last = val.lastIndexOf('"');
val = (last > 0) ? val.mid(1, last - 1) : val.mid(1);
} else {
const int inlineComment = val.indexOf(QLatin1String(" #"));
if (inlineComment >= 0) {
val = val.left(inlineComment).trimmed();
}
}
if (section == QLatin1String("[access.users]")) {
if (key.startsWith(QLatin1String("extra"))) {
if (hex32.match(val).hasMatch() && !tc->additionalSecrets.contains(val)) {
tc->additionalSecrets.append(val);
}
} else if (!userNameSet) {
tc->userName = key;
userNameSet = true;
if (tc->secret.isEmpty() && hex32.match(val).hasMatch()) {
tc->secret = val;
}
}
continue;
}
if (key == QLatin1String("tls") && section == QLatin1String("[general.modes]")) {
tc->transportMode = (val == QLatin1String("true"))
? QString::fromUtf8(protocols::telemt::transportModeFakeTLS)
: QString::fromUtf8(protocols::telemt::transportModeStandard);
} else if (key == QLatin1String("tls_domain") && section == QLatin1String("[censorship]")) {
tc->tlsDomain = val;
} else if (key == QLatin1String("mask") && section == QLatin1String("[censorship]")) {
tc->maskEnabled = (val == QLatin1String("true"));
} else if (key == QLatin1String("tls_emulation") && section == QLatin1String("[censorship]")) {
tc->tlsEmulation = (val == QLatin1String("true"));
} else if (key == QLatin1String("use_middle_proxy") && section == QLatin1String("[general]")) {
tc->useMiddleProxy = (val == QLatin1String("true"));
} else if (key == QLatin1String("middle_proxy_nat_ip") && section == QLatin1String("[general]")) {
if (!val.isEmpty()) {
tc->natExternalIp = val;
tc->natEnabled = true;
}
} else if (key == QLatin1String("ad_tag") && section == QLatin1String("[general]") && tc->tag.isEmpty()) {
tc->tag = val;
} else if (key == QLatin1String("public_host") && section == QLatin1String("[general.links]")
&& tc->publicHost.isEmpty()) {
tc->publicHost = val;
} else if (key == QLatin1String("port") && section == QLatin1String("[server]") && tc->port.isEmpty()) {
tc->port = val;
}
}
}

View File

@@ -26,6 +26,31 @@ namespace
}
return fp;
}
// Parse an xray int range: "from-to" string, plain int, or legacy {from,to} object.
void parseIntRange(const QJsonValue &v, QString &minOut, QString &maxOut)
{
if (v.isString()) {
const QString s = v.toString().trimmed();
const int dash = s.indexOf(QLatin1Char('-'), 1);
if (dash > 0) {
minOut = s.left(dash).trimmed();
maxOut = s.mid(dash + 1).trimmed();
} else if (!s.isEmpty()) {
minOut = s;
maxOut = s;
}
} else if (v.isDouble()) {
minOut = QString::number(v.toInt());
maxOut = minOut;
} else if (v.isObject()) {
const QJsonObject o = v.toObject();
if (o.contains(QLatin1String("from")) || o.contains(QLatin1String("to"))) {
minOut = QString::number(o.value(QLatin1String("from")).toInt());
maxOut = QString::number(o.value(QLatin1String("to")).toInt());
}
}
}
}
using namespace amnezia;
@@ -125,7 +150,13 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
QJsonArray alpnArr = tls.value("alpn").toArray();
QStringList alpnList;
for (const QJsonValue &v : alpnArr) {
alpnList << v.toString();
QString t = v.toString().trimmed();
if (t.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("h2");
else if (t.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
t = QStringLiteral("http/1.1");
if (!t.isEmpty())
alpnList << t;
}
srv.alpn = alpnList.join(",");
}
@@ -159,12 +190,6 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
srv.xhttp.host = xhttpObj.value("host").toString();
srv.xhttp.path = xhttpObj.value("path").toString();
{
const QJsonObject hdrs = xhttpObj.value("headers").toObject();
if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty())
srv.xhttp.headersTemplate = QStringLiteral("HTTP");
}
if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod")))
srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString();
else
@@ -184,7 +209,9 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
return QStringLiteral("Query");
return core;
};
QString sess = xhttpObj.value("sessionPlacement").toString();
QString sess = xhttpObj.value("sessionIDPlacement").toString();
if (sess.isEmpty())
sess = xhttpObj.value("sessionPlacement").toString();
if (sess.isEmpty())
sess = xhttpObj.value("scSessionPlacement").toString();
srv.xhttp.sessionPlacement = sessionSeqUi(sess);
@@ -210,14 +237,17 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
udata = xhttpObj.value("scUplinkDataPlacement").toString();
srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata);
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
srv.xhttp.sessionKey = xhttpObj.value("sessionIDKey").toString();
if (srv.xhttp.sessionKey.isEmpty())
srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString();
srv.xhttp.seqKey = xhttpObj.value("seqKey").toString();
srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString();
if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) {
QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject();
if (!uc.isEmpty())
srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt());
QString ucMin, ucMax;
parseIntRange(xhttpObj.value("uplinkChunkSize"), ucMin, ucMax);
if (!ucMin.isEmpty())
srv.xhttp.uplinkChunkSize = ucMin;
} else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) {
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt());
}
@@ -226,11 +256,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
}
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject();
if (!r.isEmpty()) {
minOut = QString::number(r.value("from").toInt());
maxOut = QString::number(r.value("to").toInt());
}
parseIntRange(xhttpObj.value(QLatin1String(key)), minOut, maxOut);
};
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
@@ -243,10 +269,11 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString();
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString();
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
if (!bytesRange.isEmpty()) {
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
QString bytesMin, bytesMax;
parseIntRange(pad.value("xPaddingBytes"), bytesMin, bytesMax);
if (!bytesMin.isEmpty()) {
srv.xhttp.xPadding.bytesMin = bytesMin;
srv.xhttp.xPadding.bytesMax = bytesMax;
}
QString pl = srv.xhttp.xPadding.placement.toLower();
if (pl == QLatin1String("cookie"))
@@ -264,7 +291,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
srv.xhttp.xPadding.method = QStringLiteral("Tokenish");
};
if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey"))
|| !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) {
|| xhttpObj.contains(QLatin1String("xPaddingBytes"))) {
loadPaddingFromObject(xhttpObj);
} else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) {
const QJsonObject nested = xhttpObj.value("xPadding").toObject();
@@ -280,11 +307,7 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
srv.xhttp.xmux.enabled = true;
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
QJsonObject r = mux.value(QLatin1String(key)).toObject();
if (!r.isEmpty()) {
minOut = QString::number(r.value("from").toInt());
maxOut = QString::number(r.value("to").toInt());
}
parseIntRange(mux.value(QLatin1String(key)), minOut, maxOut);
};
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);

View File

@@ -2,6 +2,10 @@
#include <QJsonDocument>
#include <QJsonArray>
#include <QObject>
#include <QSet>
#include <algorithm>
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
@@ -13,6 +17,41 @@ using namespace ProtocolUtils;
namespace amnezia
{
namespace
{
template <typename T>
QString awgVersionOf(const T &config)
{
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
const QStringList awg3Params = { config.headerProtectionKey, config.contentPaddingAddition,
config.rekeyAfterTime, config.rekeyTimeout,
config.rejectAfterTime, config.keepaliveTimeout,
config.maxHandshakeAttempts };
if (std::any_of(awg3Params.begin(), awg3Params.end(), hasValue)) {
return protocols::awg::awgV3;
}
const QStringList junkSizes = { config.cookieReplyPacketJunkSize, config.transportPacketJunkSize };
const QStringList magicHeaders = { config.initPacketMagicHeader, config.responsePacketMagicHeader,
config.underloadPacketMagicHeader, config.transportPacketMagicHeader };
bool hasJunkSizes = std::any_of(junkSizes.begin(), junkSizes.end(), hasValue);
bool hasHeaderRanges = std::any_of(magicHeaders.begin(), magicHeaders.end(),
[](const QString &header) { return header.contains('-'); });
if (hasJunkSizes || hasHeaderRanges) {
return protocols::awg::awgV2;
}
const QStringList specialJunk = { config.specialJunk1, config.specialJunk2, config.specialJunk3,
config.specialJunk4, config.specialJunk5 };
if (std::any_of(specialJunk.begin(), specialJunk.end(), hasValue)) {
return protocols::awg::awgV1_5;
}
return QString();
}
} // namespace
QJsonObject AwgServerConfig::toJson() const
{
QJsonObject obj;
@@ -74,6 +113,28 @@ QJsonObject AwgServerConfig::toJson() const
obj[configKey::specialJunk4] = specialJunk4;
obj[configKey::specialJunk5] = specialJunk5;
if (!headerProtectionKey.isEmpty()) {
obj[configKey::headerProtectionKey] = headerProtectionKey;
}
if (!contentPaddingAddition.isEmpty()) {
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
}
if (!rekeyAfterTime.isEmpty()) {
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
}
if (!rekeyTimeout.isEmpty()) {
obj[configKey::rekeyTimeout] = rekeyTimeout;
}
if (!rejectAfterTime.isEmpty()) {
obj[configKey::rejectAfterTime] = rejectAfterTime;
}
if (!keepaliveTimeout.isEmpty()) {
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
}
if (!maxHandshakeAttempts.isEmpty()) {
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
}
if (isThirdPartyConfig) {
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
}
@@ -109,7 +170,15 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
return config;
@@ -211,11 +280,29 @@ QJsonObject AwgClientConfig::toJson() const
if (!specialJunk5.isEmpty()) {
obj[configKey::specialJunk5] = specialJunk5;
}
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
if (!headerProtectionKey.isEmpty()) {
obj[configKey::headerProtectionKey] = headerProtectionKey;
}
if (!contentPaddingAddition.isEmpty()) {
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
}
if (!rekeyAfterTime.isEmpty()) {
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
}
if (!rekeyTimeout.isEmpty()) {
obj[configKey::rekeyTimeout] = rekeyTimeout;
}
if (!rejectAfterTime.isEmpty()) {
obj[configKey::rejectAfterTime] = rejectAfterTime;
}
if (!keepaliveTimeout.isEmpty()) {
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
}
if (!maxHandshakeAttempts.isEmpty()) {
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
}
return obj;
}
@@ -258,21 +345,26 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
return config;
}
QJsonObject AwgProtocolConfig::toJson() const
{
QJsonObject obj = serverConfig.toJson();
if (clientConfig.has_value()) {
QJsonObject clientJson = clientConfig->toJson();
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
}
return obj;
}
@@ -293,6 +385,24 @@ AwgProtocolConfig AwgProtocolConfig::fromJson(const QJsonObject& json)
return config;
}
QString AwgProtocolConfig::serverProtocolVersion() const
{
return awgVersionOf(serverConfig);
}
QString AwgProtocolConfig::clientProtocolVersion() const
{
return clientConfig.has_value() ? awgVersionOf(clientConfig.value()) : QString();
}
QString AwgProtocolConfig::protocolVersionString(const QString &version)
{
if (version == protocols::awg::awgV3) return QObject::tr(" (version 3)");
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}
bool AwgProtocolConfig::hasClientConfig() const
{
return clientConfig.has_value();
@@ -320,24 +430,31 @@ bool AwgServerConfig::hasEqualServerSettings(const AwgServerConfig& other) const
transportPacketMagicHeader != other.transportPacketMagicHeader ||
specialJunk1 != other.specialJunk1 || specialJunk2 != other.specialJunk2 ||
specialJunk3 != other.specialJunk3 || specialJunk4 != other.specialJunk4 ||
specialJunk5 != other.specialJunk5) {
specialJunk5 != other.specialJunk5 ||
cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize ||
headerProtectionKey != other.headerProtectionKey ||
contentPaddingAddition != other.contentPaddingAddition ||
rekeyAfterTime != other.rekeyAfterTime || rekeyTimeout != other.rekeyTimeout ||
rejectAfterTime != other.rejectAfterTime || keepaliveTimeout != other.keepaliveTimeout ||
maxHandshakeAttempts != other.maxHandshakeAttempts) {
return false;
}
bool isV2 = protocolVersion == protocols::awg::awgV2;
if (isV2) {
if (cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize) {
return false;
}
}
return true;
}
bool AwgProtocolConfig::isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4)
{
return (h1 == h2) || (h1 == h3) || (h1 == h4) || (h2 == h3) || (h2 == h4) || (h3 == h4);
QSet<QString> uniqueHeaders;
int filledHeaders = 0;
for (const QString &header : { h1, h2, h3, h4 }) {
if (!header.trimmed().isEmpty()) {
++filledHeaders;
uniqueHeaders.insert(header);
}
}
return uniqueHeaders.size() != filledHeaders;
}
bool AwgProtocolConfig::isPacketSizeEqual(int s1, int s2, int s3, int s4)

View File

@@ -39,6 +39,13 @@ struct AwgServerConfig {
QString specialJunk3;
QString specialJunk4;
QString specialJunk5;
QString headerProtectionKey;
QString contentPaddingAddition;
QString rekeyAfterTime;
QString rekeyTimeout;
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
bool isThirdPartyConfig = false;
QJsonObject toJson() const;
@@ -76,8 +83,13 @@ struct AwgClientConfig {
QString specialJunk3;
QString specialJunk4;
QString specialJunk5;
bool isObfuscationEnabled = false;
QString headerProtectionKey;
QString contentPaddingAddition;
QString rekeyAfterTime;
QString rekeyTimeout;
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
QJsonObject toJson() const;
static AwgClientConfig fromJson(const QJsonObject& json);
};
@@ -89,6 +101,10 @@ struct AwgProtocolConfig {
QJsonObject toJson() const;
static AwgProtocolConfig fromJson(const QJsonObject& json);
QString serverProtocolVersion() const;
QString clientProtocolVersion() const;
static QString protocolVersionString(const QString &version);
bool hasClientConfig() const;
void setClientConfig(const AwgClientConfig& config);
void clearClientConfig();

View File

@@ -99,9 +99,6 @@ bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolCo
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
};
const auto normWorkersMode = [](const QString &m) {
return m.isEmpty() ? QString(protocols::telemt::workersModeAuto) : m;
};
if (normPort(port) != normPort(other.port)) {
return false;
@@ -133,18 +130,9 @@ bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolCo
if (userName != other.userName) {
return false;
}
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
return false;
}
if (workers != other.workers) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natInternalIp != other.natInternalIp) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}

View File

@@ -105,11 +105,17 @@ QJsonObject WireGuardClientConfig::toJson() const
if (!mtu.isEmpty()) {
obj[configKey::mtu] = mtu;
}
for (auto it = awgParams.constBegin(); it != awgParams.constEnd(); ++it) {
if (!it.value().isEmpty()) {
obj[it.key()] = it.value();
}
}
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
}
return obj;
}
@@ -133,9 +139,16 @@ WireGuardClientConfig WireGuardClientConfig::fromJson(const QJsonObject& json)
}
config.persistentKeepAlive = json.value(configKey::persistentKeepAlive).toString();
config.mtu = json.value(configKey::mtu).toString();
for (const QString &key : configKey::awgProtocolKeys()) {
const QString value = json.value(key).toString();
if (!value.isEmpty()) {
config.awgParams.insert(key, value);
}
}
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
return config;
}

View File

@@ -2,6 +2,7 @@
#define WIREGUARDPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QMap>
#include <QString>
#include <QStringList>
#include <optional>
@@ -36,8 +37,10 @@ struct WireGuardClientConfig {
QStringList allowedIps;
QString persistentKeepAlive;
QString mtu;
QMap<QString, QString> awgParams;
bool isObfuscationEnabled = false;
QJsonObject toJson() const;
static WireGuardClientConfig fromJson(const QJsonObject& json);
};

View File

@@ -81,7 +81,6 @@ QJsonObject XrayXhttpConfig::toJson() const
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
obj[configKey::xhttpDisableGrpc] = disableGrpc;
obj[configKey::xhttpDisableSse] = disableSse;
@@ -116,7 +115,6 @@ namespace
c.mode = QString();
c.host = QString();
c.path = QString();
c.headersTemplate = QString();
c.uplinkMethod = QString();
c.disableGrpc = false;
c.disableSse = false;
@@ -155,9 +153,6 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
if (json.contains(configKey::xhttpPath)) {
c.path = json.value(configKey::xhttpPath).toString();
}
if (json.contains(configKey::xhttpHeadersTemplate)) {
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
}
if (json.contains(configKey::xhttpUplinkMethod)) {
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
}

View File

@@ -48,7 +48,6 @@ struct XrayXhttpConfig {
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
QString host = protocols::xray::defaultXhttpHost;
QString path;
QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH
bool disableGrpc = true;
bool disableSse = true;

View File

@@ -64,7 +64,11 @@ QString getProtocolName(DockerContainer defaultContainer, const QMap<DockerConta
const auto it = containers.constFind(defaultContainer);
if (it != containers.cend()) {
if (const AwgProtocolConfig *awg = it->getAwgProtocolConfig()) {
protocolVersion = ProtocolUtils::getProtocolVersionString(awg->toJson());
QString version = awg->clientProtocolVersion();
if (version.isEmpty()) {
version = awg->serverProtocolVersion();
}
protocolVersion = AwgProtocolConfig::protocolVersionString(version);
if (defaultContainer == DockerContainer::Awg && !awg->serverConfig.isThirdPartyConfig) {
containerName = QStringLiteral("AmneziaWG Legacy");
}

View File

@@ -222,6 +222,18 @@ ErrorCode OpenVpnProtocol::start()
}
#endif
#ifdef Q_OS_WIN
// In "all except sites" mode the config uses redirect-gateway !ipv4, so OpenVPN
// never reports net_route_v4_best_gw and m_routeGateway would stay empty
const QString winGateway = NetworkUtilities::getGatewayAndIface().first;
if (!winGateway.isEmpty()) {
m_routeGateway = winGateway;
qDebug() << "Set VPN route gateway" << m_routeGateway;
} else {
qWarning() << "Unable to detect physical default gateway";
}
#endif
uint mgmtPort = selectMgmtPort();
qDebug() << "OpenVpnProtocol::start mgmt port selected:" << mgmtPort;

View File

@@ -209,16 +209,3 @@ QString ProtocolUtils::key_proto_config_path(Proto p)
return protoToString(p) + "_config_path";
}
QString ProtocolUtils::getProtocolVersion(const QJsonObject &protocolConfig)
{
return protocolConfig.value(configKey::protocolVersion).toString();
}
QString ProtocolUtils::getProtocolVersionString(const QJsonObject &protocolConfig)
{
auto version = getProtocolVersion(protocolConfig);
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}

View File

@@ -39,8 +39,6 @@ namespace amnezia
QString key_proto_config_data(Proto p);
QString key_proto_config_path(Proto p);
QString getProtocolVersion(const QJsonObject &protocolConfig);
QString getProtocolVersionString(const QJsonObject &protocolConfig);
}
}

View File

@@ -120,34 +120,60 @@ QVariantMap SecureAppSettingsRepository::vpnSites(RouteMode mode) const
return value("Conf/" + routeModeString(mode)).toMap();
}
QStringList SecureAppSettingsRepository::siteIpList(const QVariant &value)
{
// QVariant::toStringList() handles both a QStringList/QVariantList and a single QString
// (a single string is returned as a one-element list), which covers the legacy format.
QStringList result = value.toStringList();
result.removeAll(QString());
result.removeDuplicates();
return result;
}
void SecureAppSettingsRepository::setVpnSites(RouteMode mode, const QVariantMap &sites)
{
setValue("Conf/" + routeModeString(mode), sites);
}
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QStringList &ips)
{
QVariantMap sites = vpnSites(mode);
if (sites.contains(site) && ip.isEmpty())
const bool siteExisted = sites.contains(site);
if (siteExisted && ips.isEmpty())
return false;
sites.insert(site, ip);
QStringList mergedIps = siteIpList(sites.value(site));
bool changed = !siteExisted;
for (const QString &ip : ips) {
if (!ip.isEmpty() && !mergedIps.contains(ip)) {
mergedIps.append(ip);
changed = true;
}
}
if (!changed)
return false;
sites.insert(site, mergedIps);
setVpnSites(mode, sites);
emit sitesChanged(mode);
return true;
}
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QString> &sites)
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites)
{
QVariantMap allSites = vpnSites(mode);
for (auto i = sites.constBegin(); i != sites.constEnd(); ++i) {
const QString &site = i.key();
const QString &ip = i.value();
if (allSites.contains(site) && allSites.value(site) == ip)
continue;
QStringList mergedIps = siteIpList(allSites.value(site));
for (const QString &ip : i.value()) {
if (!ip.isEmpty() && !mergedIps.contains(ip))
mergedIps.append(ip);
}
allSites.insert(site, ip);
allSites.insert(site, mergedIps);
}
setVpnSites(mode, allSites);

View File

@@ -38,11 +38,15 @@ public:
RouteMode routeMode() const;
void setRouteMode(RouteMode mode);
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites);
bool addVpnSite(RouteMode mode, const QString &site, const QStringList &ips = {});
void addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites);
void removeVpnSite(RouteMode mode, const QString &site);
void removeAllVpnSites(RouteMode mode);
QVariantMap vpnSites(RouteMode mode) const;
// Normalizes a stored vpn site value into a list of IPs.
// Supports both the legacy format (a single IP string) and the current one (a list of IPs).
static QStringList siteIpList(const QVariant &value);
bool isSitesSplitTunnelingEnabled() const;
void setSitesSplitTunnelingEnabled(bool enabled);

View File

@@ -2,6 +2,7 @@
#define CONFIGKEYS_H
#include <QLatin1String>
#include <QStringList>
namespace amnezia
{
@@ -70,6 +71,8 @@ namespace amnezia
constexpr QLatin1String lastConfig("last_config");
constexpr QLatin1String protocolVersion("protocol_version");
constexpr QLatin1String isThirdPartyConfig("isThirdPartyConfig");
constexpr QLatin1String isObfuscationEnabled("isObfuscationEnabled");
@@ -90,7 +93,40 @@ namespace amnezia
constexpr QLatin1String specialJunk4("I4");
constexpr QLatin1String specialJunk5("I5");
constexpr QLatin1String protocolVersion("protocol_version");
constexpr QLatin1String headerProtectionKey("HeaderProtectionKey");
constexpr QLatin1String contentPaddingAddition("ContentPaddingAddition");
constexpr QLatin1String rekeyAfterTime("RekeyAfterTime");
constexpr QLatin1String rekeyTimeout("RekeyTimeout");
constexpr QLatin1String rejectAfterTime("RejectAfterTime");
constexpr QLatin1String keepaliveTimeout("KeepaliveTimeout");
constexpr QLatin1String maxHandshakeAttempts("MaxHandshakeAttempts");
inline QStringList awgProtocolKeys()
{
return { junkPacketCount,
junkPacketMinSize,
junkPacketMaxSize,
initPacketJunkSize,
responsePacketJunkSize,
cookieReplyPacketJunkSize,
transportPacketJunkSize,
initPacketMagicHeader,
responsePacketMagicHeader,
underloadPacketMagicHeader,
transportPacketMagicHeader,
specialJunk1,
specialJunk2,
specialJunk3,
specialJunk4,
specialJunk5,
headerProtectionKey,
contentPaddingAddition,
rekeyAfterTime,
rekeyTimeout,
rejectAfterTime,
keepaliveTimeout,
maxHandshakeAttempts };
}
constexpr QLatin1String openvpn("openvpn");
constexpr QLatin1String wireguard("wireguard");
@@ -150,7 +186,6 @@ namespace amnezia
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
constexpr QLatin1String xhttpHost("xhttp_host");
constexpr QLatin1String xhttpPath("xhttp_path");
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool

View File

@@ -64,14 +64,13 @@ namespace amnezia
constexpr char defaultFlow[] = "xtls-rprx-vision";
constexpr char defaultTransport[] = "raw";
constexpr char defaultFingerprint[] = "chrome";
constexpr char defaultSni[] = "cdn.example.com";
constexpr char defaultAlpn[] = "HTTP/2";
constexpr char defaultSni[] = "www.googletagmanager.com";
constexpr char defaultAlpn[] = "h2";
constexpr char defaultXhttpMode[] = "Auto";
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
constexpr char defaultXhttpUplinkMethod[] = "POST";
constexpr char defaultXhttpSessionPlacement[] = "Path";
constexpr char defaultXhttpSessionKey[] = "Path";
constexpr char defaultXhttpSessionKey[] = "";
constexpr char defaultXhttpSeqPlacement[] = "Path";
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
@@ -86,6 +85,10 @@ namespace amnezia
constexpr char defaultXPaddingPlacement[] = "Cookie";
constexpr char defaultXPaddingMethod[] = "Repeat-x";
constexpr char defaultXPaddingKey[] = "x_padding";
constexpr char defaultXPaddingHeader[] = "X-Padding";
constexpr char defaultXPaddingBytesMin[] = "1";
constexpr char defaultXPaddingBytesMax[] = "256";
constexpr char defaultMkcpTti[] = "50";
constexpr char defaultMkcpUplinkCapacity[] = "5";
@@ -147,6 +150,7 @@ namespace amnezia
constexpr char defaultSubnetCidr[] = "24";
constexpr char defaultPort[] = "51820";
constexpr char defaultPersistentKeepAlive[] = "25";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
constexpr char defaultMtu[] = "1280";
@@ -182,15 +186,16 @@ namespace amnezia
constexpr char defaultJunkPacketCount[] = "3";
constexpr char defaultJunkPacketMinSize[] = "10";
constexpr char defaultJunkPacketMaxSize[] = "30";
constexpr char defaultInitPacketJunkSize[] = "15";
constexpr char defaultResponsePacketJunkSize[] = "18";
constexpr char defaultCookieReplyPacketJunkSize[] = "20";
constexpr char defaultTransportPacketJunkSize[] = "23";
constexpr int junkPacketSizeMin = 12;
constexpr int initPacketJunkSizeMax = 150;
constexpr int responsePacketJunkSizeMax = 150;
constexpr int cookieReplyPacketJunkSizeMax = 64;
constexpr int defaultTransportPacketJunkSize = 12;
constexpr char defaultInitPacketMagicHeader[] = "1020325451";
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
constexpr char defaultInitPacketMagicHeader[] = "1";
constexpr char defaultResponsePacketMagicHeader[] = "2";
constexpr char defaultUnderloadPacketMagicHeader[] = "3";
constexpr char defaultTransportPacketMagicHeader[] = "4";
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
constexpr char defaultSpecialJunk2[] = "";
constexpr char defaultSpecialJunk3[] = "";
@@ -199,6 +204,17 @@ namespace amnezia
constexpr char awgV1_5[] = "1.5";
constexpr char awgV2[] = "2";
constexpr char awgV3[] = "3";
constexpr char defaultContentPaddingAddition[] = "10-100";
constexpr char defaultRekeyAfterTime[] = "100-120";
constexpr char defaultRekeyTimeout[] = "3-7";
constexpr char defaultRejectAfterTime[] = "150-180";
constexpr char defaultKeepaliveTimeout[] = "5-15";
constexpr char defaultMaxHandshakeAttempts[] = "15-20";
constexpr char defaultPersistentKeepAlive[] = "25-35";
}
namespace socks5Proxy
@@ -234,7 +250,8 @@ namespace amnezia
constexpr char defaultPort[] = "443";
constexpr char defaultWorkers[] = "2";
constexpr int maxWorkers = 32;
// mtproto-proxy loses connectivity with -M >= 20; keep the cap at the highest known-good value.
constexpr int maxWorkers = 19;
constexpr int botTagHexLength = 32;
constexpr char defaultTlsDomain[] = "googletagmanager.com";
}
@@ -253,7 +270,6 @@ namespace amnezia
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
constexpr char userNameKey[] = "telemt_user_name";
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
constexpr char workersKey[] = "telemt_workers";
constexpr char workersModeKey[] = "telemt_workers_mode";

View File

@@ -286,7 +286,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
#endif
#ifdef Q_OS_LINUX
constexpr int BUFFER_SIZE = 100;
constexpr int BUFFER_SIZE = 8192;
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
int sock = -1, msgseq = 0;
struct nlmsghdr *nlh, *nlmsg;
@@ -294,7 +294,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
// This struct contain route attributes (route type)
struct rtattr *route_attribute;
char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE];
char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE];
char msgbuf[100], buffer[BUFFER_SIZE];
char *ptr = buffer;
struct timeval tv;
@@ -339,8 +339,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
nlh = (struct nlmsghdr *) ptr;
/* Check if the header is valid */
if((NLMSG_OK(nlmsg, received_bytes) == 0) ||
(nlmsg->nlmsg_type == NLMSG_ERROR))
if((NLMSG_OK(nlh, received_bytes) == 0) ||
(nlh->nlmsg_type == NLMSG_ERROR))
{
perror("Error in received packet");
return {};
@@ -355,13 +355,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
}
/* Break if its not a multi part message */
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0)
if ((nlh->nlmsg_flags & NLM_F_MULTI) == 0)
break;
}
while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid()));
while ((nlh->nlmsg_seq != msgseq) || (nlh->nlmsg_pid != getpid()));
/* parse response */
for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes))
int remaining = msg_len + received_bytes;
nlh = (struct nlmsghdr *) buffer;
for ( ; NLMSG_OK(nlh, remaining); nlh = NLMSG_NEXT(nlh, remaining))
{
/* Get the route data */
route_entry = (struct rtmsg *) NLMSG_DATA(nlh);
@@ -370,6 +372,10 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
if (route_entry->rtm_table != RT_TABLE_MAIN)
continue;
/* Reset per-route to avoid cross-route state pollution */
memset(gateway_address, 0, sizeof(gateway_address));
memset(interface, 0, sizeof(interface));
route_attribute = (struct rtattr *) RTM_RTA(route_entry);
route_attribute_len = RTM_PAYLOAD(nlh);
@@ -395,6 +401,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
break;
}
}
if (!(*gateway_address) || !(*interface))
qDebug() << "getGatewayAndIface: no gateway found";
close(sock);
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
#endif

View File

@@ -24,6 +24,13 @@ QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
return chunks;
}
QString qrCodeUtils::generatePlainQrCodeImage(const QByteArray &data)
{
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data, qrcodegen::QrCode::Ecc::LOW);
QString svg = QString::fromStdString(toSvgString(qr, 1));
return svgToBase64(svg);
}
QString qrCodeUtils::svgToBase64(const QString &image)
{
return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data());

View File

@@ -10,6 +10,7 @@ namespace qrCodeUtils
constexpr const qint16 qrMagicCode = 1984;
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
QString generatePlainQrCodeImage(const QByteArray &data);
qrcodegen::QrCode generateQrCode(const QByteArray &data);
QString svgToBase64(const QString &image);
};

View File

@@ -254,8 +254,20 @@ amnezia::ScriptVars amnezia::genAwgVars(const ContainerConfig &containerConfig)
vars.append({ { "$SPECIAL_JUNK_3", config.specialJunk3 } });
vars.append({ { "$SPECIAL_JUNK_4", config.specialJunk4 } });
vars.append({ { "$SPECIAL_JUNK_5", config.specialJunk5 } });
const bool isAwg3 = config.protocolVersion == protocols::awg::awgV3;
vars.append({ { "$PERSISTENT_KEEPALIVE", isAwg3 ? QString(protocols::awg::defaultPersistentKeepAlive)
: QString(protocols::wireguard::defaultPersistentKeepAlive) } });
vars.append({ { "$HEADER_PROTECTION_KEY", config.headerProtectionKey } });
vars.append({ { "$CONTENT_PADDING_ADDITION", config.contentPaddingAddition } });
vars.append({ { "$REKEY_AFTER_TIME", config.rekeyAfterTime } });
vars.append({ { "$REKEY_TIMEOUT", config.rekeyTimeout } });
vars.append({ { "$REJECT_AFTER_TIME", config.rejectAfterTime } });
vars.append({ { "$KEEPALIVE_TIMEOUT", config.keepaliveTimeout } });
vars.append({ { "$MAX_HANDSHAKE_ATTEMPTS", config.maxHandshakeAttempts } });
}
return vars;
}
@@ -329,6 +341,7 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
: QStringLiteral("2");
}
vars.append({{"$MTPROXY_WORKERS_MODE", workersMode}});
vars.append({{"$MTPROXY_WORKERS", workers}});
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
@@ -375,6 +388,12 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
}
}
vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } });
QString middleProxyNatIp;
if (c.natEnabled && !c.natExternalIp.isEmpty()) {
middleProxyNatIp = c.natExternalIp;
}
vars.append({ { "$TELEMT_MIDDLE_PROXY_NAT_IP", middleProxyNatIp } });
}
return vars;

View File

@@ -176,7 +176,8 @@ QByteArray SshSession::getTextFileFromContainer(DockerContainer container, const
errorCode = ErrorCode::NoError;
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2'\"").arg(ContainerUtils::containerToString(container), path);
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2' 2>/dev/null || od -An -v -tx1 '%2'\"")
.arg(ContainerUtils::containerToString(container), path);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {

View File

@@ -265,6 +265,8 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
#endif
}
config.m_persistentKeepalive = obj.value("persistentKeepalive").toString();
config.m_deviceIpv4Address = obj.value("deviceIpv4Address").toString();
config.m_deviceIpv6Address = obj.value("deviceIpv6Address").toString();
if (config.m_deviceIpv4Address.isNull() &&
@@ -441,6 +443,28 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
config.m_specialJunk["I5"] = i5.toString();
}
if (const auto headerProtectionKey = obj.value("HeaderProtectionKey"); !headerProtectionKey.isUndefined()) {
config.m_headerProtectionKey = headerProtectionKey.toString();
}
if (const auto contentPaddingAddition = obj.value("ContentPaddingAddition"); !contentPaddingAddition.isUndefined()) {
config.m_contentPaddingAddition = contentPaddingAddition.toString();
}
if (const auto rekeyAfterTime = obj.value("RekeyAfterTime"); !rekeyAfterTime.isUndefined()) {
config.m_rekeyAfterTime = rekeyAfterTime.toString();
}
if (const auto rekeyTimeout = obj.value("RekeyTimeout"); !rekeyTimeout.isUndefined()) {
config.m_rekeyTimeout = rekeyTimeout.toString();
}
if (const auto rejectAfterTime = obj.value("RejectAfterTime"); !rejectAfterTime.isUndefined()) {
config.m_rejectAfterTime = rejectAfterTime.toString();
}
if (const auto keepaliveTimeout = obj.value("KeepaliveTimeout"); !keepaliveTimeout.isUndefined()) {
config.m_keepaliveTimeout = keepaliveTimeout.toString();
}
if (const auto maxHandshakeAttempts = obj.value("MaxHandshakeAttempts"); !maxHandshakeAttempts.isUndefined()) {
config.m_maxHandshakeAttempts = maxHandshakeAttempts.toString();
}
return true;
}

View File

@@ -24,6 +24,9 @@ QJsonObject InterfaceConfig::toJson() const {
json.insert("serverIpv6AddrIn", QJsonValue(m_serverIpv6AddrIn));
json.insert("serverPort", QJsonValue((double)m_serverPort));
json.insert("deviceMTU", QJsonValue(m_deviceMTU));
if (!m_persistentKeepalive.isEmpty()) {
json.insert("persistentKeepalive", QJsonValue(m_persistentKeepalive));
}
if ((m_hopType == InterfaceConfig::MultiHopExit) ||
(m_hopType == InterfaceConfig::SingleHop)) {
json.insert("serverIpv4Gateway", QJsonValue(m_serverIpv4Gateway));
@@ -115,42 +118,66 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
out << "DNS = " << dnsServers.join(", ") << "\n";
}
if (!m_junkPacketCount.isNull()) {
if (!m_junkPacketCount.isEmpty()) {
out << "Jc = " << m_junkPacketCount << "\n";
}
if (!m_junkPacketMinSize.isNull()) {
if (!m_junkPacketMinSize.isEmpty()) {
out << "JMin = " << m_junkPacketMinSize << "\n";
}
if (!m_junkPacketMaxSize.isNull()) {
if (!m_junkPacketMaxSize.isEmpty()) {
out << "JMax = " << m_junkPacketMaxSize << "\n";
}
if (!m_initPacketJunkSize.isNull()) {
if (!m_initPacketJunkSize.isEmpty()) {
out << "S1 = " << m_initPacketJunkSize << "\n";
}
if (!m_responsePacketJunkSize.isNull()) {
if (!m_responsePacketJunkSize.isEmpty()) {
out << "S2 = " << m_responsePacketJunkSize << "\n";
}
if (!m_cookieReplyPacketJunkSize.isNull()) {
if (!m_cookieReplyPacketJunkSize.isEmpty()) {
out << "S3 = " << m_cookieReplyPacketJunkSize << "\n";
}
if (!m_transportPacketJunkSize.isNull()) {
if (!m_transportPacketJunkSize.isEmpty()) {
out << "S4 = " << m_transportPacketJunkSize << "\n";
}
if (!m_initPacketMagicHeader.isNull()) {
if (!m_initPacketMagicHeader.isEmpty()) {
out << "H1 = " << m_initPacketMagicHeader << "\n";
}
if (!m_responsePacketMagicHeader.isNull()) {
if (!m_responsePacketMagicHeader.isEmpty()) {
out << "H2 = " << m_responsePacketMagicHeader << "\n";
}
if (!m_underloadPacketMagicHeader.isNull()) {
if (!m_underloadPacketMagicHeader.isEmpty()) {
out << "H3 = " << m_underloadPacketMagicHeader << "\n";
}
if (!m_transportPacketMagicHeader.isNull()) {
if (!m_transportPacketMagicHeader.isEmpty()) {
out << "H4 = " << m_transportPacketMagicHeader << "\n";
}
for (const QString& key : m_specialJunk.keys()) {
out << key << " = " << m_specialJunk[key] << "\n";
if (!m_specialJunk[key].isEmpty()) {
out << key << " = " << m_specialJunk[key] << "\n";
}
}
if (!m_headerProtectionKey.isEmpty()) {
out << "HeaderProtectionKey = " << m_headerProtectionKey << "\n";
}
if (!m_contentPaddingAddition.isEmpty()) {
out << "ContentPaddingAddition = " << m_contentPaddingAddition << "\n";
}
if (!m_rekeyAfterTime.isEmpty()) {
out << "RekeyAfterTime = " << m_rekeyAfterTime << "\n";
}
if (!m_rekeyTimeout.isEmpty()) {
out << "RekeyTimeout = " << m_rekeyTimeout << "\n";
}
if (!m_rejectAfterTime.isEmpty()) {
out << "RejectAfterTime = " << m_rejectAfterTime << "\n";
}
if (!m_keepaliveTimeout.isEmpty()) {
out << "KeepaliveTimeout = " << m_keepaliveTimeout << "\n";
}
if (!m_maxHandshakeAttempts.isEmpty()) {
out << "MaxHandshakeAttempts = " << m_maxHandshakeAttempts << "\n";
}
// If any extra config was provided, append it now.
@@ -173,6 +200,9 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
ranges.append(ip.toString());
}
out << "AllowedIPs = " << ranges.join(", ") << "\n";
if (!m_persistentKeepalive.isEmpty()) {
out << "PersistentKeepalive = " << m_persistentKeepalive << "\n";
}
return content;
}

View File

@@ -36,6 +36,7 @@ class InterfaceConfig {
QString m_secondaryDnsServer;
int m_serverPort = 0;
int m_deviceMTU = 1420;
QString m_persistentKeepalive;
QList<IPAddress> m_allowedIPAddressRanges;
QStringList m_excludedAddresses;
QStringList m_vpnDisabledApps;
@@ -58,6 +59,14 @@ class InterfaceConfig {
QString m_transportPacketMagicHeader;
QMap<QString, QString> m_specialJunk;
QString m_headerProtectionKey;
QString m_contentPaddingAddition;
QString m_rekeyAfterTime;
QString m_rekeyTimeout;
QString m_rejectAfterTime;
QString m_keepaliveTimeout;
QString m_maxHandshakeAttempts;
QJsonObject toJson() const;
QString toWgConf(
const QMap<QString, QString>& extra = QMap<QString, QString>()) const;

View File

@@ -16,8 +16,6 @@
constexpr const char* WG_INTERFACE = "amn0";
constexpr uint16_t WG_KEEPALIVE_PERIOD = 60;
class WireguardUtils : public QObject {
Q_OBJECT

View File

@@ -18,11 +18,10 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${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"
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"

View File

@@ -44,8 +44,20 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIcons</key>
<dict/>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon</string>
</array>
<key>CFBundleIconName</key>
<string>AppIcon</string>
</dict>
</dict>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>

View File

@@ -31,8 +31,6 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(DEVELOPMENT_TEAM).*</string>

View File

@@ -7,21 +7,14 @@ add_executable(AmneziaVPNNetworkExtension)
message("executable_path is: @executable_path/../../Frameworks")
set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
XCODE_PRODUCT_TYPE com.apple.product-type.app-extension
# MACOSX_BUNDLE YES
BUNDLE_EXTENSION appex
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPNNetworkExtension"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPNNetworkExtension"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
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 "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks"

View File

@@ -24,7 +24,7 @@
<false/>
<key>LSMinimumSystemVersion</key>
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>CFBundleDisplayName</key>
<string>AmneziaVPNNetworkExtension</string>

View File

@@ -1,12 +1,19 @@
#include <QDebug>
#include <QTimer>
#include <libssh/libssh.h>
#include <openssl/ssl.h>
#include "amneziaApplication.h"
#include "core/utils/osSignalHandler.h"
#include "core/utils/migrations.h"
#include "version.h"
// use openssl symbols to prevent linker throwing-off the OpenSSL dependency
void anchorOpenSSL() {
SSL_CTX_free(SSL_CTX_new(TLS_method()));
}
#ifdef Q_OS_WIN
#include "Windows.h"
#endif
@@ -46,6 +53,8 @@ int main(int argc, char *argv[])
AmneziaApplication app(argc, argv);
OsSignalHandler::setup();
anchorOpenSSL();
ssh_init();
QObject::connect(&app, &QCoreApplication::aboutToQuit, []() {
ssh_finalize();

View File

@@ -18,6 +18,7 @@
#include <QLocalSocket>
#include <QObject>
#include <QStandardPaths>
#include <QStringList>
#include <QTimer>
#include "leakdetector.h"
@@ -159,6 +160,11 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert("serverIpv4Gateway", wgConfig.value(amnezia::configKey::hostName));
// json.insert("serverIpv6Gateway", QJsonValue(hop.m_server.ipv6Gateway()));
if (wgConfig.contains(amnezia::configKey::persistentKeepAlive)) {
json.insert("persistentKeepalive",
wgConfig.value(amnezia::configKey::persistentKeepAlive).toString());
}
json.insert("primaryDnsServer", rawConfig.value(amnezia::configKey::dns1));
// We don't use secondary DNS if primary DNS is AmneziaDNS
@@ -246,50 +252,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert(amnezia::configKey::killSwitchOption, rawConfig.value(amnezia::configKey::killSwitchOption));
if (protocolName == amnezia::configKey::awg) {
json.insert(amnezia::configKey::junkPacketCount, wgConfig.value(amnezia::configKey::junkPacketCount));
json.insert(amnezia::configKey::junkPacketMinSize, wgConfig.value(amnezia::configKey::junkPacketMinSize));
json.insert(amnezia::configKey::junkPacketMaxSize, wgConfig.value(amnezia::configKey::junkPacketMaxSize));
json.insert(amnezia::configKey::initPacketJunkSize, wgConfig.value(amnezia::configKey::initPacketJunkSize));
json.insert(amnezia::configKey::responsePacketJunkSize, wgConfig.value(amnezia::configKey::responsePacketJunkSize));
json.insert(amnezia::configKey::cookieReplyPacketJunkSize, wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize));
json.insert(amnezia::configKey::transportPacketJunkSize, wgConfig.value(amnezia::configKey::transportPacketJunkSize));
json.insert(amnezia::configKey::initPacketMagicHeader, wgConfig.value(amnezia::configKey::initPacketMagicHeader));
json.insert(amnezia::configKey::responsePacketMagicHeader, wgConfig.value(amnezia::configKey::responsePacketMagicHeader));
json.insert(amnezia::configKey::underloadPacketMagicHeader, wgConfig.value(amnezia::configKey::underloadPacketMagicHeader));
json.insert(amnezia::configKey::transportPacketMagicHeader, wgConfig.value(amnezia::configKey::transportPacketMagicHeader));
json.insert(amnezia::configKey::specialJunk1, wgConfig.value(amnezia::configKey::specialJunk1));
json.insert(amnezia::configKey::specialJunk2, wgConfig.value(amnezia::configKey::specialJunk2));
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
} else if (!wgConfig.value(amnezia::configKey::junkPacketCount).isUndefined()
&& !wgConfig.value(amnezia::configKey::junkPacketMinSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::junkPacketMaxSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::initPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::responsePacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::transportPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::configKey::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::configKey::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::configKey::transportPacketMagicHeader).isUndefined()) {
json.insert(amnezia::configKey::junkPacketCount, wgConfig.value(amnezia::configKey::junkPacketCount));
json.insert(amnezia::configKey::junkPacketMinSize, wgConfig.value(amnezia::configKey::junkPacketMinSize));
json.insert(amnezia::configKey::junkPacketMaxSize, wgConfig.value(amnezia::configKey::junkPacketMaxSize));
json.insert(amnezia::configKey::initPacketJunkSize, wgConfig.value(amnezia::configKey::initPacketJunkSize));
json.insert(amnezia::configKey::responsePacketJunkSize, wgConfig.value(amnezia::configKey::responsePacketJunkSize));
json.insert(amnezia::configKey::cookieReplyPacketJunkSize, wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize));
json.insert(amnezia::configKey::transportPacketJunkSize, wgConfig.value(amnezia::configKey::transportPacketJunkSize));
json.insert(amnezia::configKey::initPacketMagicHeader, wgConfig.value(amnezia::configKey::initPacketMagicHeader));
json.insert(amnezia::configKey::responsePacketMagicHeader, wgConfig.value(amnezia::configKey::responsePacketMagicHeader));
json.insert(amnezia::configKey::underloadPacketMagicHeader, wgConfig.value(amnezia::configKey::underloadPacketMagicHeader));
json.insert(amnezia::configKey::transportPacketMagicHeader, wgConfig.value(amnezia::configKey::transportPacketMagicHeader));
json.insert(amnezia::configKey::specialJunk1, wgConfig.value(amnezia::configKey::specialJunk1));
json.insert(amnezia::configKey::specialJunk2, wgConfig.value(amnezia::configKey::specialJunk2));
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
const QStringList awgProtocolKeys = amnezia::configKey::awgProtocolKeys();
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = wgConfig.value(key);
if (value.isString() && !value.toString().isEmpty()) {
json.insert(key, value);
}
}
write(json);

View File

@@ -154,6 +154,28 @@ void AndroidController::resetLastServer(int serverIndex)
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::showUpdateCover()
{
callActivityMethod("showUpdateCover", "()V");
}
void AndroidController::hideUpdateCover()
{
callActivityMethod("hideUpdateCover", "()V");
}
void AndroidController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl)
{
callActivityMethod("showUpdatePrompt",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>(),
QJniObject::fromString(updateTitle).object<jstring>(),
QJniObject::fromString(skipTitle).object<jstring>(),
QJniObject::fromString(storeUrl).object<jstring>());
}
void AndroidController::saveFile(const QString &fileName, const QString &data)
{
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",

View File

@@ -56,6 +56,11 @@ public:
bool requestAuthentication();
void sendTouch(float x, float y);
void showUpdateCover();
void hideUpdateCover();
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl);
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);

View File

@@ -7,10 +7,6 @@ struct OpenVPNConfig: Decodable {
let config: String
let splitTunnelType: Int
let splitTunnelSites: [String]
var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
}
}
extension PacketTunnelProvider {
@@ -30,11 +26,6 @@ extension PacketTunnelProvider {
do {
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
let ovpnPreview = String(openVPNConfig.config.prefix(512))
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
splitTunnelType = openVPNConfig.splitTunnelType
splitTunnelSites = openVPNConfig.splitTunnelSites
@@ -105,12 +96,6 @@ extension PacketTunnelProvider {
let hasTlsAuthClose = configString.contains("</tls-auth>")
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
let lines = configString.split(separator: "\n")
let head = lines.prefix(10).joined(separator: "\n")
let tail = lines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigHead", message: head)
ovpnLog(.debug, title: "ConfigTail", message: tail)
if hasTlsAuthOpen && hasTlsAuthClose {
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
}
@@ -155,8 +140,6 @@ extension PacketTunnelProvider {
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") }

View File

@@ -16,7 +16,6 @@ extension PacketTunnelProvider {
do {
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
let wgConfigStr = wgConfig.str
wg_log(.info, title: "config: ", message: wgConfig.redux)
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)

View File

@@ -6,6 +6,8 @@ struct WGConfig: Decodable {
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
let initPacketJunkSize, responsePacketJunkSize, cookieReplyPacketJunkSize, transportPacketJunkSize: String?
let specialJunk1, specialJunk2, specialJunk3, specialJunk4, specialJunk5: String?
let headerProtectionKey: String?
let contentPaddingAddition, rekeyAfterTime, rekeyTimeout, rejectAfterTime, keepaliveTimeout, maxHandshakeAttempts: String?
let dns1: String
let dns2: String
let mtu: String
@@ -16,7 +18,7 @@ struct WGConfig: Decodable {
let serverPublicKey: String
let presharedKey: String?
var allowedIPs: [String]
var persistentKeepAlive: String
var persistentKeepAlive: String?
let splitTunnelType: Int
let splitTunnelSites: [String]
@@ -26,6 +28,11 @@ struct WGConfig: Decodable {
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2", cookieReplyPacketJunkSize = "S3", transportPacketJunkSize = "S4"
case specialJunk1 = "I1", specialJunk2 = "I2", specialJunk3 = "I3", specialJunk4 = "I4", specialJunk5 = "I5"
case headerProtectionKey = "HeaderProtectionKey"
case contentPaddingAddition = "ContentPaddingAddition"
case rekeyAfterTime = "RekeyAfterTime", rekeyTimeout = "RekeyTimeout"
case rejectAfterTime = "RejectAfterTime", keepaliveTimeout = "KeepaliveTimeout"
case maxHandshakeAttempts = "MaxHandshakeAttempts"
case dns1
case dns2
case mtu
@@ -100,6 +107,28 @@ struct WGConfig: Decodable {
settingsLines.append("I5 = \(i5)")
}
if let headerProtectionKey = trimmed(headerProtectionKey) {
settingsLines.append("HeaderProtectionKey = \(headerProtectionKey)")
}
if let contentPaddingAddition = trimmed(contentPaddingAddition) {
settingsLines.append("ContentPaddingAddition = \(contentPaddingAddition)")
}
if let rekeyAfterTime = trimmed(rekeyAfterTime) {
settingsLines.append("RekeyAfterTime = \(rekeyAfterTime)")
}
if let rekeyTimeout = trimmed(rekeyTimeout) {
settingsLines.append("RekeyTimeout = \(rekeyTimeout)")
}
if let rejectAfterTime = trimmed(rejectAfterTime) {
settingsLines.append("RejectAfterTime = \(rejectAfterTime)")
}
if let keepaliveTimeout = trimmed(keepaliveTimeout) {
settingsLines.append("KeepaliveTimeout = \(keepaliveTimeout)")
}
if let maxHandshakeAttempts = trimmed(maxHandshakeAttempts) {
settingsLines.append("MaxHandshakeAttempts = \(maxHandshakeAttempts)")
}
return settingsLines.joined(separator: "\n")
}
@@ -116,27 +145,7 @@ struct WGConfig: Decodable {
\(presharedKey == nil ? "" : "PresharedKey = \(presharedKey!)")
AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(hostName):\(port)
PersistentKeepalive = \(persistentKeepAlive)
"""
}
var redux: String {
"""
[Interface]
Address = \(clientIP)
DNS = \(dns1), \(dns2)
MTU = \(mtu)
PrivateKey = ***
\(settings)
[Peer]
PublicKey = ***
PresharedKey = ***
AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(hostName):\(port)
PersistentKeepalive = \(persistentKeepAlive)
SplitTunnelType = \(splitTunnelType)
SplitTunnelSites = \(splitTunnelSites.joined(separator: ", "))
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
"""
}
}

View File

@@ -80,6 +80,11 @@ public:
void requestInetAccess();
bool isTestFlight();
void showUpdateCover();
void hideUpdateCover();
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl);
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);

View File

@@ -552,6 +552,18 @@ bool IosController::setupOpenVPN()
return startOpenVPN(openVPNConfigStr);
}
static void insertNonEmptyAwgParams(QJsonObject &wgConfig, const QJsonObject &config)
{
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = config.value(key);
if (value.isString() && !value.toString().isEmpty()) {
wgConfig.insert(key, value);
}
}
}
bool IosController::setupWireGuard()
{
QJsonObject config = m_rawConfig[ProtocolUtils::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
@@ -591,25 +603,9 @@ bool IosController::setupWireGuard()
if (config.contains(configKey::persistentKeepAlive)) {
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
} else {
wgConfig.insert(configKey::persistentKeepAlive, "25");
}
if (config.contains(configKey::isObfuscationEnabled) && config.value(configKey::isObfuscationEnabled).toBool()) {
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
wgConfig.insert(configKey::underloadPacketMagicHeader, config[configKey::underloadPacketMagicHeader]);
wgConfig.insert(configKey::transportPacketMagicHeader, config[configKey::transportPacketMagicHeader]);
wgConfig.insert(configKey::initPacketJunkSize, config[configKey::initPacketJunkSize]);
wgConfig.insert(configKey::responsePacketJunkSize, config[configKey::responsePacketJunkSize]);
wgConfig.insert(configKey::cookieReplyPacketJunkSize, config[configKey::cookieReplyPacketJunkSize]);
wgConfig.insert(configKey::transportPacketJunkSize, config[configKey::transportPacketJunkSize]);
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
}
insertNonEmptyAwgParams(wgConfig, config);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
@@ -697,29 +693,9 @@ bool IosController::setupAwg()
if (config.contains(configKey::persistentKeepAlive)) {
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
} else {
wgConfig.insert(configKey::persistentKeepAlive, "25");
}
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
wgConfig.insert(configKey::underloadPacketMagicHeader, config[configKey::underloadPacketMagicHeader]);
wgConfig.insert(configKey::transportPacketMagicHeader, config[configKey::transportPacketMagicHeader]);
wgConfig.insert(configKey::initPacketJunkSize, config[configKey::initPacketJunkSize]);
wgConfig.insert(configKey::responsePacketJunkSize, config[configKey::responsePacketJunkSize]);
wgConfig.insert(configKey::cookieReplyPacketJunkSize, config[configKey::cookieReplyPacketJunkSize]);
wgConfig.insert(configKey::transportPacketJunkSize, config[configKey::transportPacketJunkSize]);
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
wgConfig.insert(configKey::specialJunk1, config[configKey::specialJunk1]);
wgConfig.insert(configKey::specialJunk2, config[configKey::specialJunk2]);
wgConfig.insert(configKey::specialJunk3, config[configKey::specialJunk3]);
wgConfig.insert(configKey::specialJunk4, config[configKey::specialJunk4]);
wgConfig.insert(configKey::specialJunk5, config[configKey::specialJunk5]);
insertNonEmptyAwgParams(wgConfig, config);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
@@ -1200,3 +1176,138 @@ bool IosController::isTestFlight() {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
}
#if !MACOS_NE
static UIWindow *s_updateCoverWindow = nil;
static UIWindowScene *activeWindowScene() {
UIWindowScene *fallback = nil;
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
fallback = (UIWindowScene *)scene;
if (scene.activationState == UISceneActivationStateForegroundActive) {
return (UIWindowScene *)scene;
}
}
return fallback;
}
#endif
void IosController::showUpdateCover() {
#if !MACOS_NE
void (^build)(void) = ^{
if (s_updateCoverWindow) {
return;
}
UIWindowScene *scene = activeWindowScene();
if (!scene) {
return;
}
UIWindow *win = [[UIWindow alloc] initWithWindowScene:scene];
win.windowLevel = UIWindowLevelAlert + 1;
UIViewController *vc = [[[UIViewController alloc] init] autorelease];
vc.view.backgroundColor = [UIColor colorWithRed:0.055 green:0.055 blue:0.063 alpha:1.0];
win.rootViewController = vc;
[win makeKeyAndVisible];
s_updateCoverWindow = win;
};
if ([NSThread isMainThread]) {
build();
} else {
dispatch_sync(dispatch_get_main_queue(), build);
}
#endif
}
void IosController::hideUpdateCover() {
#if !MACOS_NE
dispatch_async(dispatch_get_main_queue(), ^{
if (!s_updateCoverWindow) {
return;
}
s_updateCoverWindow.hidden = YES;
[s_updateCoverWindow release];
s_updateCoverWindow = nil;
});
#endif
}
void IosController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
const QString &skipTitle, const QString &storeUrl) {
#if !MACOS_NE
NSString *nsTitle = title.toNSString();
NSString *nsMessage = message.toNSString();
NSString *nsUpdate = updateTitle.toNSString();
NSString *nsSkip = skipTitle.toNSString();
NSString *nsUrl = storeUrl.toNSString();
dispatch_async(dispatch_get_main_queue(), ^{
if (!s_updateCoverWindow) {
return;
}
UIViewController *vc = s_updateCoverWindow.rootViewController;
void (^dismissCover)(void) = ^{
s_updateCoverWindow.hidden = YES;
[s_updateCoverWindow release];
s_updateCoverWindow = nil;
};
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
titleLabel.text = nsTitle;
titleLabel.font = [UIFont boldSystemFontOfSize:22];
titleLabel.textColor = [UIColor whiteColor];
titleLabel.textAlignment = NSTextAlignmentCenter;
titleLabel.numberOfLines = 0;
UILabel *messageLabel = [[[UILabel alloc] init] autorelease];
messageLabel.text = nsMessage;
messageLabel.font = [UIFont systemFontOfSize:16];
messageLabel.textColor = [UIColor colorWithWhite:0.78 alpha:1.0];
messageLabel.textAlignment = NSTextAlignmentCenter;
messageLabel.numberOfLines = 0;
UIButton *updateButton = [UIButton buttonWithType:UIButtonTypeSystem];
[updateButton setTitle:nsUpdate forState:UIControlStateNormal];
[updateButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
updateButton.backgroundColor = [UIColor colorWithRed:1.0 green:0.6 blue:0.0 alpha:1.0];
updateButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
updateButton.layer.cornerRadius = 12;
[updateButton.heightAnchor constraintEqualToConstant:52].active = YES;
[updateButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
NSURL *url = [NSURL URLWithString:nsUrl];
if (url) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
dismissCover();
}] forControlEvents:UIControlEventTouchUpInside];
UIButton *skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
[skipButton setTitle:nsSkip forState:UIControlStateNormal];
[skipButton setTitleColor:[UIColor colorWithWhite:0.7 alpha:1.0] forState:UIControlStateNormal];
skipButton.titleLabel.font = [UIFont systemFontOfSize:17];
[skipButton.heightAnchor constraintEqualToConstant:44].active = YES;
[skipButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
dismissCover();
}] forControlEvents:UIControlEventTouchUpInside];
UIStackView *stack = [[[UIStackView alloc] initWithArrangedSubviews:@[titleLabel, messageLabel, updateButton, skipButton]] autorelease];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 16;
stack.translatesAutoresizingMaskIntoConstraints = NO;
[stack setCustomSpacing:28 afterView:messageLabel];
[vc.view addSubview:stack];
[NSLayoutConstraint activateConstraints:@[
[stack.centerYAnchor constraintEqualToAnchor:vc.view.centerYAnchor],
[stack.leadingAnchor constraintEqualToAnchor:vc.view.leadingAnchor constant:32],
[stack.trailingAnchor constraintEqualToAnchor:vc.view.trailingAnchor constant:-32]
]];
});
#else
Q_UNUSED(title) Q_UNUSED(message) Q_UNUSED(updateTitle) Q_UNUSED(skipTitle) Q_UNUSED(storeUrl)
#endif
}

View File

@@ -0,0 +1,24 @@
#ifndef IOSCONTEXTMENU_H
#define IOSCONTEXTMENU_H
#include <QObject>
#include <QQuickItem>
// Presents the native iOS edit menu (UIEditMenuInteraction) for a text
// control. The menu items (Cut/Copy/Paste/Select All) are provided by the
// system based on the first responder, which is Qt's text input responder
// for the focused control.
//
// Needed because the ContextMenu attached type is backed by a native menu
// on iOS only since Qt 6.10 — on Qt 6.9 it opens a Qt-drawn menu instead.
class IosContextMenu : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
Q_INVOKABLE bool isAvailable() const;
Q_INVOKABLE void present(QQuickItem *target, qreal x, qreal y);
};
#endif // IOSCONTEXTMENU_H

View File

@@ -0,0 +1,215 @@
#import "ioscontextmenu.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#include <QtCore/QCoreApplication>
#include <QtCore/QPointer>
#include <QtGui/QGuiApplication>
#include <QtGui/QInputMethod>
#include <QtQuick/QQuickWindow>
namespace
{
// Keys for attaching the helper objects to the UIView.
const void *kEditMenuInteractionKey = &kEditMenuInteractionKey;
const void *kEditMenuDelegateKey = &kEditMenuDelegateKey;
const void *kEditMenuResponderKey = &kEditMenuResponderKey;
// Menu titles reuse the ContextMenuType translations; the accelerator
// ampersands are meaningless on iOS and get stripped.
NSString *menuTitle(const char *sourceText)
{
QString title = QCoreApplication::translate("ContextMenuType", sourceText);
title.remove(QLatin1Char('&'));
return title.toNSString();
}
// The handler outlives the delegate call, so it must own its own copy of the
// guarded pointer: the local variable here is captured by the block by value
// (copy-constructed when the block is copied to the heap). Capturing a
// C++ lambda's reference capture instead would leave the block with a
// dangling pointer into the delegate method's stack frame.
API_AVAILABLE(ios(16.0))
UIAction *makeEditAction(NSString *title, const QPointer<QQuickItem> &target, const char *slot)
{
const QPointer<QQuickItem> guardedTarget = target;
return [UIAction actionWithTitle:title
image:nil
identifier:nil
handler:^(UIAction *) {
if (QQuickItem *item = guardedTarget.data()) {
// Queued: the handler fires mid-dismissal
// of the menu, let UIKit unwind first.
QMetaObject::invokeMethod(item, slot, Qt::QueuedConnection);
}
}];
}
}
// UIKit presents an edit menu only when the first responder is inside the
// interaction view's hierarchy. While the virtual keyboard is up that is
// Qt's text input responder, but for read-only fields (or before the
// keyboard appears) nothing suitable is first responder and the present is
// silently ignored ("did not have performable commands and/or actions").
// This zero-sized subview steps in as the first responder for those cases.
@interface AmneziaEditMenuResponderView : UIView
@end
@implementation AmneziaEditMenuResponderView
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end
// Builds the edit menu from the state of the focused QML text control and
// invokes its slots directly. The system's suggested actions can't be used:
// they are collected from the first responder, and Qt's text responder is
// first responder only while the virtual keyboard is up (never for read-only
// fields), which would leave the menu empty.
API_AVAILABLE(ios(16.0))
@interface AmneziaEditMenuDelegate : NSObject <UIEditMenuInteractionDelegate>
@property (nonatomic, weak) UIView *responderView;
- (void)setTargetItem:(QQuickItem *)item;
@end
@implementation AmneziaEditMenuDelegate {
QPointer<QQuickItem> m_target;
}
- (void)setTargetItem:(QQuickItem *)item
{
m_target = item;
}
- (UIMenu *)editMenuInteraction:(UIEditMenuInteraction *)interaction
menuForConfiguration:(UIEditMenuConfiguration *)configuration
suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions
{
QQuickItem *item = m_target.data();
if (!item) {
return nil;
}
// The system's suggested actions are UICommands and get re-validated
// against the first responder right before the menu shows, which makes
// them hostage to Qt's input-method state. UIActions with handlers skip
// that validation entirely, so the menu is always built by hand from the
// QML control's state.
const bool hasSelection = !item->property("selectedText").toString().isEmpty();
const bool readOnly = item->property("readOnly").toBool();
const bool canPaste = item->property("canPaste").toBool();
const bool hasText = item->property("length").toInt() > 0;
NSMutableArray<UIMenuElement *> *actions = [NSMutableArray array];
if (hasSelection && !readOnly) {
[actions addObject:makeEditAction(menuTitle("C&ut"), m_target, "cut")];
}
if (hasSelection) {
[actions addObject:makeEditAction(menuTitle("&Copy"), m_target, "copy")];
}
if (canPaste && !readOnly) {
[actions addObject:makeEditAction(menuTitle("&Paste"), m_target, "paste")];
}
if (hasText) {
[actions addObject:makeEditAction(menuTitle("&SelectAll"), m_target, "selectAll")];
}
if (actions.count == 0) {
return nil;
}
return [UIMenu menuWithTitle:@"" children:actions];
}
- (void)editMenuInteraction:(UIEditMenuInteraction *)interaction
willDismissMenuForConfiguration:(UIEditMenuConfiguration *)configuration
animator:(id<UIEditMenuInteractionAnimating>)animator
{
// Give the borrowed first-responder status back once the menu goes away.
if (self.responderView.isFirstResponder) {
[self.responderView resignFirstResponder];
}
}
@end
bool IosContextMenu::isAvailable() const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
// Since Qt 6.10 the ContextMenu attached type is backed by a native menu
// on iOS, so the helper must stay out of the way.
return false;
#else
// UIEditMenuInteraction needs iOS 16, which is the deployment target.
return true;
#endif
}
void IosContextMenu::present(QQuickItem *target, qreal x, qreal y)
{
if (!target || !target->window()) {
return;
}
// On iOS QWindow::winId() is the backing UIView.
UIView *view = (__bridge UIView *)reinterpret_cast<void *>(target->window()->winId());
if (!view) {
return;
}
// Scene coordinates match the backing view's coordinate space.
const QPointF scenePos = target->mapToScene(QPointF(x, y));
AmneziaEditMenuDelegate *delegate = objc_getAssociatedObject(view, kEditMenuDelegateKey);
UIEditMenuInteraction *interaction = objc_getAssociatedObject(view, kEditMenuInteractionKey);
AmneziaEditMenuResponderView *responderView = objc_getAssociatedObject(view, kEditMenuResponderKey);
if (!interaction) {
responderView = [[AmneziaEditMenuResponderView alloc] initWithFrame:CGRectZero];
[view addSubview:responderView];
delegate = [[AmneziaEditMenuDelegate alloc] init];
delegate.responderView = responderView;
interaction = [[UIEditMenuInteraction alloc] initWithDelegate:delegate];
[view addInteraction:interaction];
objc_setAssociatedObject(view, kEditMenuResponderKey, responderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(view, kEditMenuDelegateKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(view, kEditMenuInteractionKey, interaction, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[delegate setTargetItem:target];
// While the keyboard is up for the target field, Qt's text input
// responder is the first responder and lives in this view's responder
// chain, which satisfies UIKit. Otherwise (read-only fields never raise
// the keyboard) borrow first-responder status. Qt's idea of the keyboard
// state is not trustworthy here, so key off the field being editable.
const bool readOnly = target->property("readOnly").toBool();
if (readOnly || !QGuiApplication::inputMethod()->isVisible()) {
[responderView becomeFirstResponder];
}
// Defer the actual present to the next main-loop iteration: the request
// arrives while the long-press touch is still active, and presenting
// mid-gesture makes UIKit discard the menu. Requests are also coalesced —
// a double tap asks for the menu twice (the TapHandler and the ContextMenu
// attached type both fire), and re-presenting makes the menu flicker.
static BOOL presentPending = NO;
if (presentPending) {
return;
}
presentPending = YES;
UIEditMenuConfiguration *configuration =
[UIEditMenuConfiguration configurationWithIdentifier:nil
sourcePoint:CGPointMake(scenePos.x(), scenePos.y())];
dispatch_async(dispatch_get_main_queue(), ^{
presentPending = NO;
[interaction presentEditMenuWithConfiguration:configuration];
});
}

View File

@@ -144,6 +144,30 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
}
if (!config.m_headerProtectionKey.isEmpty()) {
QByteArray headerProtectionKey =
QByteArray::fromBase64(config.m_headerProtectionKey.toUtf8());
out << "header_protection_key=" << QString(headerProtectionKey.toHex()) << "\n";
}
if (!config.m_contentPaddingAddition.isEmpty()) {
out << "content_padding_addition=" << config.m_contentPaddingAddition << "\n";
}
if (!config.m_rekeyAfterTime.isEmpty()) {
out << "rekey_after_time=" << config.m_rekeyAfterTime << "\n";
}
if (!config.m_rekeyTimeout.isEmpty()) {
out << "rekey_timeout=" << config.m_rekeyTimeout << "\n";
}
if (!config.m_rejectAfterTime.isEmpty()) {
out << "reject_after_time=" << config.m_rejectAfterTime << "\n";
}
if (!config.m_keepaliveTimeout.isEmpty()) {
out << "keepalive_timeout=" << config.m_keepaliveTimeout << "\n";
}
if (!config.m_maxHandshakeAttempts.isEmpty()) {
out << "max_handshake_attempts=" << config.m_maxHandshakeAttempts << "\n";
}
int err = uapiErrno(uapiCommand(message));
if (err != 0) {
logger.error() << "Interface configuration failed:" << strerror(err);
@@ -229,7 +253,9 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
out << config.m_serverPort << "\n";
out << "replace_allowed_ips=true\n";
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
if (!config.m_persistentKeepalive.isEmpty()) {
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
}
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
out << "allowed_ip=" << ip.toString() << "\n";
}

View File

@@ -142,6 +142,30 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
}
if (!config.m_headerProtectionKey.isEmpty()) {
QByteArray headerProtectionKey =
QByteArray::fromBase64(config.m_headerProtectionKey.toUtf8());
out << "header_protection_key=" << QString(headerProtectionKey.toHex()) << "\n";
}
if (!config.m_contentPaddingAddition.isEmpty()) {
out << "content_padding_addition=" << config.m_contentPaddingAddition << "\n";
}
if (!config.m_rekeyAfterTime.isEmpty()) {
out << "rekey_after_time=" << config.m_rekeyAfterTime << "\n";
}
if (!config.m_rekeyTimeout.isEmpty()) {
out << "rekey_timeout=" << config.m_rekeyTimeout << "\n";
}
if (!config.m_rejectAfterTime.isEmpty()) {
out << "reject_after_time=" << config.m_rejectAfterTime << "\n";
}
if (!config.m_keepaliveTimeout.isEmpty()) {
out << "keepalive_timeout=" << config.m_keepaliveTimeout << "\n";
}
if (!config.m_maxHandshakeAttempts.isEmpty()) {
out << "max_handshake_attempts=" << config.m_maxHandshakeAttempts << "\n";
}
int err = uapiErrno(uapiCommand(message));
if (err != 0) {
logger.error() << "Interface configuration failed:" << strerror(err);
@@ -229,7 +253,9 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) {
out << config.m_serverPort << "\n";
out << "replace_allowed_ips=true\n";
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
if (!config.m_persistentKeepalive.isEmpty()) {
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
}
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
out << "allowed_ip=" << ip.toString() << "\n";
}

View File

@@ -5,8 +5,10 @@
#ifndef MACOSSTATUSICON_H
#define MACOSSTATUSICON_H
#include <QMenu>
#include <QObject>
#include <QString>
class QMenu;
class MacOSStatusIcon final : public QObject {
Q_OBJECT
@@ -16,12 +18,12 @@ class MacOSStatusIcon final : public QObject {
explicit MacOSStatusIcon(QObject* parent);
~MacOSStatusIcon();
public:
void setIcon(const QString& iconUrl);
void setIndicatorColor(const QColor& indicatorColor);
void setMenu(NSMenu* statusBarMenu);
void setToolTip(const QString& tooltip);
void setIcon(const QString& iconPath);
void setMenu(QMenu* menu);
void showMessage(const QString& title, const QString& message);
private:
void* m_statusItem = nullptr;
};
#endif // MACOSSTATUSICON_H

View File

@@ -4,201 +4,79 @@
#include "macosstatusicon.h"
#include "leakdetector.h"
#include "logger.h"
#include <QDebug>
#include <QMenu>
#import <Cocoa/Cocoa.h>
#import <UserNotifications/UserNotifications.h>
#import <QResource>
/**
* Creates a NSStatusItem with that can hold an icon. Additionally a NSView is
* set as a subview to the button item of the status item. The view serves as
* an indicator that can be displayed in color eventhough the icon is set as a
* template. In that way we give the system control over its effective
* appearance.
*/
@interface MacOSStatusIconDelegate : NSObject
@property(assign) NSStatusItem* statusItem;
@property(assign) NSView* statusIndicator;
- (void)setIcon:(NSData*)imageData;
- (void)setIndicator;
- (void)setIndicatorColor:(NSColor*)color;
- (void)setMenu:(NSMenu*)statusBarMenu;
- (void)setToolTip:(NSString*)tooltip;
@end
@implementation MacOSStatusIconDelegate
/**
* Initializes and sets the status item and indicator objects.
*
* @return An instance of MacOSStatusIconDelegate.
*/
- (id)init {
self = [super init];
// Create status item
self.statusItem =
[[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
self.statusItem.visible = true;
// Add the indicator as a subview
[self setIndicator];
return self;
}
/**
* Sets the image for the status icon.
*
* @param iconPath The data for the icon image.
*/
- (void)setIcon:(NSData*)imageData {
NSImage* image = [[NSImage alloc] initWithData:imageData];
[image setTemplate:true];
[self.statusItem.button setImage:image];
[image release];
}
/**
* Adds status indicator as a subview to the status item button.
*/
- (void)setIndicator {
float viewHeight = NSHeight([self.statusItem.button bounds]);
float dotSize = viewHeight * 0.35;
float dotOrigin = (viewHeight - dotSize) * 0.8;
NSView* dot = [[NSView alloc] initWithFrame:NSMakeRect(dotOrigin, dotOrigin, dotSize, dotSize)];
self.statusIndicator = dot;
self.statusIndicator.wantsLayer = true;
self.statusIndicator.layer.cornerRadius = dotSize * 0.5;
[self.statusItem.button addSubview:self.statusIndicator];
[dot release];
}
/**
* Sets the color if the indicator.
*
* @param color The indicator background color.
*/
- (void)setIndicatorColor:(NSColor*)color {
if (self.statusIndicator) {
self.statusIndicator.layer.backgroundColor = color.CGColor;
}
}
/**
* Sets the status bar menu to the status item.
*
* @param statusBarMenu The menu object that is passed from QT.
*/
- (void)setMenu:(NSMenu*)statusBarMenu {
[self.statusItem setMenu:statusBarMenu];
}
/**
* Sets the tooltip string for the status item.
*
* @param tooltip The tooltip string.
*/
- (void)setToolTip:(NSString*)tooltip {
[self.statusItem.button setToolTip:tooltip];
}
@end
namespace {
Logger logger("MacOSStatusIcon");
MacOSStatusIconDelegate* m_statusBarIcon = nullptr;
}
MacOSStatusIcon::MacOSStatusIcon(QObject* parent) : QObject(parent) {
MZ_COUNT_CTOR(MacOSStatusIcon);
logger.debug() << "Register delegate";
Q_ASSERT(!m_statusBarIcon);
m_statusBarIcon = [[MacOSStatusIconDelegate alloc] init];
NSStatusItem* item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
item.visible = YES;
m_statusItem = [item retain];
}
MacOSStatusIcon::~MacOSStatusIcon() {
MZ_COUNT_DTOR(MacOSStatusIcon);
logger.debug() << "Remove delegate";
Q_ASSERT(m_statusBarIcon);
[static_cast<MacOSStatusIconDelegate*>(m_statusBarIcon) dealloc];
m_statusBarIcon = nullptr;
NSStatusItem* item = static_cast<NSStatusItem*>(m_statusItem);
item.menu = nil;
[[NSStatusBar systemStatusBar] removeStatusItem:item];
[item release];
m_statusItem = nullptr;
}
void MacOSStatusIcon::setIcon(const QString& iconPath) {
logger.debug() << "Set icon" << iconPath;
QResource imageResource = QResource(iconPath);
Q_ASSERT(imageResource.isValid());
[m_statusBarIcon setIcon:imageResource.uncompressedData().toNSData()];
}
void MacOSStatusIcon::setIndicatorColor(const QColor& indicatorColor) {
logger.debug() << "Set indicator color";
if (!indicatorColor.isValid()) {
[m_statusBarIcon setIndicatorColor:[NSColor clearColor]];
QResource resource(iconPath);
if (!resource.isValid()) {
qWarning() << "MacOSStatusIcon: invalid icon resource" << iconPath;
return;
}
NSColor* color = [NSColor colorWithCalibratedRed:indicatorColor.red() / 255.0f
green:indicatorColor.green() / 255.0f
blue:indicatorColor.blue() / 255.0f
alpha:indicatorColor.alpha() / 255.0f];
[m_statusBarIcon setIndicatorColor:color];
NSImage* image = [[NSImage alloc] initWithData:resource.uncompressedData().toNSData()];
CGFloat side = [[NSStatusBar systemStatusBar] thickness] - 4.0;
image.size = NSMakeSize(side, side);
static_cast<NSStatusItem*>(m_statusItem).button.image = image;
[image release];
}
void MacOSStatusIcon::setMenu(NSMenu* statusBarMenu) {
logger.debug() << "Set menu";
[m_statusBarIcon setMenu:statusBarMenu];
}
void MacOSStatusIcon::setToolTip(const QString& tooltip) {
logger.debug() << "Set tooltip";
[m_statusBarIcon setToolTip:tooltip.toNSString()];
void MacOSStatusIcon::setMenu(QMenu* menu) {
static_cast<NSStatusItem*>(m_statusItem).menu = menu ? menu->toNSMenu() : nil;
}
void MacOSStatusIcon::showMessage(const QString& title, const QString& message) {
logger.debug() << "Show message";
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
// This is a no-op is authorization has been granted.
// This is a no-op if authorization has already been granted.
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError* _Nullable error) {
completionHandler:^(BOOL, NSError* _Nullable error) {
if (error) {
// Note: This error may happen if the application is not signed.
NSLog(@"Error asking for permission to send notifications %@", error);
return;
// Note: this error may happen if the application is not signed.
qWarning() << "MacOSStatusIcon: notification authorization error:"
<< QString::fromNSString(error.localizedDescription);
}
}];
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [title.toNSString() autorelease];
content.body = [message.toNSString() autorelease];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound];
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
content:content
trigger:trigger];
trigger:nil];
[content release];
[center addNotificationRequest:request
withCompletionHandler:^(NSError* _Nullable error) {
if (error) {
logger.error() << "Local Notification failed" << error;
qWarning() << "MacOSStatusIcon: local notification failed:"
<< QString::fromNSString(error.localizedDescription);
}
}];
}

View File

@@ -180,7 +180,9 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
out << config.m_serverPort << "\n";
out << "replace_allowed_ips=true\n";
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
if (!config.m_persistentKeepalive.isEmpty()) {
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
}
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
out << "allowed_ip=" << ip.toString() << "\n";
}

View File

@@ -25,9 +25,19 @@ H1 = $INIT_PACKET_MAGIC_HEADER
H2 = $RESPONSE_PACKET_MAGIC_HEADER
H3 = $UNDERLOAD_PACKET_MAGIC_HEADER
H4 = $TRANSPORT_PACKET_MAGIC_HEADER
HeaderProtectionKey = $HEADER_PROTECTION_KEY
ContentPaddingAddition = $CONTENT_PADDING_ADDITION
RekeyAfterTime = $REKEY_AFTER_TIME
RekeyTimeout = $REKEY_TIMEOUT
RejectAfterTime = $REJECT_AFTER_TIME
KeepaliveTimeout = $KEEPALIVE_TIMEOUT
MaxHandshakeAttempts = $MAX_HANDSHAKE_ATTEMPTS
# I1 = $SPECIAL_JUNK_1
# I2 = $SPECIAL_JUNK_2
# I3 = $SPECIAL_JUNK_3
# I4 = $SPECIAL_JUNK_4
# I5 = $SPECIAL_JUNK_5
EOF
# Every AWG parameter is optional - drop the lines whose value came out empty
sed -i '/^[^=]*= *$/d' /opt/amnezia/awg/awg0.conf

View File

@@ -18,10 +18,17 @@ I2 = $SPECIAL_JUNK_2
I3 = $SPECIAL_JUNK_3
I4 = $SPECIAL_JUNK_4
I5 = $SPECIAL_JUNK_5
HeaderProtectionKey = $HEADER_PROTECTION_KEY
ContentPaddingAddition = $CONTENT_PADDING_ADDITION
RekeyAfterTime = $REKEY_AFTER_TIME
RekeyTimeout = $REKEY_TIMEOUT
RejectAfterTime = $REJECT_AFTER_TIME
KeepaliveTimeout = $KEEPALIVE_TIMEOUT
MaxHandshakeAttempts = $MAX_HANDSHAKE_ATTEMPTS
[Peer]
PublicKey = $WIREGUARD_SERVER_PUBLIC_KEY
PresharedKey = $WIREGUARD_PSK
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = $SERVER_IP_ADDRESS:$AWG_SERVER_PORT
PersistentKeepalive = 25
PersistentKeepalive = $PERSISTENT_KEEPALIVE

View File

@@ -47,6 +47,20 @@ else
FAKETLS_SECRET=""
fi
# Persist deployment state for restore on re-scan.
{
printf 'mode=%s\n' "$TRANSPORT_MODE"
printf 'domain=%s\n' "$MTPROXY_TLS_DOMAIN"
printf 'tag=%s\n' "$MTPROXY_TAG"
printf 'additional=%s\n' "$MTPROXY_ADDITIONAL_SECRETS"
printf 'workers_mode=%s\n' "$MTPROXY_WORKERS_MODE"
printf 'workers=%s\n' "$MTPROXY_WORKERS"
printf 'nat_enabled=%s\n' "$MTPROXY_NAT_ENABLED"
printf 'nat_internal=%s\n' "$MTPROXY_NAT_INTERNAL_IP"
printf 'nat_external=%s\n' "$MTPROXY_NAT_EXTERNAL_IP"
printf 'public_host=%s\n' "$MTPROXY_PUBLIC_HOST"
} > /data/mtproxy-meta
# Active link secret depends on transport mode
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
LINK_SECRET="$FAKETLS_SECRET"

View File

@@ -28,6 +28,9 @@ rm -f /data/config.toml
if [ -n "$TELEMT_TAG" ]; then
echo "ad_tag = \"$TELEMT_TAG\""
fi
if [ -n "$TELEMT_MIDDLE_PROXY_NAT_IP" ]; then
echo "middle_proxy_nat_ip = \"$TELEMT_MIDDLE_PROXY_NAT_IP\""
fi
echo ""
echo "[general.modes]"
echo "classic = false"

View File

@@ -1,7 +1,7 @@
FROM alpine:3.15
LABEL maintainer="AmneziaVPN"
ARG XRAY_RELEASE="v25.8.3"
ARG XRAY_RELEASE="v26.7.28"
RUN apk add --no-cache curl unzip bash openssl netcat-openbsd dumb-init rng-tools xz
RUN apk --update upgrade --no-cache

View File

@@ -2,17 +2,11 @@ cd /opt/amnezia/xray
XRAY_CLIENT_ID=$(xray uuid) && echo $XRAY_CLIENT_ID > /opt/amnezia/xray/xray_uuid.key
XRAY_SHORT_ID=$(openssl rand -hex 8) && echo $XRAY_SHORT_ID > /opt/amnezia/xray/xray_short_id.key
# Parse x25519 keypair by label (v26.7 output has an extra Hash32 line; line-index parsing breaks).
KEYPAIR=$(xray x25519)
LINE_NUM=1
while IFS= read -r line; do
if [[ $LINE_NUM -gt 1 ]]
then
IFS=":" read FIST XRAY_PUBLIC_KEY <<< "$line"
else
LINE_NUM=$((LINE_NUM + 1))
IFS=":" read FIST XRAY_PRIVATE_KEY <<< "$line"
fi
done <<< "$KEYPAIR"
XRAY_PRIVATE_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]rivate[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*(PublicKey):[[:space:]]*//p' | head -1)
[ -z "$XRAY_PUBLIC_KEY" ] && XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]ublic[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
XRAY_PRIVATE_KEY=$(echo $XRAY_PRIVATE_KEY | tr -d ' ')
XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
@@ -21,47 +15,4 @@ XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
echo $XRAY_PUBLIC_KEY > /opt/amnezia/xray/xray_public.key
echo $XRAY_PRIVATE_KEY > /opt/amnezia/xray/xray_private.key
cat > /opt/amnezia/xray/server.json <<EOF
{
"log": {
"loglevel": "error"
},
"inbounds": [
{
"port": $XRAY_SERVER_PORT,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "$XRAY_CLIENT_ID",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"dest": "$XRAY_SITE_NAME:443",
"serverNames": [
"$XRAY_SITE_NAME"
],
"privateKey": "$XRAY_PRIVATE_KEY",
"shortIds": [
"$XRAY_SHORT_ID"
]
}
}
}
],
"outbounds": [
{
"protocol": "freedom"
}
]
}
EOF
# server.json is written by the client (writeServerConfigForSetup); this script only makes keys.

View File

@@ -5,6 +5,7 @@ sudo docker run -d \
--restart always \
--cap-add=NET_ADMIN \
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/tcp \
-p $XRAY_SERVER_PORT:$XRAY_SERVER_PORT/udp \
--name $CONTAINER_NAME $CONTAINER_NAME
sudo docker network connect amnezia-dns-net $CONTAINER_NAME

View File

@@ -10,6 +10,8 @@ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport $XRAY_SERVER_PORT -j ACCEPT
iptables -A INPUT -p udp --dport $XRAY_SERVER_PORT -j ACCEPT
iptables -P INPUT DROP
ip6tables -A INPUT -i lo -j ACCEPT

View File

@@ -29,6 +29,7 @@ void ConnectionUiController::openConnection()
{
const QString serverId = m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
m_connectionController->setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}

View File

@@ -0,0 +1,164 @@
#include "marketplaceUpdateController.h"
#include <QDebug>
#include "version.h"
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocale>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QVersionNumber>
#endif
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h"
#endif
#if defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#endif
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
namespace
{
#if defined(Q_OS_IOS)
constexpr auto kIosBundleId = "org.amnezia.AmneziaVPN";
constexpr auto kIosStoreUrlFallback = "itms-apps://itunes.apple.com/app/id1600529900";
#else
constexpr auto kAndroidPackage = "org.amnezia.vpn";
constexpr auto kAndroidStoreUrl = "https://play.google.com/store/apps/details?id=org.amnezia.vpn";
#endif
} // namespace
#endif
MarketplaceUpdateController::MarketplaceUpdateController(QObject *parent) : QObject(parent)
{
}
void MarketplaceUpdateController::start()
{
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
// Cover the app immediately so the store UI is not visible before the check
// resolves (avoids a flash of the main window before the update screen).
showCover();
const QUrl url = versionSourceUrl();
if (!url.isValid()) {
hideCover();
return;
}
QNetworkRequest request(url);
request.setTransferTimeout(1000);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("AmneziaVPN"));
QNetworkReply *reply = m_nam.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[MarketplaceUpdate] network error:" << reply->errorString();
hideCover();
return;
}
QString version;
QString storeUrl;
if (!parseStoreVersion(reply->readAll(), version, storeUrl) || version.isEmpty()) {
qWarning() << "[MarketplaceUpdate] could not determine store version";
hideCover();
return;
}
const auto current = QVersionNumber::fromString(QString(APP_VERSION)).normalized();
const auto store = QVersionNumber::fromString(version).normalized();
qInfo() << "[MarketplaceUpdate] current:" << current.toString() << "store:" << store.toString();
if (store > current) {
showUpdatePrompt(storeUrl);
} else {
hideCover();
}
});
#endif
}
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QUrl MarketplaceUpdateController::versionSourceUrl() const
{
#if defined(Q_OS_IOS)
const QString country = QLocale::system().name().section('_', 1, 1).toLower();
QString url = QStringLiteral("https://itunes.apple.com/lookup?bundleId=%1").arg(kIosBundleId);
if (!country.isEmpty()) {
url += QStringLiteral("&country=%1").arg(country);
}
return QUrl(url);
#else
return QUrl(QStringLiteral("https://play.google.com/store/apps/details?id=%1&hl=en&gl=US").arg(kAndroidPackage));
#endif
}
bool MarketplaceUpdateController::parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl)
{
#if defined(Q_OS_IOS)
const auto results = QJsonDocument::fromJson(body).object().value("results").toArray();
if (results.isEmpty()) {
return false;
}
const auto first = results.first().toObject();
version = first.value("version").toString();
storeUrl = first.value("trackViewUrl").toString();
if (storeUrl.isEmpty()) {
storeUrl = QString::fromLatin1(kIosStoreUrlFallback);
}
return !version.isEmpty();
#else
const QString html = QString::fromUtf8(body);
static const QRegularExpression re(QStringLiteral("\\[\\[\\[\"(\\d+\\.\\d+(?:\\.\\d+){0,2})\"\\]\\]"));
const auto match = re.match(html);
if (match.hasMatch()) {
version = match.captured(1);
}
storeUrl = QString::fromLatin1(kAndroidStoreUrl);
return !version.isEmpty();
#endif
}
void MarketplaceUpdateController::showCover()
{
#if defined(Q_OS_IOS)
IosController::Instance()->showUpdateCover();
#else
AndroidController::instance()->showUpdateCover();
#endif
}
void MarketplaceUpdateController::hideCover()
{
#if defined(Q_OS_IOS)
IosController::Instance()->hideUpdateCover();
#else
AndroidController::instance()->hideUpdateCover();
#endif
}
void MarketplaceUpdateController::showUpdatePrompt(const QString &storeUrl)
{
const QString title = tr("Update available");
const QString message = tr("A new version of AmneziaVPN is available.");
const QString updateTitle = tr("Update");
const QString skipTitle = tr("Skip");
#if defined(Q_OS_IOS)
IosController::Instance()->showUpdatePrompt(title, message, updateTitle, skipTitle, storeUrl);
#else
AndroidController::instance()->showUpdatePrompt(title, message, updateTitle, skipTitle, storeUrl);
#endif
}
#endif

View File

@@ -0,0 +1,30 @@
#ifndef MARKETPLACEUPDATECONTROLLER_H
#define MARKETPLACEUPDATECONTROLLER_H
#include <QNetworkAccessManager>
#include <QObject>
#include <QUrl>
class MarketplaceUpdateController : public QObject
{
Q_OBJECT
public:
explicit MarketplaceUpdateController(QObject *parent = nullptr);
public slots:
void start();
private:
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QUrl versionSourceUrl() const;
bool parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl);
void showCover();
void hideCover();
void showUpdatePrompt(const QString &storeUrl);
QNetworkAccessManager m_nam;
#endif
};
#endif // MARKETPLACEUPDATECONTROLLER_H

View File

@@ -69,6 +69,14 @@ void ExportUiController::generateQrFromString(const QString &text)
emit exportConfigChanged();
}
void ExportUiController::generateQrFromStringRaw(const QString &text)
{
clearPreviousConfig();
m_config = text;
m_qrCodes = { qrCodeUtils::generatePlainQrCodeImage(text.toUtf8()) };
emit exportConfigChanged();
}
QString ExportUiController::getConfig()
{
return m_config;

View File

@@ -26,6 +26,7 @@ public slots:
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
void generateXrayConfig(const QString &serverId, const QString &clientName);
void generateQrFromString(const QString &text);
void generateQrFromStringRaw(const QString &text);
QString getConfig();
QString getNativeConfigString();

View File

@@ -567,12 +567,12 @@ void InstallUiController::clearProcessedServerCredentials()
void InstallUiController::setProcessedServerCredentials(const QString &hostName, const QString &userName, const QString &secretData)
{
m_processedServerCredentials.hostName = hostName;
m_processedServerCredentials.hostName = hostName.trimmed();
if (m_processedServerCredentials.hostName.contains(":")) {
m_processedServerCredentials.port = m_processedServerCredentials.hostName.split(":").at(1).toInt();
m_processedServerCredentials.hostName = m_processedServerCredentials.hostName.split(":").at(0);
}
m_processedServerCredentials.userName = userName;
m_processedServerCredentials.userName = userName.trimmed();
m_processedServerCredentials.secretData = secretData;
}
@@ -635,6 +635,7 @@ void InstallUiController::validateConfig()
{
const QString serverId = m_serversController->getDefaultServerId();
if (serverId.isEmpty()) {
emit configValidated(false);
return;
}
m_installController->validateConfig(serverId);

View File

@@ -5,6 +5,8 @@
#include "core/utils/protocolEnum.h"
#include "core/models/protocolConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "core/utils/constants/protocolConstants.h"
using namespace amnezia;
@@ -385,6 +387,59 @@ bool ServersUiController::isDefaultServerCurrentlyProcessed() const
return m_serversController->getDefaultServerId() == m_processedServerId;
}
bool ServersUiController::serverHasOutdatedAwgContainer(const QString &serverId) const
{
// The warning suggests reinstalling the container, so it only makes sense
// for servers the user can administer
if (!isServerHasWriteAccess(serverId)) {
return false;
}
const QMap<DockerContainer, ContainerConfig> containers = m_serversController->getServerContainersMap(serverId);
for (DockerContainer container : { DockerContainer::Awg, DockerContainer::Awg2 }) {
if (!containers.contains(container)) {
continue;
}
if (const auto* awgConfig = containers.value(container).getAwgProtocolConfig()) {
if (awgConfig->serverConfig.protocolVersion != protocols::awg::awgV3) {
return true;
}
}
}
return false;
}
bool ServersUiController::defaultServerHasOutdatedAwgContainer() const
{
return serverHasOutdatedAwgContainer(getDefaultServerId());
}
bool ServersUiController::isContainerOutdatedAwg(int containerIndex) const
{
if (!isProcessedServerHasWriteAccess()) {
return false;
}
DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (!ContainerUtils::isAwgContainer(container)) {
return false;
}
const QMap<DockerContainer, ContainerConfig> containers = m_serversController->getServerContainersMap(m_processedServerId);
if (!containers.contains(container)) {
return false;
}
if (const auto* awgConfig = containers.value(container).getAwgProtocolConfig()) {
return awgConfig->serverConfig.protocolVersion != protocols::awg::awgV3;
}
return false;
}
bool ServersUiController::isProcessedContainerOutdatedAwg() const
{
return isContainerOutdatedAwg(m_processedContainerIndex);
}
bool ServersUiController::isProcessedServerHasWriteAccess() const
{
return isServerHasWriteAccess(m_processedServerId);

View File

@@ -27,6 +27,7 @@ class ServersUiController : public QObject
Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool defaultServerHasOutdatedAwgContainer READ defaultServerHasOutdatedAwgContainer NOTIFY defaultServerIdChanged)
Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged)
Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged)
@@ -72,6 +73,11 @@ public slots:
bool isDefaultServerFromApi() const;
bool hasServerWithWriteAccess() const;
bool serverHasOutdatedAwgContainer(const QString &serverId) const;
bool defaultServerHasOutdatedAwgContainer() const;
bool isContainerOutdatedAwg(int containerIndex) const;
bool isProcessedContainerOutdatedAwg() const;
QString serverName(const QString &serverId) const;
QString serverHostName(const QString &serverId) const;
int serverDefaultContainer(const QString &serverId) const;

View File

@@ -22,7 +22,7 @@ QVariant IpSplitTunnelingModel::data(const QModelIndex &index, int role) const
break;
}
case IpRole: {
return m_sites.at(index.row()).second;
return m_sites.at(index.row()).second.join(", ");
break;
}
default: {
@@ -33,7 +33,7 @@ QVariant IpSplitTunnelingModel::data(const QModelIndex &index, int role) const
return QVariant();
}
void IpSplitTunnelingModel::updateModel(const QVector<QPair<QString, QString>> &sites)
void IpSplitTunnelingModel::updateModel(const QVector<QPair<QString, QStringList>> &sites)
{
beginResetModel();
m_sites = sites;

View File

@@ -4,6 +4,7 @@
#include <QAbstractListModel>
#include <QVector>
#include <QPair>
#include <QStringList>
class IpSplitTunnelingModel : public QAbstractListModel
{
@@ -22,13 +23,13 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(const QVector<QPair<QString, QString>> &sites);
void updateModel(const QVector<QPair<QString, QStringList>> &sites);
protected:
QHash<int, QByteArray> roleNames() const override;
private:
QVector<QPair<QString, QString>> m_sites;
QVector<QPair<QString, QStringList>> m_sites;
};
#endif // IPSPLITTUNNELINGMODEL_H

View File

@@ -2,6 +2,8 @@
#include <QJsonDocument>
#include "core/configurators/wireguardConfigurator.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
@@ -42,6 +44,12 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
case Roles::ClientSpecialJunk3Role: m_protocolConfig.clientConfig->specialJunk3 = strValue; break;
case Roles::ClientSpecialJunk4Role: m_protocolConfig.clientConfig->specialJunk4 = strValue; break;
case Roles::ClientSpecialJunk5Role: m_protocolConfig.clientConfig->specialJunk5 = strValue; break;
case Roles::ClientContentPaddingAdditionRole: m_protocolConfig.clientConfig->contentPaddingAddition = strValue; break;
case Roles::ClientRekeyAfterTimeRole: m_protocolConfig.clientConfig->rekeyAfterTime = strValue; break;
case Roles::ClientRekeyTimeoutRole: m_protocolConfig.clientConfig->rekeyTimeout = strValue; break;
case Roles::ClientRejectAfterTimeRole: m_protocolConfig.clientConfig->rejectAfterTime = strValue; break;
case Roles::ClientKeepaliveTimeoutRole: m_protocolConfig.clientConfig->keepaliveTimeout = strValue; break;
case Roles::ClientMaxHandshakeAttemptsRole: m_protocolConfig.clientConfig->maxHandshakeAttempts = strValue; break;
case Roles::ServerJunkPacketCountRole: m_protocolConfig.serverConfig.junkPacketCount = strValue; break;
case Roles::ServerJunkPacketMinSizeRole: m_protocolConfig.serverConfig.junkPacketMinSize = strValue; break;
case Roles::ServerJunkPacketMaxSizeRole: m_protocolConfig.serverConfig.junkPacketMaxSize = strValue; break;
@@ -58,6 +66,24 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
case Roles::ServerSpecialJunk3Role: m_protocolConfig.serverConfig.specialJunk3 = strValue; break;
case Roles::ServerSpecialJunk4Role: m_protocolConfig.serverConfig.specialJunk4 = strValue; break;
case Roles::ServerSpecialJunk5Role: m_protocolConfig.serverConfig.specialJunk5 = strValue; break;
case Roles::ServerContentPaddingAdditionRole: m_protocolConfig.serverConfig.contentPaddingAddition = strValue; break;
case Roles::ServerRekeyAfterTimeRole: m_protocolConfig.serverConfig.rekeyAfterTime = strValue; break;
case Roles::ServerRekeyTimeoutRole: m_protocolConfig.serverConfig.rekeyTimeout = strValue; break;
case Roles::ServerRejectAfterTimeRole: m_protocolConfig.serverConfig.rejectAfterTime = strValue; break;
case Roles::ServerKeepaliveTimeoutRole: m_protocolConfig.serverConfig.keepaliveTimeout = strValue; break;
case Roles::ServerMaxHandshakeAttemptsRole: m_protocolConfig.serverConfig.maxHandshakeAttempts = strValue; break;
case Roles::ServerHeaderProtectionEnabledRole: {
if (value.toBool()) {
if (m_protocolConfig.serverConfig.headerProtectionKey.isEmpty()) {
const QString originalKey = m_originalProtocolConfig.serverConfig.headerProtectionKey;
m_protocolConfig.serverConfig.headerProtectionKey =
originalKey.isEmpty() ? WireguardConfigurator::genClientKeys().clientPrivKey : originalKey;
}
} else {
m_protocolConfig.serverConfig.headerProtectionKey.clear();
}
break;
}
default:
return false;
}
@@ -85,6 +111,13 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
case Roles::ClientSpecialJunk3Role: return m_protocolConfig.clientConfig->specialJunk3;
case Roles::ClientSpecialJunk4Role: return m_protocolConfig.clientConfig->specialJunk4;
case Roles::ClientSpecialJunk5Role: return m_protocolConfig.clientConfig->specialJunk5;
case Roles::ClientContentPaddingAdditionRole: return m_protocolConfig.clientConfig->contentPaddingAddition;
case Roles::ClientRekeyAfterTimeRole: return m_protocolConfig.clientConfig->rekeyAfterTime;
case Roles::ClientRekeyTimeoutRole: return m_protocolConfig.clientConfig->rekeyTimeout;
case Roles::ClientRejectAfterTimeRole: return m_protocolConfig.clientConfig->rejectAfterTime;
case Roles::ClientKeepaliveTimeoutRole: return m_protocolConfig.clientConfig->keepaliveTimeout;
case Roles::ClientMaxHandshakeAttemptsRole: return m_protocolConfig.clientConfig->maxHandshakeAttempts;
case Roles::ClientHeaderProtectionEnabledRole: return !m_protocolConfig.clientConfig->headerProtectionKey.isEmpty();
case Roles::ServerJunkPacketCountRole: return m_protocolConfig.serverConfig.junkPacketCount;
case Roles::ServerJunkPacketMinSizeRole: return m_protocolConfig.serverConfig.junkPacketMinSize;
@@ -103,7 +136,19 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
case Roles::ServerSpecialJunk4Role: return m_protocolConfig.serverConfig.specialJunk4;
case Roles::ServerSpecialJunk5Role: return m_protocolConfig.serverConfig.specialJunk5;
case Roles::IsAwg2Role: return m_protocolConfig.serverConfig.protocolVersion == protocols::awg::awgV2;
case Roles::ServerContentPaddingAdditionRole: return m_protocolConfig.serverConfig.contentPaddingAddition;
case Roles::ServerRekeyAfterTimeRole: return m_protocolConfig.serverConfig.rekeyAfterTime;
case Roles::ServerRekeyTimeoutRole: return m_protocolConfig.serverConfig.rekeyTimeout;
case Roles::ServerRejectAfterTimeRole: return m_protocolConfig.serverConfig.rejectAfterTime;
case Roles::ServerKeepaliveTimeoutRole: return m_protocolConfig.serverConfig.keepaliveTimeout;
case Roles::ServerMaxHandshakeAttemptsRole: return m_protocolConfig.serverConfig.maxHandshakeAttempts;
case Roles::ServerHeaderProtectionEnabledRole: return !m_protocolConfig.serverConfig.headerProtectionKey.isEmpty();
case Roles::IsAwg2Role: {
QString version = serverProtocolVersion();
return version == protocols::awg::awgV2 || version == protocols::awg::awgV3;
}
case Roles::IsAwg3Role: return serverProtocolVersion() == protocols::awg::awgV3;
}
return QVariant();
@@ -116,8 +161,6 @@ void AwgConfigModel::updateModel(amnezia::DockerContainer container, const amnez
m_protocolConfig = protocolConfig;
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
if (!m_protocolConfig.clientConfig.has_value()) {
m_protocolConfig.clientConfig = amnezia::AwgClientConfig{};
}
@@ -128,68 +171,9 @@ void AwgConfigModel::updateModel(amnezia::DockerContainer container, const amnez
endResetModel();
}
void AwgConfigModel::applyDefaultsToServerConfig(amnezia::AwgServerConfig& config)
QString AwgConfigModel::serverProtocolVersion() const
{
if (config.subnetAddress.isEmpty()) {
config.subnetAddress = protocols::wireguard::defaultSubnetAddress;
}
if (config.port.isEmpty()) {
config.port = protocols::awg::defaultPort;
}
if (config.transportProto.isEmpty()) {
config.transportProto = ProtocolUtils::transportProtoToString(
ProtocolUtils::defaultTransportProto(amnezia::Proto::Awg), amnezia::Proto::Awg);
}
if (config.junkPacketCount.isEmpty()) {
config.junkPacketCount = protocols::awg::defaultJunkPacketCount;
}
if (config.junkPacketMinSize.isEmpty()) {
config.junkPacketMinSize = protocols::awg::defaultJunkPacketMinSize;
}
if (config.junkPacketMaxSize.isEmpty()) {
config.junkPacketMaxSize = protocols::awg::defaultJunkPacketMaxSize;
}
if (config.initPacketJunkSize.isEmpty()) {
config.initPacketJunkSize = protocols::awg::defaultInitPacketJunkSize;
}
if (config.responsePacketJunkSize.isEmpty()) {
config.responsePacketJunkSize = protocols::awg::defaultResponsePacketJunkSize;
}
if (config.protocolVersion == protocols::awg::awgV2) {
if (config.cookieReplyPacketJunkSize.isEmpty()) {
config.cookieReplyPacketJunkSize = protocols::awg::defaultCookieReplyPacketJunkSize;
}
if (config.transportPacketJunkSize.isEmpty()) {
config.transportPacketJunkSize = protocols::awg::defaultTransportPacketJunkSize;
}
}
if (config.initPacketMagicHeader.isEmpty()) {
config.initPacketMagicHeader = protocols::awg::defaultInitPacketMagicHeader;
}
if (config.responsePacketMagicHeader.isEmpty()) {
config.responsePacketMagicHeader = protocols::awg::defaultResponsePacketMagicHeader;
}
if (config.underloadPacketMagicHeader.isEmpty()) {
config.underloadPacketMagicHeader = protocols::awg::defaultUnderloadPacketMagicHeader;
}
if (config.transportPacketMagicHeader.isEmpty()) {
config.transportPacketMagicHeader = protocols::awg::defaultTransportPacketMagicHeader;
}
if (config.specialJunk1.isEmpty()) {
config.specialJunk1 = protocols::awg::defaultSpecialJunk1;
}
if (config.specialJunk2.isEmpty()) {
config.specialJunk2 = protocols::awg::defaultSpecialJunk2;
}
if (config.specialJunk3.isEmpty()) {
config.specialJunk3 = protocols::awg::defaultSpecialJunk3;
}
if (config.specialJunk4.isEmpty()) {
config.specialJunk4 = protocols::awg::defaultSpecialJunk4;
}
if (config.specialJunk5.isEmpty()) {
config.specialJunk5 = protocols::awg::defaultSpecialJunk5;
}
return m_protocolConfig.serverConfig.protocolVersion;
}
void AwgConfigModel::applyDefaultsToClientConfig(amnezia::AwgClientConfig& config)
@@ -197,46 +181,6 @@ void AwgConfigModel::applyDefaultsToClientConfig(amnezia::AwgClientConfig& confi
if (config.mtu.isEmpty()) {
config.mtu = protocols::awg::defaultMtu;
}
if (config.junkPacketCount.isEmpty()) {
config.junkPacketCount = m_protocolConfig.serverConfig.junkPacketCount.isEmpty()
? protocols::awg::defaultJunkPacketCount
: m_protocolConfig.serverConfig.junkPacketCount;
}
if (config.junkPacketMinSize.isEmpty()) {
config.junkPacketMinSize = m_protocolConfig.serverConfig.junkPacketMinSize.isEmpty()
? protocols::awg::defaultJunkPacketMinSize
: m_protocolConfig.serverConfig.junkPacketMinSize;
}
if (config.junkPacketMaxSize.isEmpty()) {
config.junkPacketMaxSize = m_protocolConfig.serverConfig.junkPacketMaxSize.isEmpty()
? protocols::awg::defaultJunkPacketMaxSize
: m_protocolConfig.serverConfig.junkPacketMaxSize;
}
if (config.specialJunk1.isEmpty()) {
config.specialJunk1 = m_protocolConfig.serverConfig.specialJunk1.isEmpty()
? protocols::awg::defaultSpecialJunk1
: m_protocolConfig.serverConfig.specialJunk1;
}
if (config.specialJunk2.isEmpty()) {
config.specialJunk2 = m_protocolConfig.serverConfig.specialJunk2.isEmpty()
? protocols::awg::defaultSpecialJunk2
: m_protocolConfig.serverConfig.specialJunk2;
}
if (config.specialJunk3.isEmpty()) {
config.specialJunk3 = m_protocolConfig.serverConfig.specialJunk3.isEmpty()
? protocols::awg::defaultSpecialJunk3
: m_protocolConfig.serverConfig.specialJunk3;
}
if (config.specialJunk4.isEmpty()) {
config.specialJunk4 = m_protocolConfig.serverConfig.specialJunk4.isEmpty()
? protocols::awg::defaultSpecialJunk4
: m_protocolConfig.serverConfig.specialJunk4;
}
if (config.specialJunk5.isEmpty()) {
config.specialJunk5 = m_protocolConfig.serverConfig.specialJunk5.isEmpty()
? protocols::awg::defaultSpecialJunk5
: m_protocolConfig.serverConfig.specialJunk5;
}
}
amnezia::AwgProtocolConfig AwgConfigModel::getProtocolConfig()
@@ -247,21 +191,6 @@ amnezia::AwgProtocolConfig AwgConfigModel::getProtocolConfig()
m_protocolConfig.clearClientConfig();
}
if (m_protocolConfig.serverConfig.protocolVersion.isEmpty() ||
m_protocolConfig.serverConfig.protocolVersion != protocols::awg::awgV2) {
bool hasSpecialJunk = !m_protocolConfig.serverConfig.specialJunk1.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk2.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk3.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk4.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk5.trimmed().isEmpty();
if (hasSpecialJunk) {
m_protocolConfig.serverConfig.protocolVersion = protocols::awg::awgV1_5;
} else if (m_protocolConfig.serverConfig.protocolVersion.isEmpty()) {
m_protocolConfig.serverConfig.protocolVersion = QString();
}
}
return m_protocolConfig;
}
@@ -296,6 +225,13 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
roles[ClientSpecialJunk3Role] = "clientSpecialJunk3";
roles[ClientSpecialJunk4Role] = "clientSpecialJunk4";
roles[ClientSpecialJunk5Role] = "clientSpecialJunk5";
roles[ClientContentPaddingAdditionRole] = "clientContentPaddingAddition";
roles[ClientRekeyAfterTimeRole] = "clientRekeyAfterTime";
roles[ClientRekeyTimeoutRole] = "clientRekeyTimeout";
roles[ClientRejectAfterTimeRole] = "clientRejectAfterTime";
roles[ClientKeepaliveTimeoutRole] = "clientKeepaliveTimeout";
roles[ClientMaxHandshakeAttemptsRole] = "clientMaxHandshakeAttempts";
roles[ClientHeaderProtectionEnabledRole] = "clientHeaderProtectionEnabled";
roles[ServerJunkPacketCountRole] = "serverJunkPacketCount";
roles[ServerJunkPacketMinSizeRole] = "serverJunkPacketMinSize";
@@ -315,7 +251,16 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
roles[ServerSpecialJunk4Role] = "serverSpecialJunk4";
roles[ServerSpecialJunk5Role] = "serverSpecialJunk5";
roles[ServerContentPaddingAdditionRole] = "serverContentPaddingAddition";
roles[ServerRekeyAfterTimeRole] = "serverRekeyAfterTime";
roles[ServerRekeyTimeoutRole] = "serverRekeyTimeout";
roles[ServerRejectAfterTimeRole] = "serverRejectAfterTime";
roles[ServerKeepaliveTimeoutRole] = "serverKeepaliveTimeout";
roles[ServerMaxHandshakeAttemptsRole] = "serverMaxHandshakeAttempts";
roles[ServerHeaderProtectionEnabledRole] = "serverHeaderProtectionEnabled";
roles[IsAwg2Role] = "isAwg2";
roles[IsAwg3Role] = "isAwg3";
return roles;
}

View File

@@ -26,6 +26,13 @@ public:
ClientSpecialJunk3Role,
ClientSpecialJunk4Role,
ClientSpecialJunk5Role,
ClientContentPaddingAdditionRole,
ClientRekeyAfterTimeRole,
ClientRekeyTimeoutRole,
ClientRejectAfterTimeRole,
ClientKeepaliveTimeoutRole,
ClientMaxHandshakeAttemptsRole,
ClientHeaderProtectionEnabledRole,
ServerJunkPacketCountRole,
ServerJunkPacketMinSizeRole,
@@ -45,7 +52,16 @@ public:
ServerSpecialJunk4Role,
ServerSpecialJunk5Role,
IsAwg2Role
ServerContentPaddingAdditionRole,
ServerRekeyAfterTimeRole,
ServerRekeyTimeoutRole,
ServerRejectAfterTimeRole,
ServerKeepaliveTimeoutRole,
ServerMaxHandshakeAttemptsRole,
ServerHeaderProtectionEnabledRole,
IsAwg2Role,
IsAwg3Role
};
explicit AwgConfigModel(QObject *parent = nullptr);
@@ -71,7 +87,7 @@ private:
amnezia::AwgProtocolConfig m_protocolConfig;
amnezia::AwgProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::AwgServerConfig& config);
QString serverProtocolVersion() const;
void applyDefaultsToClientConfig(amnezia::AwgClientConfig& config);
};

View File

@@ -71,8 +71,6 @@ void WireGuardConfigModel::updateModel(amnezia::DockerContainer container, const
m_protocolConfig = protocolConfig;
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
if (!m_protocolConfig.clientConfig.has_value()) {
m_protocolConfig.clientConfig = amnezia::WireGuardClientConfig{};
}
@@ -83,20 +81,6 @@ void WireGuardConfigModel::updateModel(amnezia::DockerContainer container, const
endResetModel();
}
void WireGuardConfigModel::applyDefaultsToServerConfig(amnezia::WireGuardServerConfig& config)
{
if (config.subnetAddress.isEmpty()) {
config.subnetAddress = protocols::wireguard::defaultSubnetAddress;
}
if (config.port.isEmpty()) {
config.port = protocols::wireguard::defaultPort;
}
if (config.transportProto.isEmpty()) {
config.transportProto = ProtocolUtils::transportProtoToString(
ProtocolUtils::defaultTransportProto(amnezia::Proto::WireGuard), amnezia::Proto::WireGuard);
}
}
void WireGuardConfigModel::applyDefaultsToClientConfig(amnezia::WireGuardClientConfig& config)
{
if (config.mtu.isEmpty()) {

View File

@@ -40,7 +40,6 @@ private:
amnezia::WireGuardProtocolConfig m_protocolConfig;
amnezia::WireGuardProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::WireGuardServerConfig& config);
void applyDefaultsToClientConfig(amnezia::WireGuardClientConfig& config);
};

Some files were not shown because too many files have changed in this diff Show More