From b83e74427efdf2af59456212ea08e0c6130af127 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Thu, 15 Aug 2024 19:51:49 +0300 Subject: [PATCH 01/90] Update TAP-Windows driver --- client/3rd-prebuilt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index c38a587f..ba580dc5 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit c38a587fcda89bab4009560d36239fa8de74705e +Subproject commit ba580dc5bd7784f7b1e110ff0365f3286e549a61 From 51618fb8824ab68e7bf476e81042b3c9af976271 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 27 Aug 2024 13:14:15 +0300 Subject: [PATCH 02/90] fixed a possible unhandled exception --- client/secure_qsettings.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index 1e2a2273..88c0242b 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -174,13 +174,25 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json) QByteArray SecureQSettings::encryptText(const QByteArray &value) const { QSimpleCrypto::QBlockCipher cipher; - return cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); + QByteArray result; + try { + result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); + } catch (...) { // todo change error handling in QSimpleCrypto? + qCritical() << "error when encrypting the settings value"; + } + return result; } QByteArray SecureQSettings::decryptText(const QByteArray &ba) const { QSimpleCrypto::QBlockCipher cipher; - return cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); + QByteArray result; + try { + result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); + } catch (...) { // todo change error handling in QSimpleCrypto? + qCritical() << "error when decrypting the settings value"; + } + return result; } bool SecureQSettings::encryptionRequired() const From 558f613accfed1de0f6994a4cf27486882f01bd8 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 30 Aug 2024 16:19:11 +0400 Subject: [PATCH 03/90] feature: added 'copy mail' button on about page --- client/ui/qml/Pages2/PageSettingsAbout.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index 1e38a539..cde9ee20 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -120,7 +120,7 @@ PageType { id: mailButton Layout.fillWidth: true - text: qsTr("Mail") + text: qsTr("support@amnezia.org") descriptionText: qsTr("For reviews and bug reports") leftImageSource: "qrc:/images/controls/mail.svg" @@ -128,6 +128,8 @@ PageType { parentFlickable: fl clickedFunction: function() { + GC.copyToClipBoard(text) + PageController.showNotificationMessage(qsTr("Copied")) } } From e94fc688ba3b7222bc336b6264f373b56ed2f3d3 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 30 Aug 2024 16:31:03 +0400 Subject: [PATCH 04/90] chore: set screenshotsEnabled to true by default --- client/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/settings.h b/client/settings.h index ee10c3b8..55a3d057 100644 --- a/client/settings.h +++ b/client/settings.h @@ -183,7 +183,7 @@ public: bool isScreenshotsEnabled() const { - return value("Conf/screenshotsEnabled", false).toBool(); + return value("Conf/screenshotsEnabled", true).toBool(); } void setScreenshotsEnabled(bool enabled) { From b39a0a1d948d99a3c2cb3b728044ba596d492594 Mon Sep 17 00:00:00 2001 From: Shehab Ahmed Date: Fri, 30 Aug 2024 15:53:48 +0300 Subject: [PATCH 05/90] fix start Minimized feature issue on linux, Closes #1016 (#1021) fix start Minized feature issue on linux --- client/ui/controllers/pageController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index b9561600..3e5e5cc3 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -131,7 +131,7 @@ void PageController::showOnStartup() if (!m_settings->isStartMinimized()) { emit raiseMainWindow(); } else { -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) emit hideMainWindow(); #elif defined Q_OS_MACX setDockIconVisible(false); From 248f487d4e35ad88fc4b19bd61c7b0fd6a137f21 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Tue, 3 Sep 2024 12:03:42 +0300 Subject: [PATCH 06/90] Update amneziavpn_fa_IR.ts (#1005) Persian language updated --- client/translations/amneziavpn_fa_IR.ts | 284 ++++++++++++------------ 1 file changed, 144 insertions(+), 140 deletions(-) diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts index 5b2c5818..26c4f810 100644 --- a/client/translations/amneziavpn_fa_IR.ts +++ b/client/translations/amneziavpn_fa_IR.ts @@ -6,47 +6,47 @@ Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیوها، از VPN کلاسیک استفاده کنید. این VPN برای هر سایتی کار می‌کند و سرعت آن تا %1 مگابیت بر ثانیه است. VPN to access blocked sites in regions with high levels of Internet censorship. - + وی پی ان برای دسترسی به سایت‌های مسدود شده در مناطق با سانسور شدید اینترنت. Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + امنزیا پریمیوم - یک وی پی ان کلاسیک برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیو با کیفیت بالا. قابل استفاده برای تمامی سایت‌ها، حتی در کشورهایی با بالاترین سطح سانسور اینترنت. Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + امنزیا رایگان یک وی پی ان رایگان برای دور زدن مسدودیت‌ها در کشورهایی با سطح بالای سانسور اینترنت است. %1 MBit/s - + %1 MBit/s %1 days - + %1 روز VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + وی پی ان فقط سایت‌های محبوبی را که در منطقه شما مسدود شده‌اند، مانند اینستاگرام، فیسبوک، توییتر و غیره باز می‌کند. سایر سایت‌ها با آدرس آی‌پی واقعی شما باز خواهند شد. <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> Free - + رایگان %1 $/month - + %1 $/ماه @@ -54,22 +54,22 @@ Application added: %1 - + برنامه اضافه شد: %1 The application has already been added - + برنامه از قبل اضافه شده است The selected applications have been added - + برنامه‌های انتخاب شده اضافه شدند Application removed: %1 - + برنامه حذف شد: %1 @@ -77,7 +77,7 @@ Unable to disconnect during configuration preparation - + در هنگام آماده‌سازی پیکربندی، نمی‌توان از اتصال خارج شد. @@ -102,7 +102,7 @@ Preparing... - + در حال آماده‌سازی... @@ -118,12 +118,12 @@ The selected protocol is not supported on the current platform - پروتکل انتخاب شده بر روی این پلتفرم پشتیبانی نمی‎‎شود + پروتکل انتخاب‌شده در پلتفرم فعلی پشتیبانی نمی‌شود. unable to create configuration - + نمی‌توان پیکربندی را ایجاد کرد. @@ -256,13 +256,13 @@ Can't be disabled for current server Unable to open file - + نمی‌توان فایل را باز کرد. Invalid configuration file - + فایل پیکربندی نامعتبر است. @@ -272,7 +272,7 @@ Can't be disabled for current server In the imported configuration, potentially dangerous lines were found: - + در پیکربندی وارد شده، خطوطی که ممکن است خطرناک باشند، یافت شدند: @@ -329,12 +329,12 @@ Already installed containers were found on the server. All installed containers Api config removed - + پیکربندی API حذف شد. %1 cached profile cleared - + %1 پروفایل ذخیره شده پاک شد. @@ -349,17 +349,17 @@ Already installed containers were found on the server. All installed containers %1 installed successfully. - + %1 با موفقیت نصب شد. API config reloaded - + پیکربندی API دوباره بارگذاری شد. Successfully changed the country of connection to %1 - + کشور اتصال با موفقیت به %1 تغییر یافت. @@ -367,17 +367,17 @@ Already installed containers were found on the server. All installed containers Choose application - + انتخاب برنامه application name - + نام برنامه Add selected - + اضافه کردن انتخاب شده @@ -453,7 +453,7 @@ Already installed containers were found on the server. All installed containers Logging enabled - + لاگ‌برداری فعال شد @@ -589,7 +589,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -623,7 +623,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -794,7 +794,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. Remove OpenVPN @@ -895,7 +895,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -903,22 +903,22 @@ Already installed containers were found on the server. All installed containers WG settings - + تنظیمات WG Port - پورت + پورت MTU - + Unable change settings while there is an active connection - + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. All users with whom you shared a connection will no longer be able to connect to it. @@ -939,22 +939,22 @@ Already installed containers were found on the server. All installed containers XRay settings - + تنظیمات XRay Disguised as traffic from - پنهان کردن به عنوان ترافیک از + به‌عنوان ترافیک از طرف زیر نمایش داده می‌شود Save - ذخیره + ذخیره Unable change settings while there is an active connection - + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -1001,7 +1001,7 @@ Already installed containers were found on the server. All installed containers Cannot remove AmneziaDNS from running server - + نمی‌توان AmneziaDNS را از سرور در حال اجرا حذف کرد. @@ -1093,18 +1093,18 @@ Already installed containers were found on the server. All installed containers Settings updated successfully - + تنظیمات با موفقیت به‌روزرسانی شد. SOCKS5 settings - + تنظیمات SOCKS5 Host - هاست + هاستمیزبان @@ -1112,50 +1112,50 @@ Already installed containers were found on the server. All installed containers Copied - کپی شد + کپی شد Port - پورت + پورت User name - نام کاربری + نام کاربری Password - رمز عبور + رمز عبور Username - + نام کاربری Change connection settings - + تغییر تنظیمات اتصال The port must be in the range of 1 to 65535 - + پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد Password cannot be empty - + رمز عبور نمی‌تواند خالی باشد Username cannot be empty - + نام کاربری نمی‌تواند خالی باشد @@ -1285,7 +1285,7 @@ Already installed containers were found on the server. All installed containers https://t.me/amnezia_vpn_en - https://t.me/amnezia_vpn + https://t.me/amnezia_vpn_ir @@ -1337,22 +1337,22 @@ Already installed containers were found on the server. All installed containers For the region - + برای منطقه Price - + قیمت Work period - + مدت زمان کار Speed - + سرعت @@ -1362,49 +1362,49 @@ Already installed containers were found on the server. All installed containers Copied - کپی شد + کپی شد Reload API config - + بارگذاری مجدد پیکربندی API Reload API config? - + آیا می‌خواهید پیکربندی API را دوباره بارگذاری کنید؟ Continue - + ادامه دهید Cancel - کنسل + لغو Cannot reload API config during active connection - + نمی‌توان پیکربندی API را در حین اتصال فعال دوباره بارگذاری کرد. Remove from application - + حذف از برنامه Remove from application? - + آیا می‌خواهید از برنامه حذف کنید؟ Cannot remove server during active connection - + نمی‌توان سرور را در حین اتصال فعال حذف کرد. @@ -1412,57 +1412,57 @@ Already installed containers were found on the server. All installed containers Cannot change split tunneling settings during active connection - نمی توان تنظیمات تونل تقسیم را در طول اتصال فعال تغییر داد + نمی توان تنظیمات تونل تقسیم را در طول اتصال فعال تغییر دادنمی‌توان تنظیمات تقسیم تونلینگ را در حین اتصال فعال تغییر داد. Only the apps from the list should have access via VPN - + فقط برنامه‌های موجود در لیست باید از طریق VPN دسترسی داشته باشند. Apps from the list should not have access via VPN - + برنامه‌های موجود در لیست نباید از طریق VPN دسترسی داشته باشند. App split tunneling - + تقسیم تونلینگ برنامه‌ها Mode - حالت + حالت Remove - + حذف Continue - + ادامه دهید Cancel - کنسل + کنسل application name - + نام برنامه Open executable file - + فایل اجرایی را باز کنید Executable files (*.*) - + فایل‌های اجرایی (*.*) @@ -1480,12 +1480,12 @@ Already installed containers were found on the server. All installed containers Enable notifications - + فعال کردن اعلان‌ها Enable notifications to show the VPN state in the status bar - + اعلان ها را فعال کنید تا وضعیت VPN را در نوار وضعیت ببینید @@ -1565,7 +1565,7 @@ Already installed containers were found on the server. All installed containers Cannot reset settings during active connection - + نمی‌توان تنظیمات را در حین اتصال فعال بازنشانی کرد. @@ -1644,7 +1644,7 @@ Already installed containers were found on the server. All installed containers Cannot restore backup settings during active connection - + نمی‌توان تنظیمات پشتیبان را در حین اتصال فعال بازیابی کرد. @@ -1682,17 +1682,17 @@ Already installed containers were found on the server. All installed containers KillSwitch - + KillSwitch Disables your internet if your encrypted VPN connection drops out for any reason. - + اگر به هر دلیلی اتصال VPN رمزگذاری شده شما قطع شود، اینترنت شما را غیرفعال می‌کند. Cannot change killSwitch settings during active connection - + نمی‌توان تنظیمات Kill Switch را در حین اتصال فعال تغییر داد. @@ -1778,7 +1778,7 @@ Already installed containers were found on the server. All installed containers Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + ثبت وقایع فعال است. توجه داشته باشید که ثبت وقایع به‌طور خودکار پس از ۱۴ روز غیرفعال شده و تمام فایل‌های ثبت وقایع حذف خواهند شد. @@ -1919,7 +1919,7 @@ Already installed containers were found on the server. All installed containers Cannot reboot server during active connection - + نمی‌توان سرور را در حین اتصال فعال راه‌اندازی مجدد کرد. @@ -1929,7 +1929,7 @@ Already installed containers were found on the server. All installed containers Cannot remove server during active connection - + نمی‌توان سرور را در حین اتصال فعال حذف کرد. @@ -1944,7 +1944,7 @@ Already installed containers were found on the server. All installed containers Cannot clear server from Amnezia software during active connection - + نمی‌توان سرور را در حین اتصال فعال از نرم‌افزار Amnezia پاک کرد. @@ -1959,7 +1959,7 @@ Already installed containers were found on the server. All installed containers Cannot reset API config during active connection - + نمی‌توان پیکربندی API را در حین اتصال فعال بازنشانی کرد. @@ -2015,12 +2015,12 @@ Already installed containers were found on the server. All installed containers Clear %1 profile - + پاک کردن پروفایل %1 Clear %1 profile? - + آیا می‌خواهید پروفایل %1 را پاک کنید؟ @@ -2030,7 +2030,7 @@ Already installed containers were found on the server. All installed containers Unable to clear %1 profile while there is an active connection - + نمی‌توان پروفایل %1 را در حین اتصال فعال پاک کرد. @@ -2050,7 +2050,7 @@ Already installed containers were found on the server. All installed containers Cannot remove active container - + نمی‌توان کانتینر فعال را حذف کرد. @@ -2179,32 +2179,32 @@ Already installed containers were found on the server. All installed containers For the region - + برای منطقه Price - + قیمت Work period - + مدت زمان کار Speed - + سرعت Features - + ویژگی‌ها Connect - اتصال + اتصال @@ -2212,12 +2212,12 @@ Already installed containers were found on the server. All installed containers VPN by Amnezia - + VPN توسط Amnezia Choose a VPN service that suits your needs. - + یک سرویس VPN که مناسب نیازهای شما باشد را انتخاب کنید. @@ -2254,67 +2254,67 @@ It's okay as long as it's from someone you trust. Connection - ارتباط + ارتباط Insert the key, add a configuration file or scan the QR-code - + کلید را وارد کنید، فایل پیکربندی را اضافه کنید یا کد QR را اسکن کنید Insert key - + کلید را وارد کنید Insert - وارد کردن + وارد کردن Continue - + ادامه دهید Other connection options - + گزینه‌های اتصال دیگر VPN by Amnezia - + VPN توسط Amnezia Connect to classic paid and free VPN services from Amnezia - + اتصال به سرویس‌های VPN کلاسیک پولی و رایگان از Amnezia Self-hosted VPN - + Self-hosted VPN Configure Amnezia VPN on your own server - + پیکربندی VPN Amnezia بر روی سرور خودتان Restore from backup - بازیابی از پشتیبان + بازیابی از پشتیبان Open backup file - باز کردن فایل پشتیبان + باز کردن فایل پشتیبان Backup files (*.backup) - Backup files (*.backup) + Backup files (*.backup) @@ -2329,7 +2329,7 @@ It's okay as long as it's from someone you trust. I have nothing - من هیچی ندارم + من هیچی ندارم Key as text @@ -2381,12 +2381,12 @@ It's okay as long as it's from someone you trust. How to run your VPN server - + چگونه سرور VPN خود را اجرا کنید Where to get connection data, step-by-step instructions for buying a VPS - + داده‌های اتصال را از کجا دریافت کنید و دستورالعمل‌های مرحله به مرحله برای خرید یک VPS @@ -2501,7 +2501,7 @@ It's okay as long as it's from someone you trust. The port must be in the range of 1 to 65535 - + پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد @@ -2550,7 +2550,7 @@ It's okay as long as it's from someone you trust. Let's get started - + بیایید شروع کنیم @@ -2601,7 +2601,7 @@ It's okay as long as it's from someone you trust. Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. - + فعال‌سازی استتار WireGuard. این ممکن است مفید باشد اگر WireGuard توسط ارائه‌دهنده شما مسدود شده باشد. @@ -2680,7 +2680,7 @@ It's okay as long as it's from someone you trust. Save XRay config - + ذخیره پیکربندی XRay @@ -2705,7 +2705,7 @@ It's okay as long as it's from someone you trust. XRay native format - + فرمت بومی XRay @@ -2741,22 +2741,22 @@ It's okay as long as it's from someone you trust. Creation date: %1 - + تاریخ ایجاد: %1 Latest handshake: %1 - + آخرین ارتباط: %1 Data received: %1 - + داده‌های دریافت شده: %1 Data sent: %1 - + داده‌های ارسال شده: %1 Creation date: @@ -2882,12 +2882,12 @@ It's okay as long as it's from someone you trust. Logging was disabled after 14 days, log files were deleted - + ثبت وقایع پس از ۱۴ روز غیرفعال شد و فایل‌های ثبت وقایع حذف شدند Settings restored from backup file - + تنظیمات از فایل پشتیبان بازیابی شد @@ -3296,17 +3296,17 @@ It's okay as long as it's from someone you trust. Background service is not running - + Background service is not running Server error: Packet manager error - + Server error: Packet manager error SCP error: Generic failure - + SCP error: Generic failure @@ -3356,17 +3356,17 @@ It's okay as long as it's from someone you trust. In the response from the server, an empty config was received - + در پاسخ از سرور، پیکربندی خالی دریافت شد SSL error occurred - + SSL error occurred Server response timeout on api request - + Server response timeout on api request @@ -3426,12 +3426,12 @@ It's okay as long as it's from someone you trust. XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - + XRay با REALITY - مناسب برای کشورهایی با بالاترین سطح سانسور اینترنت. استتار ترافیک به عنوان ترافیک وب در سطح TLS و حفاظت در برابر شناسایی با روش‌های پروب فعال. IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. - + IKEv2/IPsec - پروتکل مدرن و پایدار، کمی سریع‌تر از سایرین است و پس از قطع شدن سیگنال، اتصال را بازیابی می‌کند. از پشتیبانی بومی در آخرین نسخه‌های Android و iOS برخوردار است. @@ -3504,7 +3504,11 @@ WireGuard به دلیل امضاهای بسته متمایز خود، بسیار It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - + پروتکل REALITY، یک توسعه پیشگامانه توسط خالقان XRay، به‌طور خاص برای مقابله با بالاترین سطح سانسور اینترنتی طراحی شده است و از رویکرد نوآورانه‌ای برای دور زدن محدودیت‌ها استفاده می‌کند. + +REALITY به‌طور منحصربه‌فردی سانسورچیان را در مرحله دست‌دهی TLS شناسایی می‌کند و به‌صورت یکپارچه به‌عنوان پراکسی برای کاربران قانونی عمل می‌کند، در حالی که سانسورچیان را به سایت‌های معتبر مانند google.com هدایت می‌کند و در نتیجه یک گواهی TLS واقعی و داده‌های اصلی ارائه می‌دهد. + +این قابلیت پیشرفته، REALITY را از فناوری‌های مشابه متمایز می‌کند، زیرا می‌تواند ترافیک وب را بدون نیاز به پیکربندی‌های خاص، به‌عنوان ترافیک از سایت‌های تصادفی و معتبر جا بزند. برخلاف پروتکل‌های قدیمی‌تر مانند VMess، VLESS و انتقال XTLS-Vision، تشخیص نوآورانه "دوست یا دشمن" REALITY در مرحله دست‌دهی TLS امنیت را افزایش داده و از شناسایی توسط سیستم‌های پیشرفته DPI که از تکنیک‌های پروب فعال استفاده می‌کنند، جلوگیری می‌کند. این ویژگی REALITY را به یک راه‌حل قوی برای حفظ آزادی اینترنت در محیط‌هایی با سانسور شدید تبدیل می‌کند. @@ -3665,7 +3669,7 @@ For more detailed information, you can SOCKS5 proxy server - + سرور پروکسی SOCKS5 From 88a2b9a07a599f313544d85d7cd08fdf6b497ad8 Mon Sep 17 00:00:00 2001 From: Shehab Ahmed Date: Tue, 3 Sep 2024 12:06:13 +0300 Subject: [PATCH 07/90] Update Arabic, Burmese translation (#1022) Update Arabic and Burmese translation --- client/translations/amneziavpn_ar_EG.ts | 178 +++---- client/translations/amneziavpn_my_MM.ts | 682 ++++++++---------------- 2 files changed, 300 insertions(+), 560 deletions(-) diff --git a/client/translations/amneziavpn_ar_EG.ts b/client/translations/amneziavpn_ar_EG.ts index 42ea2720..be37275f 100644 --- a/client/translations/amneziavpn_ar_EG.ts +++ b/client/translations/amneziavpn_ar_EG.ts @@ -6,47 +6,47 @@ Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + شبكة VPN كلاسيكية للعمل المريح وتنزيل الملفات الكبيرة ومشاهدة مقاطع الفيديو. تعمل مع أي موقع. تصل السرعة إلى %1 ميجابت/ثانية VPN to access blocked sites in regions with high levels of Internet censorship. - + شبكة VPN للولوج للمواقع المحظورة في بلاد ذو مستوي عالي من الرقابة علي الانترنت. Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amenzia Premium - شبكة VPN للعمل المريح, تحميل ملفات كبيرة الحجم, ومشاهدة مقاطع الفيديو ب جودة عالية. تعمل لجميع المواقع, حتي في البلاد ذو مستوي عالي من الرقابة علي الانترنت Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + Amnezia Free هو VPN مجاني لتخطي الحظر في البلاد ذو مستوي عالي من الرقابة علي الانترنت %1 MBit/s - + %1 ميجابت/ثانية %1 days - + %1 ايام VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + سيقوم VPN فقط بفتح المواقع المشهورة المحظورة في بلدك, مثل Instagram, Facebook, Twitter و مواقع اخري. المواقع الاخري ستٌفتح من عنوان ال IP الحقيقي الخاص بك, <a href="%1/free" style="color: #FBB26A;">معلومات اخري علي الموقع.</a> Free - + مجاني %1 $/month - + %1 دولار/الشهر @@ -325,7 +325,7 @@ Already installed containers were found on the server. All installed containers Api config removed - + تم حذف تكوين Api @@ -345,17 +345,17 @@ Already installed containers were found on the server. All installed containers %1 installed successfully. - + تم تحميل %1 بنجاح API config reloaded - + تمت إعادة تحميل تكوين API Successfully changed the country of connection to %1 - + تم تغيير بلد الاتصال بنجاح إلى %1 @@ -441,7 +441,7 @@ Already installed containers were found on the server. All installed containers Gateway endpoint - + نقطة نهاية البوابة @@ -507,47 +507,47 @@ Already installed containers were found on the server. All installed containers Jc - Junk packet count - + Jc - عدد الحزم غير المرغوب فيها Jmin - Junk packet minimum size - + Jmin - الحجم الادني للحزم الغير مرغوب فيها Jmax - Junk packet maximum size - + Jmax - الحجم الاقصي للحزم الغير مرغوب فيها S1 - Init packet junk size - + S1 - حجم حزمة البيانات العشوائية الأولية S2 - Response packet junk size - + S2 - حجم حزمة الاستجابة غير المرغوب فيها H1 - Init packet magic header - + H1 - حزمة رأس سحرية مبدئية H2 - Response packet magic header - + H2 - رأس حزمة الاستجابة السحرية H4 - Transport packet magic header - + H4 - رأس حزمة النقل السحرية H3 - Underload packet magic header - + H3 - رأس حزمة السحر غير المحمل @@ -1164,7 +1164,7 @@ Already installed containers were found on the server. All installed containers Dev console - + وحدة تحكم التطوير @@ -1250,74 +1250,74 @@ Already installed containers were found on the server. All installed containers For the region - + للمنطقة Price - + السعر Work period - + مدة العمل Speed - + السرعة Support tag - + علامة الدعم Copied - + تم النسخ Reload API config - + إعادة تحميل تكوين API Reload API config? - + إعادة تحميل تكوين API Continue - واصل + واصل Cancel - إلغاء + إلغاء Cannot reload API config during active connection - + لا يمكن إعادة تحميل تكوين API اثناء تواجد اتصال نشط Remove from application - + احذف من التطبيق Remove from application? - + احذف من التطبيق؟ Cannot remove server during active connection - لا يمكن إزالة الخادم أثناء الاتصال النشط + لا يمكن إزالة الخادم أثناء الاتصال النشط @@ -1769,7 +1769,7 @@ Already installed containers were found on the server. All installed containers No new installed containers found - لم يتم العثور علي اي حاويات جديدة مٌثبتة + لم يتم العثور علي اي خدمات مٌثبتة سابقاً @@ -1941,7 +1941,7 @@ Already installed containers were found on the server. All installed containers Remove %1 from server? - + احذف %1 من الخادم؟ @@ -2080,32 +2080,32 @@ Already installed containers were found on the server. All installed containers For the region - + للمنطقة Price - + السعر Work period - + مدة العمل Speed - + السرعة Features - + المميزات Connect - اتصل + اتصل @@ -2113,96 +2113,80 @@ Already installed containers were found on the server. All installed containers VPN by Amnezia - + VPN بواسطة Amnezia Choose a VPN service that suits your needs. - + اختر خدمة VPN تلبي احتياجاتك PageSetupWizardConfigSource - - Server connection - اتصال الخادم - - - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - لا تستخدم رموز اتصال من مصادر غير موثوقة, حيث قد يكون تم إنشاؤها لاعتراض بياناتك. - - - What do you have? - ماذا لديك؟ - - - File with connection settings or backup - ملف إعدادات اتصال او نسخ احتياطي - Connection - الاتصال + الاتصال Insert the key, add a configuration file or scan the QR-code - + أدخل المفتاح، أضف ملف تكوين أو امسح رمز الاستجابة السريعة Insert key - + أدخل مفتاح Insert - ادخل + أدخل Continue - واصل + واصل Other connection options - + اختيارات اتصال اخري VPN by Amnezia - + VPN بواسطة Amnezia Connect to classic paid and free VPN services from Amnezia - + اتصل بخدمات VPN الكلاسيكية المدفوعة والمجانية من Amnezia Self-hosted VPN - + VPN ذاتية الاستضافة Configure Amnezia VPN on your own server - + قم بتكوين Amnezia VPN على الخادم الخاص بك Restore from backup - استرجاع من ملف يحتوي علي نسخة احتياطية + استرجاع من ملف يحتوي علي نسخة احتياطية Open backup file - افتح ملف نسخ احتياطي + افتح ملف نسخ احتياطي Backup files (*.backup) - ملفات نٌسخ احتياطية (*.backup) + ملفات نٌسخ احتياطية (*.backup) @@ -2222,11 +2206,7 @@ Already installed containers were found on the server. All installed containers I have nothing - ليس لدي اي شئ - - - Key as text - مفتاح كنص + ليس لدي اي شئ @@ -2269,12 +2249,12 @@ Already installed containers were found on the server. All installed containers How to run your VPN server - + كيف تقوم بتشغيل خادم ال VPN الخاص بك Where to get connection data, step-by-step instructions for buying a VPS - + اين تحصل علي بيانات الاتصال, تعليمات خطوة ب خطوة لشراء VPS @@ -2392,7 +2372,7 @@ Already installed containers were found on the server. All installed containers تثبيت - + The port must be in the range of 1 to 65535 يجب أن يكون المنفذ في النطاق من 1 إلى 65535 @@ -2420,30 +2400,10 @@ Already installed containers were found on the server. All installed containers PageSetupWizardStart - - Settings restored from backup file - تم استرداد الإعدادات من ملف نسخة احتياطية - - - Free service for creating a personal VPN on your server. - خدمة مجانية لأنشاء VPN شخصي علي الخادم الشخصي. - - - Helps you access blocked content without revealing your privacy, even to VPN providers. - يساعدك في الولوج للمحتوي المحظور بدون إظهار خصوصيات, حتي لمزود ال VPN. - - - I have the data to connect - لدي البيانات المطلوبة للأتصال - - - I have nothing - ليس لدي اي شئ - Let's get started - + هيا نبدأ @@ -2777,7 +2737,7 @@ Already installed containers were found on the server. All installed containers Settings restored from backup file - + تم تحميل الإعدادات من ملف نسخة احتياطية @@ -3221,7 +3181,7 @@ Already installed containers were found on the server. All installed containers Missing AGW public key - + مفتاح AGW عام مفقود @@ -3299,7 +3259,7 @@ Already installed containers were found on the server. All installed containers XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - الأشعة السينية مع الواقع - مناسبة للبلدان التي لديها أعلى مستوى من الرقابة على الإنترنت. إخفاء حركة المرور كحركة مرور على الويب على مستوى TLS، والحماية من الكشف عن طريق طرق التحقيق النشطة. + XRay مع REALITY - مناسبة للبلدان التي لديها أعلى مستوى من الرقابة على الإنترنت. إخفاء حركة المرور كحركة مرور على الويب على مستوى TLS، والحماية من الكشف عن طريق طرق التحقيق النشطة. diff --git a/client/translations/amneziavpn_my_MM.ts b/client/translations/amneziavpn_my_MM.ts index 0a71b0a5..473c97f8 100644 --- a/client/translations/amneziavpn_my_MM.ts +++ b/client/translations/amneziavpn_my_MM.ts @@ -6,47 +6,47 @@ Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ မည်သည့်ဆိုက်များအတွက်မဆိုအလုပ်လုပ်ပြီး လိုင်းအရှိန် %1 MBit/s အထိအသုံးပြုနိုင်ပါတယ်။ VPN to access blocked sites in regions with high levels of Internet censorship. - + အင်တာနက် ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသော ဒေသများရှိ ပိတ်ဆို့ထားသော ဆိုက်များကို ဝင်ရောက်ရန် VPN။. Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Premium - သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကိုကြည်လင်ပြတ်သားစွာကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ အင်တာနက်ဆင်ဆာဖြတ်မှု အဆင့်အမြင့်ဆုံးနိုင်ငံများတွင်ပင် မည်သည့်ဆိုက်များအတွက်မဆို အလုပ်လုပ်ပါသည်။. Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + Amnezia Free သည် အင်တာနက်ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသောနိုင်ငံများတွင် ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်ရန်အတွက် အခမဲ့ VPN တစ်ခုဖြစ်ပါသည်။ %1 MBit/s - + %1 MBit/s %1 days - + %1 ရက် VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + ဤ VPN သည် သင့်ဒေသရှိ Instagram၊ Facebook၊ Twitter နှင့် အခြားသော လူကြိုက်များသော ဆိုက်များကိုသာ ဖွင့်ပေးပါမည်။ အခြားဝဘ်ဆိုက်များကိုမူ သင်၏ IP လိပ်စာအစစ်အမှန်ဖြင့်သာ ဖွင့်ပေးပါမည်၊ <a href="%1/free" style="color: #FBB26A;">နောက်ထပ်အသေးစိတ်အချက်အလက်များကို ဝဘ်ဆိုဒ်ပေါ်တွင်ကြည့်ရန်</a> Free - + အခမဲ့ %1 $/month - + %1 $/တစ်လ @@ -54,22 +54,22 @@ Application added: %1 - + အပလီကေးရှင်းထည့်ပြီးပါပြီ: %1 The application has already been added - + အပလီကေးရှင်းကို ထည့်သွင်းသားဖြစ်သည် The selected applications have been added - + ရွေးချယ်ထားသောအပလီကေးရှင်းများကို ထည့်သွင်းပြီးပါပြီ Application removed: %1 - + အပလီကေးရှင်းကို ဖယ်ရှားလိုက်သည်: %1 @@ -77,7 +77,7 @@ Unable to disconnect during configuration preparation - + Configuration ပြင်ဆင်ခြင်းလုပ်ဆောင်နေချိန်အတွင်း ချိတ်ဆက်မှုဖြတ်တောက်၍မရပါ @@ -102,7 +102,7 @@ Preparing... - + ပြင်ဆင်နေသည်... @@ -112,17 +112,17 @@ Settings updated successfully - ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ + ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ The selected protocol is not supported on the current platform - ရွေးချယ်ထားသော ပရိုတိုကောကို လက်ရှိပလက်ဖောင်းပေါ်တွင် အ‌ထောက်အပံ့မပေးထားပါ။ + ရွေးချယ်ထားသော ပရိုတိုကောကို လက်ရှိပလက်ဖောင်းပေါ်တွင် အ‌ထောက်အပံ့မပေးထားပါ unable to create configuration - + configuration ဖန်တီး၍မရပါ @@ -148,17 +148,17 @@ Add new connection - ချိတ်ဆက်မှုအသစ်ထည့်သွင်းပါ။ + ချိတ်ဆက်မှုအသစ်ထည့်သွင်းမည် Configure your server - သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိပါ။ + သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိမည် Open config file, key or QR code - config ဖိုင်၊ key သို့မဟုတ် QR ကုဒ်ကို ဖွင့်ပါ။ + config ဖိုင်၊ key သို့မဟုတ် QR ကုဒ်ကို ဖွင့်မည် @@ -199,10 +199,6 @@ Unable change protocol while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ပရိုတိုကောကို ပြောင်းလဲ၍မရပါ။ - - The selected protocol is not supported on the current platform - ရွေးချယ်ထားသော ပရိုတိုကောကို လက်ရှိပလက်ဖောင်းပေါ်တွင် အ‌ထောက်အပံ့မပေးထားပါ။ - HomeSplitTunnelingDrawer @@ -256,13 +252,13 @@ Can't be disabled for current server Unable to open file - + ဖိုင်ကိုဖွင့်၍မရပါ Invalid configuration file - + Configuration ဖိုင် မမှန်ကန်ပါ @@ -272,7 +268,7 @@ Can't be disabled for current server In the imported configuration, potentially dangerous lines were found: - + တင်သွင်းသည့် configuration တွင်၊ အန္တရာယ်ရှိနိုင်သည့်စာလိုင်းများကို တွေ့ရှိခဲ့သည်: @@ -329,37 +325,37 @@ Already installed containers were found on the server. All installed containers Api config removed - + Api config ကိုဖယ်ရှားလိုက်သည် %1 cached profile cleared - + ကက်ရှ်လုပ်ထားတဲ့ ပရိုဖိုင် %1 ခုကို ရှင်းပြီးပါပြီ Please login as the user - အသုံးပြုသူအဖြစ် log in ဝင်ရောက်ပါ။ + အသုံးပြုသူအဖြစ် log in ဝင်ရောက်ပါ Server added successfully - ဆာဗာကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ။ + ဆာဗာကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ %1 installed successfully. - + %1 ခုကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ. API config reloaded - + API config ကို ပြန်လည်စတင်လိုက်ပါပြီ Successfully changed the country of connection to %1 - + ချိတ်ဆက်မှုနိုင်ငံကို %1 သို့ အောင်မြင်စွာ ပြောင်းလဲလိုက်ပါပြီ @@ -367,17 +363,17 @@ Already installed containers were found on the server. All installed containers Choose application - + အပလီကေးရှင်းရွေးမည် application name - + အပလီကေးရှင်းအမည် Add selected - + ရွေးချယ်ထားသည်များကိုထည့်မည် @@ -390,7 +386,7 @@ Already installed containers were found on the server. All installed containers Write key failed: %1 - key ရေးမှု မအောင်မြင်ပါ: %1 + key ရေးသားမှု မအောင်မြင်ပါ: %1 @@ -437,7 +433,7 @@ Already installed containers were found on the server. All installed containers Usually it takes no more than 5 minutes - များသောအားဖြင့် 5 မိနစ်ထက်မပိုပါ။ + များသောအားဖြင့် 5 မိနစ်ထက်ပိုမကြာပါ @@ -445,7 +441,7 @@ Already installed containers were found on the server. All installed containers Gateway endpoint - + Gateway အဆုံးမှတ် @@ -453,17 +449,17 @@ Already installed containers were found on the server. All installed containers Logging enabled - + Logging ဖွင့်ထားပါသည် Split tunneling enabled - split tunnelling ဖွင့်ထားပါသည်။ + split tunnelling ဖွင့်ထားပါသည် Split tunneling disabled - split tunnelling ပိတ်ထားပါသည်။ + split tunnelling ပိတ်ထားပါသည် @@ -478,7 +474,7 @@ Already installed containers were found on the server. All installed containers Unable change server while there is an active connection - လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ။ + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ @@ -496,20 +492,12 @@ Already installed containers were found on the server. All installed containers MTU - - - - Remove AmneziaWG - AmneziaWG ကို ဖယ်ရှားမည်။ - - - Remove AmneziaWG from server? - AmneziaWG ကို ဆာဗာမှ ဖယ်ရှားမည်လား? + MTU All users with whom you shared a connection with will no longer be able to connect to it. - သင့်တွင် သင့်ကိုမည်သည့် ချိတ်ဆက်ထားသော အသုံးပြုသူများသည် အကြောင်းအရာသို့ ဆက်သွယ်ရန် မရနိုင်ပါ။ + သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. @@ -519,62 +507,62 @@ Already installed containers were found on the server. All installed containers Jc - Junk packet count - + Jc - Junk packet အရေအတွက် Jmin - Junk packet minimum size - + Jmin - Junk packet အသေးငယ်ဆုံးလက်ခံနိုင်မှုအရွယ်အစား Jmax - Junk packet maximum size - + Jmax - Junk packet အကြီးဆုံးလက်ခံနိုင်မှုအရွယ်အစား S1 - Init packet junk size - + S1 - Init packet junk အရွယ်အစား S2 - Response packet junk size - + S2 - Response packet junk အရွယ်အစား H1 - Init packet magic header - + H1 - Init packet magic header H2 - Response packet magic header - + H2 - Response packet magic header H4 - Transport packet magic header - + H4 - Transport packet magic header H3 - Underload packet magic header - + H3 - Underload packet magic header The values of the H1-H4 fields must be unique - + H1-H4 အကွက်များ၏ တန်ဖိုးများသည် အခြားတန်ဖိုးများနှင့်မတူ တမူထူးခြားနေရပါမည် The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + အကွက် S1 + မက်ဆေ့ချ် စတင်ခြင်း အရွယ်အစား (148) ၏ တန်ဖိုးသည် S2 + မက်ဆေ့ချ် တုံ့ပြန်မှု အရွယ်အစား (92) နှင့် မညီမျှရပါ Save settings? - သိမ်းဆည်းမည်လား။ + ဆက်တင်များကို သိမ်းဆည်းမည်လား? @@ -589,7 +577,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -623,7 +611,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -794,27 +782,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - - - - Remove OpenVPN - AmneziaWG ကို ဖယ်ရှားမည်။ - - - Remove OpenVPN from server? - AmneziaWG ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - - - All users with whom you shared a connection with will no longer be able to connect to it. - သင့်တွင် သင့်ကိုမည်သည့် ချိတ်ဆက်ထားသော အသုံးပြုသူများသည် အကြောင်းအရာသို့ ဆက်သွယ်ရန် မရနိုင်ပါ။ - - - Continue - ဆက်လက်လုပ်ဆောင်မည် - - - Cancel - ပယ်ဖျက်မည် + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -852,11 +820,7 @@ Already installed containers were found on the server. All installed containers All users with whom you shared a connection with will no longer be able to connect to it. - သင့်တွင် သင့်ကိုမည်သည့် ချိတ်ဆက်ထားသော အသုံးပြုသူများသည် အကြောင်းအရာသို့ ဆက်သွယ်ရန် မရနိုင်ပါ။ - - - All users who you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. @@ -895,7 +859,7 @@ Already installed containers were found on the server. All installed containers Unable change settings while there is an active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -903,30 +867,22 @@ Already installed containers were found on the server. All installed containers WG settings - + WG ဆက်တင်များ Port - Port + Port MTU - + MTU Unable change settings while there is an active connection - - - - All users with whom you shared a connection will no longer be able to connect to it. - သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ဤချိတ်ဆက်မှုကိုချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - - - Cancel - ပယ်ဖျက်မည် + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -939,29 +895,22 @@ Already installed containers were found on the server. All installed containers XRay settings - + XRay ဆက်တင်များ Disguised as traffic from - traffic အဖြစ် အသွင်ယူထားသည် + traffic အဖြစ် အသွင်ယူထားသည် Save - သိမ်းဆည်းမည် + သိမ်းဆည်းမည် Unable change settings while there is an active connection - - - - - PageServerContainers - - Continue - Продолжить + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -1001,7 +950,7 @@ Already installed containers were found on the server. All installed containers Cannot remove AmneziaDNS from running server - + AmneziaDNS ကို လည်ပတ်နေသည့်ဆာဗာမှ ဖယ်ရှား၍မရပါ @@ -1071,40 +1020,24 @@ Already installed containers were found on the server. All installed containers Detailed instructions အသေးစိတ်ညွှန်ကြားချက်များ - - Remove SFTP and all data stored there - SFTP ဖယ်ရှားပါ - - - Remove SFTP and all data stored there? - SFTP နှင့် ထိုနေရာတွင် သိမ်းဆည်းထားသည့် ဒေတာအားလုံးကို ဖယ်ရှားမည်လား? - - - Continue - ဆက်လက်လုပ်ဆောင်မည် - - - Cancel - ပယ်ဖျက်မည် - PageServiceSocksProxySettings Settings updated successfully - ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ + ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ SOCKS5 settings - + SOCKS5 ဆက်တင်များ Host - Host + Host @@ -1112,50 +1045,50 @@ Already installed containers were found on the server. All installed containers Copied - ကူးယူပြီးပါပြီ + ကူးယူပြီးပါပြီ Port - Port + Port User name - အသုံးပြုသူနာမည် + အသုံးပြုသူနာမည် Password - စကားဝှက် + စကားဝှက် Username - + အသုံးပြုသူနာမည် Change connection settings - + ချက်ဆက်မှုဆက်တင်များကို ပြောင်းလဲမည် The port must be in the range of 1 to 65535 - + Port သည် 1 မှ 65535 အတွင်း ဖြစ်ရမည် Password cannot be empty - + စကားဝှက် သည် ဗလာမဖြစ်ရပါ Username cannot be empty - + အသုံးပြုသူနာမည် သည် ဗလာမဖြစ်ရပါ @@ -1195,22 +1128,6 @@ Already installed containers were found on the server. All installed containers When configuring WordPress set the this onion address as domain. WordPress ကို ချိန်ညှိသည့်အခါ ဤ onion လိပ်စာကို domain အဖြစ် သတ်မှတ်ပါ. - - Remove website - ဝဘ်ဆိုက်ကိုဖယ်ရှားမည် - - - The site with all data will be removed from the tor network. - ဒေတာအားလုံးပါသည့် ဆိုက်ကို tor ကွန်ရက်မှ ဖယ်ရှားပါမည်. - - - Continue - ဆက်လက်လုပ်ဆောင်မည် - - - Cancel - ပယ်ဖျက်မည် - PageSettings @@ -1247,7 +1164,7 @@ Already installed containers were found on the server. All installed containers Dev console - + ဒက်ဗယ်လော်ပါ console @@ -1265,7 +1182,7 @@ Already installed containers were found on the server. All installed containers Amnezia is a free and open-source application. You can support the developers if you like it. - Amnezia သည် အခမဲ့ဖြစ်ပြီး open-source application တစ်ခုဖြစ်သည်။ သင်နှစ်သက်ပါက developer များကို ပံ့ပိုးနိုင်ပါသည်။ + Amnezia သည် အခမဲ့ open-source application တစ်ခုဖြစ်သည်။ သင်နှစ်သက်ပါက developer များကို ပံ့ပိုးနိုင်ပါသည်. @@ -1312,10 +1229,6 @@ Already installed containers were found on the server. All installed containers Website ဝဘ်ဆိုက် - - https://amnezia.org - https://amnezia.org - Software version: %1 @@ -1337,74 +1250,74 @@ Already installed containers were found on the server. All installed containers For the region - + ဒေသအတွက် Price - + စျေးနှုန်း Work period - + အလုပ်လုပ်မည့်ကာလ Speed - + မြန်နှုန်း Support tag - + ကူညီပံ့ပိုးမှု tag Copied - ကူးယူပြီးပါပြီ + ကူးယူပြီးပါပြီ Reload API config - + API config ကို ပြန်လည်စတင်မည် Reload API config? - + API config ကို ပြန်လည်စတင်မည်လား? Continue - + ဆက်လက်လုပ်ဆောင်မည် Cancel - ပယ်ဖျက်မည် + ပယ်ဖျက်မည် Cannot reload API config during active connection - + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း API config ကို ပြန်လည်စတင်၍မရပါ Remove from application - + အပလီကေးရှင်းမှဖယ်ရှားမည် Remove from application? - + အပလီကေးရှင်းမှဖယ်ရှားမည်လား? Cannot remove server during active connection - + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ဖယ်ရှား၍မရပါ @@ -1412,57 +1325,57 @@ Already installed containers were found on the server. All installed containers Cannot change split tunneling settings during active connection - လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် split tunneling ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် split tunneling ဆက်တင်များကို ပြောင်းလဲ၍မရပါ Only the apps from the list should have access via VPN - + စာရင်းတွင်းပါဝင်သောအက်ပ်များသာလျှင် VPN မှတစ်ဆင့် ဝင်ရောက်ခွင့်ရှိလိမ့်မည်ဖြစ်သည် Apps from the list should not have access via VPN - + စာရင်းတွင်းပါဝင်သောအက်ပ်များကို VPN မှတစ်ဆင့် ဝင်ရောက်ခွင့်ရရှိလိမ့်မည်မဟုတ်ပေ App split tunneling - + App split tunneling Mode - Mode + Mode Remove - ဖယ်ရှားမည် + ဖယ်ရှားမည် Continue - + ဆက်လက်လုပ်ဆောင်မည် Cancel - ပယ်ဖျက်မည် + ပယ်ဖျက်မည် application name - + အပလီကေးရှင်းအမည် Open executable file - + စီမံလုပ်ဆောင်နိုင်မှုဖိုင်ကိုဖွင့်မည် Executable files (*.*) - + စီမံလုပ်ဆောင်နိုင်မှုဖိုင်များ (*.*) @@ -1480,12 +1393,12 @@ Already installed containers were found on the server. All installed containers Enable notifications - + နိုတီများဖွင့်မည် Enable notifications to show the VPN state in the status bar - + စတေးတပ်ဘားတွင် VPN အခြေအနေကိုပြသရန် နိုတီများကို ဖွင့်မည် @@ -1515,7 +1428,7 @@ Already installed containers were found on the server. All installed containers Launch application minimized - အက်ပ်စတင်သည့်အခါ minimized ထားပြီးစတင်မည် + အက်ပ်ဖွင့်သည့်အခါ minimized ထားပြီးဖွင့်မည် @@ -1525,7 +1438,7 @@ Already installed containers were found on the server. All installed containers Logging - လော့ဂ်အင် + Logging @@ -1550,7 +1463,7 @@ Already installed containers were found on the server. All installed containers All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. - ဆက်တင်အားလုံးကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ပါမည်. ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်. + ဆက်တင်အားလုံးကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ပါမည်။ ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်။. @@ -1565,7 +1478,7 @@ Already installed containers were found on the server. All installed containers Cannot reset settings during active connection - + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်၍မရပါ @@ -1588,43 +1501,43 @@ Already installed containers were found on the server. All installed containers The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. - မိတ္တူတွင် AmneziaVPN သို့ ထည့်ထားသော ဆာဗာအားလုံးအတွက် သင့်စကားဝှက်များနှင့် လျှို့ဝှက်သော့များ ပါဝင်ပါမည်။ ဤအချက်အလက်ကို လုံခြုံသောနေရာတွင် ထားပါ။ + အရံဖိုင်တွင် AmneziaVPN သို့ ထည့်ထားသော ဆာဗာအားလုံးအတွက် သင့်စကားဝှက်များနှင့် လျှို့ဝှက်သော့များ ပါဝင်ပါမည်။ ဤအချက်အလက်ကို လုံခြုံသောနေရာတွင် ထားပါ။. Make a backup - အရန်ဖိုင်တစ်ခု ပြုလုပ်မည် + အရံဖိုင်တစ်ခု ပြုလုပ်မည် Save backup file - အရန်ဖိုင်ကို သိမ်းဆည်းမည် + အရံဖိုင်ကို သိမ်းဆည်းမည် Backup files (*.backup) - ဖိုင်များကိုအရန်သိမ်းဆည်းမည် (*.backup) + အရံဖိုင်များ (*.backup) Backup file saved - ဖိုင်များကိုအရန်သိမ်းဆည်းပြီးပါပြီ + အရံဖိုင်ကိုသိမ်းဆည်းပြီးပါပြီ Restore from backup - အရန်သိမ်းထားသည့်ဖိုင်မှ ပြန်လည်ရယူမည် + အရံဖိုင်မှ ပြန်လည်ရယူမည် Open backup file - အရန်သိမ်းထားသည့်ဖိုင်ကို ဖွင့်မည် + အရံဖိုင်ကို ဖွင့်မည် Import settings from a backup file? - ဆက်တင်များကို အရန်ဖိုင်တစ်ခုမှ ပြန်လည်တင်သွင်းမည်လား? + ဆက်တင်များကို အရံဖိုင်တစ်ခုမှ ပြန်လည်တင်သွင်းမည်လား? @@ -1644,7 +1557,7 @@ Already installed containers were found on the server. All installed containers Cannot restore backup settings during active connection - + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း အရံဆက်တင်များကို ပြန်လည်ရယူ၍မရပါ @@ -1682,17 +1595,17 @@ Already installed containers were found on the server. All installed containers KillSwitch - + KillSwitch Disables your internet if your encrypted VPN connection drops out for any reason. - + အကြောင်းတစ်ခုခုကြောင့် VPN ချိတ်ဆက်မှု ပျက်သွားပါက သင့်အင်တာနက်ကို ချက်ချင်းရပ်ဆိုင်းပေးသည်. Cannot change killSwitch settings during active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် killSwitch ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -1715,7 +1628,7 @@ Already installed containers were found on the server. All installed containers Default server does not support custom DNS - မူရင်းဆာဗာသည် စိတ်ကြိုက် DNS ကို အထောက်အပံ့မပေးပါ + မူရင်းဆာဗာသည် စိတ်ကြိုက်ထားနိုင်သည့် DNS ကို အထောက်အပံ့မပေးပါ @@ -1778,17 +1691,17 @@ Already installed containers were found on the server. All installed containers Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + Logging ကို ဖွင့်ထားသည်။ မှတ်တမ်းများကို ၁၄ ရက်အကြာတွင် အလိုအလျောက်ပိတ်ထားမည်ဖြစ်ပြီး မှတ်တမ်းဖိုင်များအားလုံး ပျက်သွားမည်ဖြစ်ကြောင်း သတိပြုပါ။. Logging - လော့ဂ်အင် + Logging Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. - ဤလုပ်ဆောင်ချက်ကို ဖွင့်ခြင်းဖြင့် အပလီကေးရှင်း၏ မှတ်တမ်းများကို အလိုအလျောက် သိမ်းဆည်းပေးမည် ဖြစ်ပြီး မူရင်းအတိုင်း၊ မှတ်တမ်းလုပ်ဆောင်ချက်ကို ပိတ်ထားသည်။ အပလီကေးရှင်းချို့ယွင်းချက်ရှိသောအခါ မှတ်တမ်းသိမ်းဆည်းခြင်းကို ဖွင့်ပါ။ + ဤလုပ်ဆောင်ချက်ကို ဖွင့်ခြင်းဖြင့် အပလီကေးရှင်း၏ မှတ်တမ်းများကို အလိုအလျောက် သိမ်းဆည်းပေးမည် ဖြစ်သည်။ ပုံမှန်အတိုင်းဆိုလျှင် Logging လုပ်ဆောင်ချက်ကို ပိတ်ထားမည်ဖြစ်သည်။ အပလီကေးရှင်းချို့ယွင်းချက်ရှိခဲ့ပါသော် မှတ်တမ်းကိုပြန်လည်ကြည့်ရှုနိုင်ရန် မှတ်တမ်းသိမ်းဆည်းမှုကို ဖွင့်ထားလိုက်ပါ။. @@ -1798,7 +1711,7 @@ Already installed containers were found on the server. All installed containers Open folder with logs - မှတ်တမ်းများဖြင့် ဖိုင်တွဲကိုဖွင့်မည် + မှတ်တမ်းများရှိသောဖိုင်တွဲကိုဖွင့်မည် @@ -1854,18 +1767,6 @@ Already installed containers were found on the server. All installed containers All installed containers have been added to the application ထည့်သွင်းထားသည့် ကွန်တိန်နာအားလုံးကို အပလီကေးရှင်းသို့ ပေါင်းထည့်လိုက်ပြီ - - Clear Amnezia cache - Amnezia ကက်ရှ်ဖိုင်များကို ရှင်းလင်းမည် - - - May be needed when changing other settings - အခြားဆက်တင်များကို ပြောင်းလဲသည့်အခါ လိုအပ်နိုင်သည် - - - Clear cached profiles? - ကက်ရှ်ပရိုဖိုင်များကို ရှင်းမည်လား? - No new installed containers found @@ -1920,7 +1821,7 @@ Already installed containers were found on the server. All installed containers Cannot reboot server during active connection - + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ပြန်လည်စတင်၍မရပါ @@ -1930,7 +1831,7 @@ Already installed containers were found on the server. All installed containers Cannot remove server during active connection - + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ဖယ်ရှား၍မရပါ @@ -1940,12 +1841,12 @@ Already installed containers were found on the server. All installed containers All users whom you shared a connection with will no longer be able to connect to it. - သင်ချိတ်ဆက်မှုတစ်ခုနှင့် မျှဝေထားသည့် အသုံးပြုသူအားလုံး ၎င်းကို ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ။ + သင်၏ချိတ်ဆက်မှကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. Cannot clear server from Amnezia software during active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်း၍မရပါ @@ -1960,7 +1861,7 @@ Already installed containers were found on the server. All installed containers Cannot reset API config during active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် API config ကို ပြန်လည်သတ်မှတ်၍မရပါ @@ -2016,12 +1917,12 @@ Already installed containers were found on the server. All installed containers Clear %1 profile - + %1 ပရိုဖိုင်ကို ရှင်းလင်းမည် Clear %1 profile? - + %1 ပရိုဖိုင်ကို ရှင်းလင်းမည်လား? @@ -2031,7 +1932,7 @@ Already installed containers were found on the server. All installed containers Unable to clear %1 profile while there is an active connection - + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် %1 ပရိုဖိုင်ကို ရှင်းလင်း၍မရပါ @@ -2051,7 +1952,7 @@ Already installed containers were found on the server. All installed containers Cannot remove active container - + Active container ကိုဖယ်ရှား၍မရပါ @@ -2180,32 +2081,32 @@ Already installed containers were found on the server. All installed containers For the region - + ဒေသအတွက် Price - + စျေးနှုန်း Work period - + အလုပ်လုပ်မည့်ကာလ Speed - + မြန်နှုန်း Features - + Feature များ Connect - ချိတ်ဆက်မည် + ချိတ်ဆက်မည် @@ -2213,109 +2114,85 @@ Already installed containers were found on the server. All installed containers VPN by Amnezia - + Amnezia မှ VPN Choose a VPN service that suits your needs. - + သင့်လိုအပ်ချက်များနှင့် ကိုက်ညီသော VPN ဝန်ဆောင်မှုကို ရွေးချယ်ပါ. PageSetupWizardConfigSource - - Server connection - ဆာဗာချိတ်ဆက်မှု - - - Do not use connection code from public sources. It may have been created to intercept your data. - -It's okay as long as it's from someone you trust. - အများသူငှာအသုံးပြုသည့် ရင်းမြစ်များမှ ချိတ်ဆက်ကုဒ်ကို မသုံးပါနှင့်.အဆိုပါကုဒ်များသည် သင့်ဒေတာကို ကြားဖြတ်ရယူရန် ဖန်တီးထားခြင်းဖြစ်နိုင်သည်. - -သင်ယုံကြည်ရတဲ့သူတစ်ယောက်ဆီမှ ရရှိတဲ့ကုဒ်ဖြစ်နေသရွေ့တော့ အဆင်ပြေပါသည်. - - - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - သင့်ဒေတာကို ကြားဖြတ်ရန် ဖန်တီးထားနိုင်သောကြောင့် မယုံကြည်ရသော ရင်းမြစ်များမှ ချိတ်ဆက်ကုဒ်များကို မသုံးပါနှင့်။ - - - What do you have? - သင့်တွင်ဘာရှိပါသလဲ? - File with connection settings ချိတ်ဆက်မှုဆက်တင်များပါဝင်သောဖိုင် - - File with connection settings or backup - ချိတ်ဆက်မှုဆက်တင်များ သို့မဟုတ် အရန်သိမ်းဆည်းထားမှုပါဝင်သောဖိုင် - Connection - ချိတ်ဆက်မှု + ချိတ်ဆက်မှု Insert the key, add a configuration file or scan the QR-code - + Key ကိုထည့်မည်၊ ဖွဲ့စည်းမှုဖိုင်တစ်ခုကိုထည့်မည် သို့မဟုတ် QR-ကုဒ်ကို စကင်န်ဖတ်မည် Insert key - + Key ကိုထည့်သွင်းမည် Insert - ထည်သွင်းမည် + ထည့်သွင်းမည် Continue - + ဆက်လက်လုပ်ဆောင်မည် Other connection options - + အခြားချိတ်ဆက်မှုရွေးချယ်စရာများ VPN by Amnezia - + Amnezia မှ VPN Connect to classic paid and free VPN services from Amnezia - + Amnezia မှ အခပေးနှင့် အခမဲ့ မူလ VPN ဝန်ဆောင်မှုများသို့ ချိတ်ဆက်မည် Self-hosted VPN - + ကိုယ်တိုင် host လုပ်ထားသော VPN Configure Amnezia VPN on your own server - + Amnezia VPN ကို သင်၏ကိုယ်ပိုင်ဆာဗာပေါ်တွင် စီစဥ်ချိန်ညှိမည် Restore from backup - အရန်သိမ်းထားသည့်ဖိုင်မှ ပြန်လည်ရယူမည် + အရံဖိုင်မှ ပြန်လည်ရယူမည် Open backup file - အရန်သိမ်းထားသည့်ဖိုင်ကို ဖွင့်မည် + အရံဖိုင်ကို ဖွင့်မည် Backup files (*.backup) - ဖိုင်များကိုအရန်သိမ်းဆည်းမည် (*.backup) + အရံဖိုင်များ (*.backup) @@ -2330,11 +2207,7 @@ It's okay as long as it's from someone you trust. I have nothing - ကျွန်ုပ်တွင်ဘာမှမရှိပါ - - - Key as text - Key ကိုစာသားအဖြစ် + ကျွန်ုပ်တွင်ဘာမှမရှိပါ @@ -2357,7 +2230,7 @@ It's okay as long as it's from someone you trust. Configure your server - သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိပါ။ + သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိမည် @@ -2382,12 +2255,12 @@ It's okay as long as it's from someone you trust. How to run your VPN server - + သင်၏ဆာဗာကို လည်ပတ်ပုံလည်ပတ်နည်း Where to get connection data, step-by-step instructions for buying a VPS - + ချိတ်ဆက်မှုဒေတာကို ဘယ်မှာရနိုင်မလဲ၊ VPS ဝယ်ယူပုံဝယ်ယူနည်းအတွက် အဆင့်ဆင့် ညွှန်ကြားချက်များ @@ -2415,12 +2288,12 @@ It's okay as long as it's from someone you trust. Choose a VPN protocol - VPN ပရိုတိုကောကို ရွေးပါ။ + VPN ပရိုတိုကောကို ရွေးပါ Skip setup - စနစ်ထည့်သွင်းမှုကို ကျော်သွားပါ။ + စနစ်ထည့်သွင်းမှုကို ကျော်မည် @@ -2464,7 +2337,7 @@ It's okay as long as it's from someone you trust. Usually it takes no more than 5 minutes - များသောအားဖြင့် 5 မိနစ်ထက်မပိုပါ + များသောအားဖြင့် 5 မိနစ်ထက်ပိုမကြာပါ @@ -2472,7 +2345,7 @@ It's okay as long as it's from someone you trust. Installing %1 - ထည့်သွင်းနေသည် %1 + %1 ကိုထည့်သွင်းနေသည် @@ -2500,9 +2373,9 @@ It's okay as long as it's from someone you trust. ထည်သွင်းမည် - + The port must be in the range of 1 to 65535 - + Port သည် 1 မှ 65535 အတွင်း ဖြစ်ရမည် @@ -2515,7 +2388,7 @@ It's okay as long as it's from someone you trust. Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. - သင့်အတွက် ဦးစားပေးအဖြစ်ဆုံးကို ရွေးချယ်ပါ. နောက်ပိုင်းတွင်၊ သင်သည် DNS proxy နှင့် SFTP ကဲ့သို့သော အခြားပရိုတိုကောများနှင့် ထပ်ဆောင်းဝန်ဆောင်မှုများကို ထည့်သွင်းနိုင်သည်. + သင့်အတွက် ဦးစားပေးအဖြစ်ဆုံးကို ရွေးချယ်ပါ။ နောက်ပိုင်းတွင် DNS proxy နှင့် SFTP ကဲ့သို့သော အခြားပရိုတိုကောများနှင့် ထပ်ဆောင်းဝန်ဆောင်မှုများကို ထည့်သွင်းနိုင်သည်။. @@ -2528,34 +2401,10 @@ It's okay as long as it's from someone you trust. PageSetupWizardStart - - Settings restored from backup file - ဆက်တင်များကို အရန်သိမ်းဆည်းထားသောဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ - - - Free service for creating a personal VPN on your server. - သင့်ဆာဗာပေါ်တွင် ကိုယ်ပိုင် VPN ဖန်တီးရန်အတွက် အခမဲ့ဝန်ဆောင်မှု. - - - Helps you access blocked content without revealing your privacy, even to VPN providers. - အခြား VPN ဝန်ဆောင်မှုများကိုပင် သင်၏ privacy ကိုမဖော်ပြဘဲ ပိတ်ဆို့ထားသော အကြောင်းအရာများကို သင်ဝင်ရောက်ကြည့်ရှုနိုင်ရန် အကူအညီပေးပါသည်. - - - I have the data to connect - ကျွန်ုပ်တွင်ချိတ်ဆက်ဖို့အတွက်ဒေတာရှိသည် - - - I have nothing - ကျွန်ုပ်တွင်ဘာမှမရှိပါ - - - https://amnezia.org/instructions/0_starter-guide - https://amnezia.org/instructions/0_starter-guide - Let's get started - + စတင်လိုက်ကြရအောင် @@ -2606,12 +2455,12 @@ It's okay as long as it's from someone you trust. Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. - + WireGuard obfuscation ကိုဖွင့်ထားပါ။ အကယ်၍ သင်၏အင်တာနက်ဝန်ဆောင်မှုပေးသောကုမ္ပဏီက WireGuard ပိတ်ဆို့ထားသော် ၎င်းကိုဖွင့်ထားခြင်းအားဖြင့်အသုံးဝင်နိုင်သည်။. Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. - သင်ယုံကြည်ရသော ရင်းမြစ်များမှသာ ချိတ်ဆက်ကုဒ်များကို အသုံးပြုပါ။ သင့်ဒေတာကို ကြားဖြတ်ရန် အများသူငှာ ရင်းမြစ်များမှ ကုဒ်များကို ဖန်တီးထားသည်။ + သင်ယုံကြည်ရသော ရင်းမြစ်များမှရရှိသော ချိတ်ဆက်ကုဒ်များကိုသာ အသုံးပြုပါ။ လူတိုင်းဝင်ရောက်ရယူနေနိုင်သော ရင်းမြစ်များမှကုဒ်များသည် သင့်ဒေတာကို ကြားဖြတ်ရယူရန် ဖန်တီးထားသောကုဒ်များဖြစ်နေနိုင်သည်။. @@ -2685,7 +2534,7 @@ It's okay as long as it's from someone you trust. Save XRay config - + XRay config ကိုသိမ်းဆည်းမည် @@ -2710,7 +2559,7 @@ It's okay as long as it's from someone you trust. XRay native format - + XRay မူရင်းဖော်မတ် @@ -2746,26 +2595,22 @@ It's okay as long as it's from someone you trust. Creation date: %1 - + ဖန်တီးပြုလုပ်သည့်ရက်စွဲ: %1 Latest handshake: %1 - + နောက်ဆုံး handshake လုပ်ခြင်း: %1 Data received: %1 - + လက်ခံရရှိသည့်ဒေတာ: %1 Data sent: %1 - - - - Creation date: - ဖန်တီးပြုလုပ်သည့်ရက်စွဲ: + ပေးပို့လိုက်သည့်ဒေတာ: %1 @@ -2887,12 +2732,12 @@ It's okay as long as it's from someone you trust. Logging was disabled after 14 days, log files were deleted - + ၁၄ ရက်အကြာတွင် Logging ကို ပိတ်ခဲ့သည်၊ မှတ်တမ်းဖိုင်များကို ဖျက်ပစ်လိုက်ပြီဖြစ်သည် Settings restored from backup file - + ဆက်တင်များကို အရံဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ @@ -3216,58 +3061,6 @@ It's okay as long as it's from someone you trust. Timeout connecting to server ဆာဗာသို့ ချိတ်ဆက်ခြင်း အချိန်ကုန်သွားသည် - - Sftp error: End-of-file encountered - Sftp မှားယွင်းမှု: ဖိုင်အဆုံးသတ်ကို ကြုံတွေ့ခဲ့ရသည် - - - Sftp error: File does not exist - Sftp မှားယွင်းမှု: ဖိုင်မရှိပါ - - - Sftp error: Permission denied - Sftp မှားယွင်းမှု: ခွင့်ပြုချက် ငြင်းဆိုခံလိုက်ရပါသည် - - - Sftp error: Generic failure - Sftp မှားယွင်းမှု: ယေဘုယ မအောင်မြင်ခြင်း - - - Sftp error: Garbage received from server - မှားယွင်းမှု: ဆာဗာမှ အမှိုက်များကို လက်ခံရရှိခဲ့သည် - - - Sftp error: No connection has been set up - Sftp မှားယွင်းမှု: ချိတ်ဆက်မှု မသတ်မှတ်ရသေးပါ - - - Sftp error: There was a connection, but we lost it - Sftp မှားယွင်းမှု: ချိတ်ဆက်မှုတစ်ခုရှိခဲ့သော်လည်း ဆုံးရှုံးသွားခဲ့ပါသည် - - - Sftp error: Operation not supported by libssh yet - Sftp အမှား: လုပ်ဆောင်ချက်ကို libssh မှ မထောက်ပံ့သေးပါ - - - Sftp error: Invalid file handle - Sftp မှားယွင်းမှု: ဖိုင်ကိုင်တွယ်မှု မမှန်ကန်ပါ - - - Sftp error: No such file or directory path exists - Sftp မှားယွင်းမှု: ဤဖိုင်အမျိုးအစား သို့မဟုတ် လမ်းညွှန်လမ်းကြောင်းမျိုး မရှိပါ - - - Sftp error: An attempt to create an already existing file or directory has been made - Sftp မှားယွင်းမှု: ရှိပြီးသား ဖိုင် သို့မဟုတ် လမ်းညွှန်ကို ဖန်တီးရန် ကြိုးပမ်းမှုတစ်ခု ပြုလုပ်ပြီးဖြစ်သည် - - - Sftp error: Write-protected filesystem - Sftp မှားယွင်းမှု: ရေးသားခြင်းမှကာကွယ်ထားသော ဖိုင်စနစ် - - - Sftp error: No media was in remote drive - Sftp မှားယွင်းမှု: မီဒီယာသည် အဝေးမှ drive ထဲတွင် မရှိခဲ့ပါ - The config does not contain any containers and credentials for connecting to the server @@ -3296,17 +3089,17 @@ It's okay as long as it's from someone you trust. Background service is not running - + နောက်ခံဝန်ဆောင်မှု လည်ပတ်နေခြင်းမရှိပါ Server error: Packet manager error - + ဆာဗာ မှားယွင်းမှု: Packet Manager မှားယွင်းမှု SCP error: Generic failure - + SCP မှားယွင်းမှု: ယေဘုယ မအောင်မြင်ခြင်း @@ -3361,52 +3154,52 @@ It's okay as long as it's from someone you trust. In the response from the server, an empty config was received - + ဆာဗာမှ တုံ့ပြန်မှုတွင်၊ config အလွတ်တစ်ခုကို လက်ခံရရှိခဲ့သည် SSL error occurred - + SSL မှားယွင်းမှုဖြစ်သွားသည် Server response timeout on api request - + Api တောင်းဆိုမှုတွင် ဆာဗာတုံ့ပြန်မှု အချိန်ကုန်သွားသည် Missing AGW public key - + AGW public key ပျောက်ဆုံးနေသည် QFile error: The file could not be opened - + QFile မှားယွင်းမှု: ဖိုင်ကို ဖွင့်၍မရပါ QFile error: An error occurred when reading from the file - + QFile မှားယွင်းမှု: ဖိုင်ကိုဖတ်နေစဥ်အတွင်း မှားယွင်းမှုဖြစ်သွားသည် QFile error: The file could not be accessed - + QFile မှားယွင်းမှု: ဖိုင်ကို ဝင်၍မရပါ QFile error: An unspecified error occurred - + QFile မှားယွင်းမှု: သတ်မှတ်မထားသော မှားယွင်းမှုတစ်ခု ဖြစ်ပွားခဲ့သည် QFile error: A fatal error occurred - + QFile မှားယွင်းမှု: ကြီးမားသော မှားယွင်းမှုတစ်ခု ဖြစ်ပွားခဲ့သည် QFile error: The operation was aborted - + QFile မှားယွင်းမှု: လုပ်ငန်းစဥ်ကို ဖျက်သိမ်းလိုက်ရသည် @@ -3431,12 +3224,12 @@ It's okay as long as it's from someone you trust. XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - + REALITY ပါဝင်သော XRay - အင်တာနက်ဆင်ဆာဖြတ်တောက်မှုအပြင်းထန်ဆုံးနိုင်ငံများအတွက် သင့်လျော်သည်။ Web traffic အဖြစ် အသွားအလာကို TLS အဆင့်ဖြင့် ဖုံးကွယ်ပေးထားခြင်း၊ Active probing နည်းလမ်းများဖြင့် ထောက်လှမ်းခံရခြင်းမှ ကာကွယ်ပေးခြင်းများ။. IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. - + IKEv2/IPsec - ခေတ်မီပြီးတည်ငြိမ်သော ပရိုတိုကော၊ အခြားပရိုတိုကောများထက် အနည်းငယ်ပိုမြန်သည်၊ Signal ဆုံးရှုံးပြီးနောက် ချိတ်ဆက်မှုကို ပြန်လည်ရယူနိုင်သည်။ Android နှင့် iOS ၏ နောက်ဆုံးဗားရှင်းများတွင် native ပံ့ပိုးမှုရရှိသည်။. @@ -3509,7 +3302,10 @@ WireGuard သည် ၎င်း၏ ကွဲပြားသော packet လက It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. @@ -3568,10 +3364,6 @@ IKEv2 သည် လုံခြုံရေး၊ တည်ငြိမ်မှ AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. AmneziaWG - WireGuard ကိုအခြေခံထားသော Amnezia မှ အထူးပရိုတိုကော. ၎င်းသည် WireGuard ကဲ့သို့မြန်ဆန်သော်ပြီး ပိတ်ဆို့ခြင်းများကိုလည်း ခံနိုင်ရည်ရှိပါသည်. ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသော ဒေသများတွင်အသုံးပြုရန် အကြံပြုပါသည်. - - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec - ခေတ်မီတည်ငြိမ်သောပရိုတိုကော၊ အခြားအရာများထက်အနည်းငယ်ပိုမြန်သည်၊ signal ပျောက်ဆုံးပြီးနောက် ချိတ်ဆက်မှုကို ပြန်လည်ရယူပေးသည်. - Deploy a WordPress site on the Tor network in two clicks. @@ -3666,7 +3458,7 @@ For more detailed information, you can SOCKS5 proxy server - + SOCKS5 proxy ဆာဗာ @@ -3726,133 +3518,133 @@ For more detailed information, you can vmess:// url is invalid - + vmess:// url သည် မမှန်ကန်ပါ Invalid streamSettings protocol: - + မမှန်ကန်သော streamSettings ပရိုတိုကော: Unknown transport method: - + အမည်မသိ သယ်ယူပို့ဆောင်ရေးနည်းလမ်း: VMess string should start with 'vmess://' - + VMess စာကြောင်းသည် 'vmess://' ဖြင့် စတင်သည် VMess string should be a valid base64 string - + VMess စာကြောင်း သည် မှန်ကန်သော base64 စာကြောင်း ဖြစ်ရမည် JSON should not be empty - + JSON သည် ဗလာမဖြစ်ရပါ VLESS link should start with vless:// - + VLESS စာကြောင်းသည် 'vless://' ဖြင့် စတင်သည် link parse failed: %1 - + လင့်ခ်ခွဲခြမ်းစိတ်ဖြာမှု မအောင်မြင်ပါ: %1 empty host - + Host ဗလာဖြစ်နေသည် missing port - + Port ပျောက်ဆုံးနေသည် missing uuid - + uuid ပျောက်ဆုံးနေသည် Invalid ssd link: json: field %1 must exist - + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 ရှိရပါမည် Invalid ssd link: json: field %1 must be valid port number - + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 သည် မှန်ကန်သော port နံပါတ် ဖြစ်ရပါမည် Invalid ssd link: json: field %1 must be of type 'string' - + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 သည် 'စာကြောင်း' အမျိုးအစား ဖြစ်ရမည် Invalid ssd link: json: field %1 must be an array - + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 သည် array တစ်ခု ဖြစ်ရမည် Skipping invalid ssd server: server must be an object - + မမှန်ကန်သော ssd ဆာဗာကို ကျော်သွားသည်: ဆာဗာသည် object တစ်ခု ဖြစ်ရပါမည် Skipping invalid ssd server: missing required field %1 - + မမှန်ကန်သော ssd ဆာဗာကို ကျော်သွားသည်: လိုအပ်သောအကွက် %1 ပျောက်ဆုံးနေပါသည် Skipping invalid ssd server: field %1 should be of type 'string' - + မမှန်ကန်သော ssd ဆာဗာကို ကျော်သွားသည်: အကွက် %1 သည် 'စာကြောင်း' အမျိုးအစား ဖြစ်ရမည် Invalid ssd link: should begin with ssd:// - + မမှန်ကန်သော ssd လင့်ခ်: ssd:// ဖြင့် စတင်ရမည် Invalid ssd link: base64 parse failed - + မမှန်ကန်သော ssd လင့်ခ်: base64 ခွဲခြမ်းစိတ်ဖြာမှု မအောင်မြင်ပါ Invalid ssd link: json parse failed - + မမှန်ကန်သော ssd လင့်ခ်: json ခွဲခြမ်းစိတ်ဖြာမှု မအောင်မြင်ပါ Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core - + မမှန်ကန်သော ssd လင့်ခ်: rc4-md5 ကုဒ်ဝှက်ခြင်းကို v2ray-core က မပံ့ပိုးပေးပါ SS URI is too short - + SS URI တိုလွန်းသည် Can't find the colon separator between method and password - + Method နှင့် စကားဝှက်ကြားရှိ colon seperator ကို ရှာမတွေ့ပါ Can't find the at separator between password and hostname - + စကားဝှက်နှင့် hostname ကြား at seperator ကို ရှာမတွေ့ပါ Can't find the colon separator between hostname and port - + Hostname နှင့် port ကြားရှိ colon separator ကို ရှာမတွေ့ပါ @@ -3884,14 +3676,10 @@ For more detailed information, you can All settings have been reset to default values ဆက်တင်အားလုံးကို မူရင်းတန်ဖိုးများအဖြစ် ပြန်လည်သတ်မှတ်ထားသည် - - Cached profiles cleared - ကက်ရှ်ပရိုဖိုင်များကို ရှင်းလင်းပြီးပါပြီ - Backup file is corrupted - အရန်သိမ်းထားသည့်ဖိုင်ပျက်ဆီးနေသည် + အရံဖိုင်ပျက်ဆီးနေသည် @@ -4079,11 +3867,7 @@ For more detailed information, you can High - Medium သို့မဟုတ် High - - - Extreme - Extreme + High @@ -4095,10 +3879,6 @@ For more detailed information, you can I want to bypass censorship. This option recommended in most cases. ဆင်ဆာဖြတ်တောက်ခြင်းကို ကျော်ဖြတ်ချင်ပါသည်. ဤရွေးချယ်မှုကို ကိစ္စအများစုအတွက် အကြံပြုထားသည်. - - Most VPN protocols are blocked. Recommended if other options are not working. - VPN ပရိုတိုကောအများစုကို ပိတ်ဆို့ထားသည်. အခြားရွေးချယ်စရာများ အလုပ်မလုပ်ပါက အသုံးပြုရန်အကြံပြုထားသည်. - main2 From cd70b7e6190be93bb628941976d22e5788b32445 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Sep 2024 15:54:47 +0300 Subject: [PATCH 08/90] Translation updated (ukrainian) (#1048) * Update amneziavpn_uk_UA.ts --- client/translations/amneziavpn_uk_UA.ts | 502 +++++++++++++----------- 1 file changed, 269 insertions(+), 233 deletions(-) diff --git a/client/translations/amneziavpn_uk_UA.ts b/client/translations/amneziavpn_uk_UA.ts index c7206586..11377c4d 100644 --- a/client/translations/amneziavpn_uk_UA.ts +++ b/client/translations/amneziavpn_uk_UA.ts @@ -29,47 +29,47 @@ Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + Звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео. Працює для будь-яких сайтів. Швидкість до %1 MBit/s VPN to access blocked sites in regions with high levels of Internet censorship. - + VPN для доступу до заблокованих сайтів у регіонах з високим рівнем інтернет-цензури. Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Premium - звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео у високій роздільній здатності. Працює для всіх вебсайтів, навіть у країнах з найвищим рівнем інтернет-цензури. Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + Amnezia Free — це безкоштовний VPN для обходу блокувань у країнах з високим рівнем інтернет-цензури %1 MBit/s - + %1 MBit/s %1 days - + %1 днів VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Лише популярні сайти, які заблоковані у вашому регіоні, будуть відкриватись за допомогою VPN підключення (Instagram, Facebook, Twitter та ін.). Звичайні сайти будуть відкриватися без використання VPN, <a href="%1/free" style="color: #FBB26A;">більш детально на нашому сайті.</a> Free - + Безкоштовно %1 $/month - + %1 $/місяць @@ -77,22 +77,22 @@ Application added: %1 - + Застосунок додано: %1 The application has already been added - + Застосунок вже додано The selected applications have been added - + Вибрані застосунки додані Application removed: %1 - + Застосунок видалено: %1 @@ -100,7 +100,7 @@ Unable to disconnect during configuration preparation - + Неможливо відключитися під час підготовки конфігурації @@ -115,7 +115,7 @@ unable to create configuration - + Неможливо створити конфігурацію @@ -130,7 +130,7 @@ Preparing... - + Підготовка... @@ -140,12 +140,12 @@ Settings updated successfully - Налаштування оновлено. + Налаштування оновлено The selected protocol is not supported on the current platform - Вибраний протокол не підтримується на цьому пристрої + Вибраний протокол не підтримується на цьому пристрої @@ -253,7 +253,7 @@ Enabled Can't be disabled for current server Увімкнено. -Не може бути вимкнено для даного сервера. +Не може бути вимкнено для даного сервера @@ -283,13 +283,13 @@ Can't be disabled for current server Unable to open file - + Неможливо відкрити файл Invalid configuration file - + Недійсний файл конфігурації @@ -299,7 +299,7 @@ Can't be disabled for current server In the imported configuration, potentially dangerous lines were found: - + У імпортованій конфігурації знайдено потенційно небезпечні рядки: @@ -355,12 +355,12 @@ Already installed containers were found on the server. All installed containers Api config removed - + Конфігурацію API видалено %1 cached profile cleared - + Кешований профіль %1 очищено @@ -375,17 +375,17 @@ Already installed containers were found on the server. All installed containers %1 installed successfully. - + %1 встановлено успішно. API config reloaded - + Конфігурацію API перезавантажено Successfully changed the country of connection to %1 - + Успішно змінено країну підключення на %1 @@ -393,17 +393,17 @@ Already installed containers were found on the server. All installed containers Choose application - + Виберіть застосунок application name - + назва застосунку - + Add selected - + Додати вибране @@ -479,17 +479,17 @@ Already installed containers were found on the server. All installed containers Logging enabled - + Логування увімкнено Split tunneling enabled - Роздільне тунелювання увімкнено + Роздільне тунелювання увімкнено Split tunneling disabled - Роздільне тунелювання вимкнено + Роздільне тунелювання вимкнено @@ -572,32 +572,32 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти The values of the H1-H4 fields must be unique - + Значення полів H1-H4 мають бути унікальними The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + Значення поля S1 + розмір повідомлення ініціалізації (148) не має бути рівним значенню S2 + розмір повідомлення відповіді (92) Save settings? - + Зберегти налаштування? All users with whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Remove AmneziaWG @@ -656,12 +656,12 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Save and Restart Amnezia @@ -682,7 +682,7 @@ Already installed containers were found on the server. All installed containers VPN address subnet - + VPN address subnet @@ -840,12 +840,12 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Remove OpenVPN @@ -906,7 +906,7 @@ Already installed containers were found on the server. All installed containers All users with whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. All users with whom you shared a connection will no longer be able to connect to it. @@ -948,12 +948,12 @@ Already installed containers were found on the server. All installed containers Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення Save and Restart Amnezia @@ -965,27 +965,27 @@ Already installed containers were found on the server. All installed containers WG settings - + Port - Порт + Порт MTU - + MTU Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення @@ -993,22 +993,22 @@ Already installed containers were found on the server. All installed containers XRay settings - + Налаштування XRay Disguised as traffic from - Замаскувати трафік під + Замаскувати трафік під - + Save - Зберегти + Зберегти Unable change settings while there is an active connection - + Неможливо змінити налаштування, поки є активне підключення @@ -1030,7 +1030,7 @@ Already installed containers were found on the server. All installed containers The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. - Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку + Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку. @@ -1055,7 +1055,7 @@ Already installed containers were found on the server. All installed containers Cannot remove AmneziaDNS from running server - + Не вдається видалити AmneziaDNS з працюючого сервера @@ -1095,7 +1095,7 @@ Already installed containers were found on the server. All installed containers User name - Імя користувача + Імя користувача @@ -1110,7 +1110,7 @@ Already installed containers were found on the server. All installed containers In order to mount remote SFTP folder as local drive, perform following steps: <br> - Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> + Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> @@ -1151,18 +1151,18 @@ Already installed containers were found on the server. All installed containers Settings updated successfully - + Налаштування успішно оновлено SOCKS5 settings - + Налаштування SOCKS5 Host - Хост + Хост @@ -1170,50 +1170,50 @@ Already installed containers were found on the server. All installed containers Copied - Скопійовано + Скопійовано Port - Порт + Порт User name - + User name Password - Пароль + Пароль Username - + Username Change connection settings - + Змінити налаштування підключення The port must be in the range of 1 to 65535 - + Порт повинен бути в межах від 1 до 65535 Password cannot be empty - + Пароль не може бути порожнім Username cannot be empty - + Ім'я користувача не може бути порожнім @@ -1241,12 +1241,12 @@ Already installed containers were found on the server. All installed containers Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. + Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. + Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. @@ -1309,7 +1309,7 @@ Already installed containers were found on the server. All installed containers Dev console - + @@ -1351,7 +1351,7 @@ Already installed containers were found on the server. All installed containers Amnezia is a free and open-source application. You can support the developers if you like it. - + Amnezia — це безкоштовний додаток з відкритим кодом. Якщо вам подобається цей додаток, ви можете підтримати розробників. @@ -1415,7 +1415,7 @@ Already installed containers were found on the server. All installed containers Privacy Policy - + Політика конфіденційності @@ -1423,74 +1423,74 @@ Already installed containers were found on the server. All installed containers For the region - + Для регіону Price - + Ціна Work period - + Період роботи Speed - + Швидкість Support tag - + Copied - Скопійовано + Скопійовано Reload API config - + Перезавантажити конфігурацію API Reload API config? - + Перезавантажити конфігурацію API? Continue - Продовжити + Продовжити Cancel - Відмінити + Відмінити Cannot reload API config during active connection - + Неможливо перезавантажити конфігурацію API під час активного підключення Remove from application - + Видалити з додатку Remove from application? - + Видалити з додатку? Cannot remove server during active connection - + Неможливо видалити сервер під час активного підключення @@ -1498,57 +1498,57 @@ Already installed containers were found on the server. All installed containers Cannot change split tunneling settings during active connection - Не можна змінити налаштування роздільного тунелювання при підключеному VPN + Не можна змінити налаштування роздільного тунелювання при підключеному VPN Only the apps from the list should have access via VPN - + Доступ через VPN мають лише програми зі списку Apps from the list should not have access via VPN - + Програми зі списку не мають доступ через VPN App split tunneling - + Split tunneling для додатка Mode - Режим + Режим Remove - Видалити + Видалити Continue - Продовжити + Продовжити Cancel - Відмінити + Відмінити application name - + назва додатка Open executable file - + Відкрити виконуваний файл Executable files (*.*) - + Виконувані файли (*.*) @@ -1561,37 +1561,37 @@ Already installed containers were found on the server. All installed containers Allow application screenshots - Дозволити скріншоти в застосунку + Дозволити скріншоти у застосунку Enable notifications - + Увімкнути сповіщення Enable notifications to show the VPN state in the status bar - + Увімкнути сповіщення (показує стан VPN у статус барі) Auto start - Автозапуск + Автозапуск Launch the application every time the device is starts - Запускати застосунок при старті + Запускати застосунок при старті Auto connect - Автопідключення + Автопідключення Connect to VPN on app start - Підключення до VPN при старті застосунку + Підключення до VPN при старті застосунку @@ -1604,7 +1604,7 @@ Already installed containers were found on the server. All installed containers Запускати застосунок в згорнутому вигляді - + Language Мова @@ -1651,7 +1651,7 @@ Already installed containers were found on the server. All installed containers Cannot reset settings during active connection - + Неможливо скинути налаштування під час активного підключення @@ -1672,17 +1672,17 @@ Already installed containers were found on the server. All installed containers Back up your configuration - + Резервне копіювання вашої конфігурації You can save your settings to a backup file to restore them the next time you install the application. - Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми + Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми. The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. - + Резервна копія міститиме ваші паролі та приватні ключі для всіх серверів, доданих до AmneziaVPN. Зберігайте цю інформацію у безпечному місці. @@ -1738,7 +1738,7 @@ Already installed containers were found on the server. All installed containers Cannot restore backup settings during active connection - + Неможливо відновити резервну копію налаштувань під час активного підключення @@ -1774,27 +1774,27 @@ Already installed containers were found on the server. All installed containers When AmneziaDNS is not used or installed - Ці адреси будуть використовуватись коли вимкнений AmneziaDNS + Ці адреси будуть використовуватись коли вимкнений AmneziaDNS Allows you to use the VPN only for certain Apps - Дозволяє використовувати VPN тільки для вибраних застосунків + Дозволяє використовувати VPN тільки для вибраних застосунків KillSwitch - + KillSwitch Disables your internet if your encrypted VPN connection drops out for any reason. - + Вимикає ваш інтернет, якщо ваше захищене VPN-підключення зникає з будь-якої причини. Cannot change killSwitch settings during active connection - + Неможливо змінити налаштування killSwitch під час активного підключення @@ -1821,7 +1821,7 @@ Already installed containers were found on the server. All installed containers Default server does not support custom DNS - Сервер за замовчуванням не підтримує користувацький DNS + Сервер за замовчуванням не підтримує користувацький DNS @@ -1835,7 +1835,7 @@ Already installed containers were found on the server. All installed containers If AmneziaDNS is not used or installed - Якщо AmneziaDNS вимкнено або не встановлено + Якщо AmneziaDNS вимкнено або не встановлено @@ -1888,7 +1888,7 @@ Already installed containers were found on the server. All installed containers Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + Логування увімкнене. Зверніть увагу, що логування буде автоматично вимкнене через 14 днів, а всі файли журналів будуть видалені. @@ -1898,7 +1898,7 @@ Already installed containers were found on the server. All installed containers Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. - + Увімкнення цієї функції автоматично зберігатиме журнали додатка. За замовчуванням функція логування вимкнена. Увімкніть збереження журналів у випадку збою додатка. @@ -2014,62 +2014,62 @@ Already installed containers were found on the server. All installed containers Reboot server - Перезавантажити сервер + Перезавантажити сервер Do you want to reboot the server? - Ви впевнені, що хочете перезавантажити сервер? + Ви впевнені, що хочете перезавантажити сервер? The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? - Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? + Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? Cannot reboot server during active connection - + Неможливо перезавантажити сервер під час активного підключення Remove server from application - + Видалити сервер з додатка Do you want to remove the server from application? - Ви впевнені, що хочете видалити сервер із застосунку? + Ви впевнені, що хочете видалити сервер із застосунку? Cannot remove server during active connection - + Неможливо видалити сервер під час активного підключення Clear server from Amnezia software - + Очистити сервер від програмного забезпечення Amnezia Do you want to clear server from Amnezia software? - + Ви дійсно хочете очистити сервер від програмного забезпечення Amnezia? All users whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. Cannot clear server from Amnezia software during active connection - + Неможливо очистити сервер від програмного забезпечення Amnezia під час активного підключення Cannot reset API config during active connection - + Неможливо скинути конфігурацію API під час активного підключення Do you want to clear server Amnezia-installed services? @@ -2083,7 +2083,7 @@ Already installed containers were found on the server. All installed containers Do you want to reset API config? - Ви хочете скинути API конфігурацію + Ви хочете скинути API конфігурацію? Remove this server from the app @@ -2136,7 +2136,7 @@ Already installed containers were found on the server. All installed containers Management - + Управління Data @@ -2153,22 +2153,22 @@ Already installed containers were found on the server. All installed containers Clear %1 profile - + Очистити профіль %1 Clear %1 profile? - + Очистити профіль %1? Unable to clear %1 profile while there is an active connection - + Неможливо очистити профіль %1 під час активного підключення Cannot remove active container - + Неможливо видалити активний контейнер @@ -2268,17 +2268,17 @@ Already installed containers were found on the server. All installed containers Cannot change split tunneling settings during active connection - Не можна змінити налаштування роздільного тунелювання при підключеному VPN + Не можна змінити налаштування роздільного тунелювання при підключеному VPN Default server does not support split tunneling function - + website or IP - вебсайт або IP + вебсайт або IP @@ -2329,32 +2329,32 @@ Already installed containers were found on the server. All installed containers For the region - + Для регіону Price - + Ціна Work period - + Період роботи Speed - + Швидкість Features - + Особливості Connect - Підключитись + Підключитись @@ -2362,12 +2362,12 @@ Already installed containers were found on the server. All installed containers VPN by Amnezia - + VPN від Amnezia Choose a VPN service that suits your needs. - + Виберіть VPN-сервіс, який відповідає вашим потребам. @@ -2404,67 +2404,67 @@ It's okay as long as it's from someone you trust. Connection - + Підключення Insert the key, add a configuration file or scan the QR-code - + Вставте ключ, додайте файл конфігурації або відскануйте QR-код Insert key - + Вставити ключ Insert - Вставити + Вставити Continue - Продовжити + Продовжити Other connection options - + Інші параметри підключення VPN by Amnezia - + VPN від Amnezia Connect to classic paid and free VPN services from Amnezia - + Підключайтеся до звичайних платних та безкоштовних VPN-сервісів від Amnezia Self-hosted VPN - + Self-hosted VPN Configure Amnezia VPN on your own server - + Налаштуйте Amnezia VPN на власному сервері Restore from backup - Відновити із бекапа + Відновити із бекапа Open backup file - Відкрити бекап файл + Відкрити бекап файл Backup files (*.backup) - Файли резервної копії (*.backup) + Файли резервної копії (*.backup) @@ -2479,7 +2479,7 @@ It's okay as long as it's from someone you trust. I have nothing - У мене нічого нема + У мене нічого нема Key as text @@ -2533,32 +2533,32 @@ and will not be shared or disclosed to the Amnezia or any third parties 255.255.255.255:22 - + 255.255.255.255:22 SSH Username - + SSH Username Password or SSH private key - + Пароль або SSH ключ All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам + Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам How to run your VPN server - + Як запустити ваш VPN-сервер Where to get connection data, step-by-step instructions for buying a VPS - + Де отримати дані для підключення: покрокові інструкції з придбання VPS @@ -2586,12 +2586,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Choose a VPN protocol - + Виберіть протокол VPN Skip setup - + Пропустити налаштування Set up a VPN yourself @@ -2649,7 +2649,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Cancel installation - Відмінити встановлення + Відмінити встановлення @@ -2693,7 +2693,7 @@ and will not be shared or disclosed to the Amnezia or any third parties The port must be in the range of 1 to 65535 - + Порт повинен бути в межах від 1 до 65535 @@ -2714,7 +2714,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Point the camera at the QR code and hold for a couple of seconds. - Наведіть камеру на QR-код і утримуйте її протягом декількох секунд. + Наведіть камеру на QR-код і утримуйте її протягом декількох секунд. @@ -2742,7 +2742,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Let's get started - + Почнемо @@ -2801,12 +2801,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. - + Увімкніть обфускацію WireGuard. Це може бути корисним, якщо WireGuard заблокований у вашого провайдера. Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. - + Використовуйте коди підключення тільки з джерел, яким ви довіряєте. Коди з публічних джерел можуть бути створені для перехоплення ваших даних. @@ -2856,7 +2856,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Config revoked - Кофігурацію відкликано + Кофігурацію відкликано @@ -2881,22 +2881,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Save AmneziaWG config - Зберегти AmneziaWG конфігурацію + Зберегти AmneziaWG конфігурацію Save Shadowsocks config - Зберегти конфігурацію Shadowsocks + Зберегти конфігурацію Shadowsocks Save Cloak config - Зберегти конфігурацію Cloak + Зберегти конфігурацію Cloak Save XRay config - + Зберегти конфігурацію XRay @@ -2906,22 +2906,22 @@ and will not be shared or disclosed to the Amnezia or any third parties AmneziaWG native format - нативний формат AmneziaWG + нативний формат AmneziaWG Shadowsocks native format - Shadowsocks нативний формат + Shadowsocks нативний формат Cloak native format - Cloak нативний формат + Cloak нативний формат XRay native format - + XRay нативний формат @@ -2931,48 +2931,48 @@ and will not be shared or disclosed to the Amnezia or any third parties Share full access to the server and VPN - Поділитись повним доступом до серверу + Поділитись повним доступом до серверу Use for your own devices, or share with those you trust to manage the server. - Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. + Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. Users - Користувачі + Користувачі User name - Ім'я користувача + Ім'я користувача Search - Пошук + Пошук Creation date: %1 - + Дата створення: %1 Latest handshake: %1 - + Останнє з'єднання: %1 Data received: %1 - + Отримано даних: %1 Data sent: %1 - + Відправлено даних: %1 Creation date: @@ -2981,42 +2981,42 @@ and will not be shared or disclosed to the Amnezia or any third parties Rename - Перейменувати + Перейменувати Client name - + Назва клієнта Save - Зберегти + Зберегти Revoke - Відкликати + Відкликати Revoke the config for a user - %1? - Відкликати доступ для користувача - %1? + Відкликати доступ для користувача - %1? The user will no longer be able to connect to your server. - Користувач більше не зможе підключатись до вашого сервера + Користувач більше не зможе підключатись до вашого сервера - + Continue - Продовжити + Продовжити Cancel - Відмінити + Відмінити Full access @@ -3051,49 +3051,49 @@ and will not be shared or disclosed to the Amnezia or any third parties Full access to the server and VPN - Повний доступ до серверу та VPN + Повний доступ до серверу та VPN We recommend that you use full access to the server only for your own additional devices. - Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. + Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. - Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. + Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. Server - Сервер + Сервер Accessing - Доступ + Доступ File with accessing settings to - Файл з налаштуваннями доступу до + Файл з налаштуваннями доступу до Share - Поділитись + Поділитись Connection to - Підключення до + Підключення до File with connection settings to - Файл з налаштуванням доступу до + Файл з налаштуванням доступу до @@ -3101,12 +3101,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Logging was disabled after 14 days, log files were deleted - + Логування було вимкнене через 14 днів, файли журналів були видалені Settings restored from backup file - Відновлення налаштувань із бекап файлу + Відновлення налаштувань із бекап файлу @@ -3639,22 +3639,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - Shadowsocks - маскує VPN-трафік під звичайний веб-трафік, але розпізнається системами аналізу трафіка в деяких регіонах з високим рівнем цензури. + Shadowsocks - маскує VPN-трафік під звичайний веб-трафік, але розпізнається системами аналізу трафіка в деяких регіонах з високим рівнем цензури. OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak - OpenVPN з маскуванням VPN під HTTPS трафік і захистом від active-probing. Підходить для регіонів з самим високим рівнем цензури. + OpenVPN over Cloak - OpenVPN з маскуванням VPN під HTTPS трафік і захистом від active-probing. Підходить для регіонів з самим високим рівнем цензури. IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. - + IKEv2/IPsec — сучасний стабільний протокол, який дещо швидший за інші та відновлює підключення після втрати сигналу. Має нативну підтримку на останніх версіях Android та iOS. Create a file vault on your server to securely store and transfer files. - Створіть на сервері файлове сховище для безпечного зберігання та передачі файлів. + Створіть на сервері файлове сховище для безпечного зберігання та передачі файлів. @@ -3676,7 +3676,24 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - + Це комбінація протоколу OpenVPN та плагіна Cloak, розроблена спеціально для захисту від блокувань. + +OpenVPN забезпечує безпечне VPN-підключення шляхом шифрування всього інтернет-трафіку між клієнтом і сервером. + +Cloak захищає OpenVPN від виявлення та блокування. + +Cloak може змінювати метадані пакетів так, що повністю маскує VPN-трафік як звичайний веб-трафік і також захищає VPN від виявлення за допомогою активного сканування. Це робить його дуже стійким до виявлення. + +Одразу після отримання першого пакета даних Cloak аутентифікує вхідне підключення. Якщо аутентифікація не вдається, плагін маскує сервер як фальшивий вебсайт, і ваш VPN стає невидимим для систем аналізу. + +Якщо у вашому регіоні екстремальний рівень інтернет-цензури, ми радимо використовувати тільки OpenVPN через Cloak з самого початку роботи з додатком. + + Доступний в AmneziaVPN на всіх платформах + Високе споживання енергії на мобільних пристроях + Гнучкі налаштування + Не розпізнається системами аналізу DPI + Працює через TCP мережевий протокол, порт 443. + @@ -3689,7 +3706,16 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - + Відносно новий популярний VPN-протокол з спрощеною архітектурою. +WireGuard забезпечує стабільне VPN-підключення та високу продуктивність на всіх пристроях. Він використовує жорстко закодовані налаштування шифрування. Порівняно з OpenVPN, WireGuard має нижчу затримку та кращу пропускну здатність передачі даних. + +WireGuard дуже чутливий до блокувань через свої чіткі підписи пакетів. На відміну від деяких інших VPN-протоколів, які використовують техніки обфускації, постійні шаблони підписів пакетів WireGuard легше ідентифікуються та можуть бути заблоковані просунутими системами глибокого аналізу пакетів (DPI) та іншими інструментами моніторингу мережі. + +* Доступний в AmneziaVPN на всіх платформах +* Низьке споживання енергії +* Мінімальна кількість налаштувань +* Легко розпізнається системами аналізу DPI, схильний до блокування +* Працює через UDP мережевий протокол. @@ -3697,7 +3723,10 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - + Протокол **REALITY**, сучасна розробка XRay. Спеціально розроблений для протидії найвищим рівням інтернет-цензури завдяки новому підходу до маскування. +REALITY унікально ідентифікує цензорів під час фази TLS-handshake, працюючи як проксі для VPN клієнтів, при цьому перенаправляючи цензорів на справжні вебсайти, такі як google.com, надаючи справжній TLS-сертифікат та інші дані. +Цей функціонал, відрізняє REALITY від подібних технологій, своєю здатністю маскувати веб-трафік у такий такий, що походить із випадкових справжніх сайтів без необхідності спеціальних налаштувань. +На відміну від старіших протоколів, таких як VMess, VLESS та XTLS-Vision transport, продвиуте розпізнавання "Свій — Чужий" REALITY під час TLS-handshake підвищує безпеку та протидіє виявленню складними системами DPI, що використовують активні техніки аналізу. Це робить REALITY надійним рішенням для підтримання інтернет-свободи в середовищах з жорсткою цензурою. @@ -3759,7 +3788,7 @@ While it offers a blend of security, stability, and speed, it's essential t XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - + XRay with REALITY — підходить для країн з найвищим рівнем інтернет-цензури. Маскування трафіку під веб-трафік на рівні TLS. Захист від виявлення активними методами сканування (active-probing). IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. @@ -3819,7 +3848,14 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for For more detailed information, you can find it in the support section under "Create SFTP file storage." - + Після встановлення Amnezia створить + + файл-сховище на вашому сервері. Ви зможете отримати + доступ до нього за допомогою FileZilla та інших SFTP-клієнтів, +а також змонтувати диск на вашому пристрої для безпосереднього доступу до нього. + +Для більш детальної інформації зверніться + до розділу підтримки під заголовком «Створення SFTP файл-сховища». This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for blocking protection. @@ -3917,7 +3953,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SOCKS5 proxy server - + SOCKS5 proxy server @@ -4172,12 +4208,12 @@ This means that AmneziaWG keeps the fast performance of the original while addin Copy config string - Скопіювати стрічку конфігурації + Скопіювати стрічку конфігурації Show connection settings - Показати налаштування підключення + Показати налаштування підключення From 175477d31f6bd17d5ee8f5f020c2589ab00fcf6b Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 9 Sep 2024 14:36:33 +0300 Subject: [PATCH 09/90] Android qt 6.7 (#1024) * Up Gradle to 8.10 * Update Android dependencies * Up Qt to 6.7.2 * Up qtkeychain to 0.14.3 * Move function of changing the color of the navigation bar to the android side * Fix splashscreen and recent apps thumbnail backgrounds * Android authentication refactoring * Fix GitHub action * Fix the extra circle around the connect button on Android * Fix keyboard popup * Increase the amount of requestNetwork attempts on Android 11 --- .github/workflows/deploy.yml | 14 +-- client/3rd/qtkeychain | 2 +- client/android/AndroidManifest.xml | 13 ++- client/android/build.gradle | 3 + client/android/build.gradle.kts | 2 + client/android/gradle/libs.versions.toml | 22 ++-- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- client/android/gradlew | 7 +- client/android/gradlew.bat | 22 ++-- client/android/qt/build.gradle.kts | 2 +- client/android/res/values/styles.xml | 3 + client/android/settings.gradle.kts | 2 +- .../src/org/amnezia/vpn/AmneziaActivity.kt | 23 +++++ .../src/org/amnezia/vpn/AuthActivity.kt | 97 ++++++++++++++++++ .../src/org/amnezia/vpn/AuthHelper.java | 24 ----- .../org/amnezia/vpn/ImportConfigActivity.kt | 4 +- .../org/amnezia/vpn/qt/QtAndroidController.kt | 2 + .../utils/src/main/kotlin/net/NetworkState.kt | 2 +- client/cmake/android.cmake | 2 - .../platforms/android/android_controller.cpp | 30 ++++++ client/platforms/android/android_controller.h | 4 + .../platforms/android/authResultReceiver.cpp | 16 --- client/platforms/android/authResultReceiver.h | 32 ------ client/ui/controllers/exportController.cpp | 29 ------ client/ui/controllers/exportController.h | 11 -- client/ui/controllers/pageController.cpp | 23 +---- client/ui/controllers/settingsController.cpp | 9 ++ client/ui/controllers/settingsController.h | 2 + client/ui/controllers/systemController.cpp | 9 ++ client/ui/controllers/systemController.h | 1 + client/ui/qml/Components/ConnectButton.qml | 11 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 21 ++-- 33 files changed, 256 insertions(+), 192 deletions(-) create mode 100644 client/android/src/org/amnezia/vpn/AuthActivity.kt delete mode 100644 client/android/src/org/amnezia/vpn/AuthHelper.java delete mode 100644 client/platforms/android/authResultReceiver.cpp delete mode 100644 client/platforms/android/authResultReceiver.h diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 220fbd79..be4d068b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -297,24 +297,24 @@ jobs: env: ANDROID_BUILD_PLATFORM: android-34 - QT_VERSION: 6.6.2 + QT_VERSION: 6.7.2 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} steps: - name: 'Install desktop Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' target: 'desktop' - arch: 'gcc_64' + arch: 'linux_gcc_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_x86_64 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -325,7 +325,7 @@ jobs: extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_x86 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -336,7 +336,7 @@ jobs: extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_armv7 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -347,7 +347,7 @@ jobs: extra: '--external 7z --base ${{ env.QT_MIRROR }}' - name: 'Install android_arm64_v8a Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' diff --git a/client/3rd/qtkeychain b/client/3rd/qtkeychain index 74776e2a..7460df6a 160000 --- a/client/3rd/qtkeychain +++ b/client/3rd/qtkeychain @@ -1 +1 @@ -Subproject commit 74776e2a3e2d98d19943e0968901c5b5e04cc1bd +Subproject commit 7460df6a978669290de5b56c2d98b199b61c3f88 diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index c1c40b52..179def86 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -3,7 +3,6 @@ @@ -46,7 +45,7 @@ android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density |fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc" android:launchMode="singleInstance" - android:windowSoftInputMode="adjustResize" + android:windowSoftInputMode="stateUnchanged|adjustResize" android:exported="true"> @@ -68,9 +67,6 @@ android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --" /> - + + NyAue(f;$XO2=4Cg1P$=#e%|lo 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 delta 34555 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 diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties index 1af9e093..e1adfb49 100644 --- a/client/android/gradle/wrapper/gradle-wrapper.properties +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/client/android/gradlew b/client/android/gradlew index 1aa94a42..f5feea6d 100755 --- a/client/android/gradlew +++ b/client/android/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/client/android/gradlew.bat b/client/android/gradlew.bat index 93e3f59f..9d21a218 100644 --- a/client/android/gradlew.bat +++ b/client/android/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/client/android/qt/build.gradle.kts b/client/android/qt/build.gradle.kts index 139adf4f..6b1d3fd1 100644 --- a/client/android/qt/build.gradle.kts +++ b/client/android/qt/build.gradle.kts @@ -21,5 +21,5 @@ android { } dependencies { - implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) + api(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) } diff --git a/client/android/res/values/styles.xml b/client/android/res/values/styles.xml index 9f4201f8..bc67beb9 100644 --- a/client/android/res/values/styles.xml +++ b/client/android/res/values/styles.xml @@ -1,6 +1,9 @@ + #FF0E0E11 diff --git a/client/android/settings.gradle.kts b/client/android/settings.gradle.kts index 5cfc8314..68426ec8 100644 --- a/client/android/settings.gradle.kts +++ b/client/android/settings.gradle.kts @@ -22,7 +22,7 @@ dependencyResolutionManagement { includeBuild("./gradle/plugins") plugins { - id("com.android.settings") version "8.2.0" + id("com.android.settings") version "8.5.2" id("settings-property-delegate") } diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 8a78750b..9d1c31cb 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -158,6 +158,10 @@ class AmneziaActivity : QtActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "Create Amnezia activity: $intent") + window.apply { + addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + statusBarColor = getColor(R.color.black) + } mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) val proto = mainScope.async(Dispatchers.IO) { VpnStateStore.getVpnState().vpnProto @@ -610,6 +614,14 @@ class AmneziaActivity : QtActivity() { } } + @Suppress("unused") + fun setNavigationBarColor(color: Int) { + Log.v(TAG, "Change navigation bar color: ${"#%08X".format(color)}") + mainScope.launch { + window.navigationBarColor = color + } + } + @Suppress("unused") fun minimizeApp() { Log.v(TAG, "Minimize application") @@ -684,6 +696,17 @@ class AmneziaActivity : QtActivity() { .show() } + @Suppress("unused") + fun requestAuthentication() { + Log.v(TAG, "Request authentication") + mainScope.launch { + qtInitialized.await() + Intent(this@AmneziaActivity, AuthActivity::class.java).also { + startActivity(it) + } + } + } + /** * Utils methods */ diff --git a/client/android/src/org/amnezia/vpn/AuthActivity.kt b/client/android/src/org/amnezia/vpn/AuthActivity.kt new file mode 100644 index 00000000..2593315c --- /dev/null +++ b/client/android/src/org/amnezia/vpn/AuthActivity.kt @@ -0,0 +1,97 @@ +package org.amnezia.vpn + +import android.os.Build +import android.os.Bundle +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.AuthenticationResult +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import org.amnezia.vpn.qt.QtAndroidController +import org.amnezia.vpn.util.Log + +private const val TAG = "AuthActivity" + +private const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL + +class AuthActivity : FragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val biometricManager = BiometricManager.from(applicationContext) + when (biometricManager.canAuthenticate(AUTHENTICATORS)) { + BiometricManager.BIOMETRIC_SUCCESS -> { + showBiometricPrompt(biometricManager) + return + } + + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { + Log.w(TAG, "Unknown biometric status") + showBiometricPrompt(biometricManager) + return + } + + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { + Log.e(TAG, "The specified options are incompatible with the current Android " + + "version ${Build.VERSION.SDK_INT}") + } + + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + Log.w(TAG, "The hardware is unavailable") + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + Log.w(TAG, "No biometric or device credential is enrolled") + } + + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { + Log.w(TAG, "There is no suitable hardware") + } + + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { + Log.w(TAG, "A security vulnerability has been discovered with one or " + + "more hardware sensors") + } + } + QtAndroidController.onAuthResult(true) + finish() + } + + private fun showBiometricPrompt(biometricManager: BiometricManager) { + val executor = ContextCompat.getMainExecutor(applicationContext) + val biometricPrompt = BiometricPrompt(this, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.d(TAG, "Authentication succeeded") + QtAndroidController.onAuthResult(true) + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.w(TAG, "Authentication failed") + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Log.e(TAG, "Authentication error $errorCode: $errString") + QtAndroidController.onAuthResult(false) + finish() + } + }) + + + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(AUTHENTICATORS) + .setTitle("AmneziaVPN") + .setSubtitle(biometricManager.getStrings(AUTHENTICATORS)?.promptMessage) + .build() + + biometricPrompt.authenticate(promptInfo) + } +} diff --git a/client/android/src/org/amnezia/vpn/AuthHelper.java b/client/android/src/org/amnezia/vpn/AuthHelper.java deleted file mode 100644 index 940d03c2..00000000 --- a/client/android/src/org/amnezia/vpn/AuthHelper.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.amnezia.vpn; - -import android.content.Context; -import android.app.KeyguardManager; -import android.content.Intent; -import org.qtproject.qt.android.bindings.QtActivity; - - -import static android.content.Context.KEYGUARD_SERVICE; - -public class AuthHelper extends QtActivity { - - static final String TAG = "AuthHelper"; - - public static Intent getAuthIntent(Context context) { - KeyguardManager mKeyguardManager = (KeyguardManager)context.getSystemService(KEYGUARD_SERVICE); - if (mKeyguardManager.isDeviceSecure()) { - return mKeyguardManager.createConfirmDeviceCredentialIntent(null, null); - } else { - return null; - } - } - -} diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index cae7ab75..9faa30d0 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -33,10 +33,10 @@ class ImportConfigActivity : ComponentActivity() { intent?.let(::readConfig) } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.d(TAG, "onNewIntent: $intent") - intent?.let(::readConfig) + intent.let(::readConfig) } private fun readConfig(intent: Intent) { diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index e382b080..4af138a2 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -25,5 +25,7 @@ object QtAndroidController { external fun onConfigImported(data: String) + external fun onAuthResult(result: Boolean) + external fun decodeQrCode(data: String): Boolean } \ No newline at end of file diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index 26d23215..b71bf393 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -88,7 +88,7 @@ class NetworkState( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val numberAttempts = 3 + val numberAttempts = 300 var attemptCount = 0 while(true) { try { diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 13c357bd..c96d9ab8 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -27,7 +27,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h ) @@ -35,7 +34,6 @@ set(HEADERS ${HEADERS} set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp ) diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index c9ee3cfd..2790eb1b 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -98,6 +98,7 @@ bool AndroidController::initialize() {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)}, + {"onAuthResult", "(Z)V", reinterpret_cast(onAuthResult)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)} }; @@ -210,6 +211,11 @@ void AndroidController::setScreenshotsEnabled(bool enabled) callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled); } +void AndroidController::setNavigationBarColor(unsigned int color) +{ + callActivityMethod("setNavigationBarColor", "(I)V", color); +} + void AndroidController::minimizeApp() { callActivityMethod("minimizeApp", "()V"); @@ -265,6 +271,22 @@ void AndroidController::requestNotificationPermission() callActivityMethod("requestNotificationPermission", "()V"); } +bool AndroidController::requestAuthentication() +{ + QEventLoop wait; + bool result; + connect(this, &AndroidController::authenticationResult, this, + [&result, &wait](const bool &authResult){ + qDebug() << "Android authentication result:" << authResult; + result = authResult; + wait.quit(); + }, + static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); + callActivityMethod("requestAuthentication", "()V"); + wait.exec(); + return result; +} + // Moving log processing to the Android side jclass AndroidController::log; jmethodID AndroidController::logDebug; @@ -462,6 +484,14 @@ void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data)); } +// static +void AndroidController::onAuthResult(JNIEnv *env, jobject thiz, jboolean result) +{ + Q_UNUSED(thiz); + + emit AndroidController::instance()->authenticationResult(result); +} + // static bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data) { diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 1041c31f..759c9c3f 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -41,11 +41,13 @@ public: void exportLogsFile(const QString &fileName); void clearLogs(); void setScreenshotsEnabled(bool enabled); + void setNavigationBarColor(unsigned int color); void minimizeApp(); QJsonArray getAppList(); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); bool isNotificationPermissionGranted(); void requestNotificationPermission(); + bool requestAuthentication(); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); @@ -63,6 +65,7 @@ signals: void configImported(QString config); void importConfigFromOutside(QString config); void initConnectionState(Vpn::ConnectionState state); + void authenticationResult(bool result); private: bool isWaitingStatus = true; @@ -89,6 +92,7 @@ private: static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); + static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); template diff --git a/client/platforms/android/authResultReceiver.cpp b/client/platforms/android/authResultReceiver.cpp deleted file mode 100644 index 21e838a2..00000000 --- a/client/platforms/android/authResultReceiver.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "authResultReceiver.h" - -AuthResultReceiver::AuthResultReceiver(QSharedPointer ¬ifier) : m_notifier(notifier) -{ -} - -void AuthResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) -{ - qDebug() << "receiverRequestCode" << receiverRequestCode << "resultCode" << resultCode; - - if (resultCode == -1) { // ResultOK - emit m_notifier->authSuccessful(); - } else { - emit m_notifier->authFailed(); - } -} diff --git a/client/platforms/android/authResultReceiver.h b/client/platforms/android/authResultReceiver.h deleted file mode 100644 index 9a88dcf5..00000000 --- a/client/platforms/android/authResultReceiver.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef AUTHRESULTRECEIVER_H -#define AUTHRESULTRECEIVER_H - -#include - -#include - -class AuthResultNotifier : public QObject -{ - Q_OBJECT - -public: - AuthResultNotifier(QObject *parent = nullptr) : QObject(parent) {}; - -signals: - void authFailed(); - void authSuccessful(); -}; - -/* Auth result handler for Android */ -class AuthResultReceiver final : public QAndroidActivityResultReceiver -{ -public: - AuthResultReceiver(QSharedPointer ¬ifier); - - void handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) override; - -private: - QSharedPointer m_notifier; -}; - -#endif // AUTHRESULTRECEIVER_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 20c32409..2690b5b1 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -10,9 +10,6 @@ #include "core/controllers/vpnConfigurationController.h" #include "systemController.h" -#ifdef Q_OS_ANDROID - #include "platforms/android/android_utils.h" -#endif #include "qrcodegen.hpp" ExportController::ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, @@ -24,12 +21,6 @@ ExportController::ExportController(const QSharedPointer &serversMo m_clientManagementModel(clientManagementModel), m_settings(settings) { -#ifdef Q_OS_ANDROID - m_authResultNotifier.reset(new AuthResultNotifier); - m_authResultReceiver.reset(new AuthResultReceiver(m_authResultNotifier)); - connect(m_authResultNotifier.get(), &AuthResultNotifier::authFailed, this, [this]() { emit exportErrorOccurred(tr("Access error!")); }); - connect(m_authResultNotifier.get(), &AuthResultNotifier::authSuccessful, this, &ExportController::generateFullAccessConfig); -#endif } void ExportController::generateFullAccessConfig() @@ -63,26 +54,6 @@ void ExportController::generateFullAccessConfig() emit exportConfigChanged(); } -#if defined(Q_OS_ANDROID) -void ExportController::generateFullAccessConfigAndroid() -{ - /* We use builtin keyguard for ssh key export protection on Android */ - QJniObject activity = AndroidUtils::getActivity(); - auto appContext = activity.callObjectMethod("getApplicationContext", "()Landroid/content/Context;"); - if (appContext.isValid()) { - auto intent = QJniObject::callStaticObjectMethod("org/amnezia/vpn/AuthHelper", "getAuthIntent", - "(Landroid/content/Context;)Landroid/content/Intent;", appContext.object()); - if (intent.isValid()) { - if (intent.object() != nullptr) { - QtAndroidPrivate::startActivity(intent.object(), 1, m_authResultReceiver.get()); - } - } else { - generateFullAccessConfig(); - } - } -} -#endif - void ExportController::generateConnectionConfig(const QString &clientName) { clearPreviousConfig(); diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index 023f22cf..b031ea39 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -6,9 +6,6 @@ #include "ui/models/clientManagementModel.h" #include "ui/models/containers_model.h" #include "ui/models/servers_model.h" -#ifdef Q_OS_ANDROID - #include "platforms/android/authResultReceiver.h" -#endif class ExportController : public QObject { @@ -25,9 +22,6 @@ public: public slots: void generateFullAccessConfig(); -#if defined(Q_OS_ANDROID) - void generateFullAccessConfigAndroid(); -#endif void generateConnectionConfig(const QString &clientName); void generateOpenVpnConfig(const QString &clientName); void generateWireGuardConfig(const QString &clientName); @@ -74,11 +68,6 @@ private: QString m_config; QString m_nativeConfigString; QList m_qrCodes; - -#ifdef Q_OS_ANDROID - QSharedPointer m_authResultNotifier; - QSharedPointer m_authResultReceiver; -#endif }; #endif // EXPORTCONTROLLER_H diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 3e5e5cc3..9daca272 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -10,8 +10,6 @@ #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" - #include "platforms/android/android_utils.h" - #include #endif #if defined Q_OS_MAC #include "ui/macos_util.h" @@ -22,18 +20,8 @@ PageController::PageController(const QSharedPointer &serversModel, : QObject(parent), m_serversModel(serversModel), m_settings(settings) { #ifdef Q_OS_ANDROID - // Change color of navigation and status bar's auto initialPageNavigationBarColor = getInitialPageNavigationBarColor(); - AndroidUtils::runOnAndroidThreadSync([&initialPageNavigationBarColor]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - window.callMethod("addFlags", "(I)V", 0x80000000); - window.callMethod("clearFlags", "(I)V", 0x04000000); - window.callMethod("setStatusBarColor", "(I)V", 0xFF0E0E11); - window.callMethod("setNavigationBarColor", "(I)V", initialPageNavigationBarColor); - } - }); + AndroidController::instance()->setNavigationBarColor(initialPageNavigationBarColor); #endif #if defined Q_OS_MACX @@ -115,14 +103,7 @@ unsigned int PageController::getInitialPageNavigationBarColor() void PageController::updateNavigationBarColor(const int color) { #ifdef Q_OS_ANDROID - // Change color of navigation bar - AndroidUtils::runOnAndroidThreadSync([&color]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - window.callMethod("setNavigationBarColor", "(I)V", color); - } - }); + AndroidController::instance()->setNavigationBarColor(color); #endif } diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 93fd8971..911bcb16 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -285,3 +285,12 @@ QString SettingsController::getGatewayEndpoint() { return m_settings->getGatewayEndpoint(); } + +bool SettingsController::isOnTv() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->isOnTv(); +#else + return false; +#endif +} \ No newline at end of file diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index a18888a9..89de1bf8 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -82,6 +82,8 @@ public slots: void setGatewayEndpoint(const QString &endpoint); QString getGatewayEndpoint(); + bool isOnTv(); + signals: void primaryDnsChanged(); void secondaryDnsChanged(); diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index e6a9a28e..4598bff1 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -125,3 +125,12 @@ void SystemController::setQmlRoot(QObject *qmlRoot) { m_qmlRoot = qmlRoot; } + +bool SystemController::isAuthenticated() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->requestAuthentication(); +#else + return true; +#endif +} diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 274df234..d2ee6f63 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -19,6 +19,7 @@ public slots: void setQmlRoot(QObject *qmlRoot); + bool isAuthenticated(); signals: void fileDialogClosed(const bool isAccepted); diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index cb706158..fa18703b 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -14,6 +14,7 @@ Button { property string defaultButtonColor: AmneziaStyle.color.paleGray property string progressButtonColor: AmneziaStyle.color.paleGray property string connectedButtonColor: AmneziaStyle.color.goldenApricot + property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv()) implicitWidth: 190 implicitHeight: 190 @@ -50,14 +51,14 @@ Button { verticalOffset: 0 radius: 10 samples: 25 - color: root.activeFocus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.goldenApricot + color: root.buttonActiveFocus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.goldenApricot source: backgroundCircle } ShapePath { fillColor: AmneziaStyle.color.transparent strokeColor: AmneziaStyle.color.paleGray - strokeWidth: root.activeFocus ? 1 : 0 + strokeWidth: root.buttonActiveFocus ? 1 : 0 capStyle: ShapePath.RoundCap PathAngleArc { @@ -81,14 +82,14 @@ Button { return defaultButtonColor } } - strokeWidth: root.activeFocus ? 2 : 3 + strokeWidth: root.buttonActiveFocus ? 2 : 3 capStyle: ShapePath.RoundCap PathAngleArc { centerX: backgroundCircle.width / 2 centerY: backgroundCircle.height / 2 - radiusX: 93 - (root.activeFocus ? 2 : 0) - radiusY: 93 - (root.activeFocus ? 2 : 0) + radiusX: 93 - (root.buttonActiveFocus ? 2 : 0) + radiusY: 93 - (root.buttonActiveFocus ? 2 : 0) startAngle: 0 sweepAngle: 360 } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 4807c030..2a565230 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -140,22 +140,23 @@ PageType { Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { + PageController.showBusyIndicator(true) + + if (Qt.platform.os === "android" && !SystemController.isAuthenticated()) { + PageController.showBusyIndicator(false) + ExportController.exportErrorOccurred(qsTr("Access error!")) + return + } else { + ExportController.generateFullAccessConfig() + } + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text shareConnectionDrawer.open() - shareConnectionDrawer.contentVisible = false - PageController.showBusyIndicator(true) - - if (Qt.platform.os === "android") { - ExportController.generateFullAccessConfigAndroid(); - } else { - ExportController.generateFullAccessConfig(); - } + shareConnectionDrawer.contentVisible = true PageController.showBusyIndicator(false) - - shareConnectionDrawer.contentVisible = true } } } From 918be16372c0a41f783060a6bf58effae9038ed6 Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Sep 2024 16:27:29 +0400 Subject: [PATCH 10/90] feature: added isAvailable flag support (#1032) * feature: added isAvailable flag support * added the option to switch to dev env --- client/CMakeLists.txt | 3 +++ client/core/controllers/apiController.cpp | 21 +++++++++++-------- client/core/controllers/apiController.h | 3 ++- client/settings.cpp | 15 +++++++++++++ client/settings.h | 4 ++++ client/ui/controllers/installController.cpp | 8 +++---- client/ui/controllers/settingsController.cpp | 19 ++++++++++++++++- client/ui/controllers/settingsController.h | 4 ++++ client/ui/models/apiServicesModel.cpp | 19 +++++++++++++++-- client/ui/models/apiServicesModel.h | 1 + client/ui/qml/Controls2/CardWithIconsType.qml | 1 + client/ui/qml/Pages2/PageDevMenu.qml | 15 +++++++++++++ .../Pages2/PageSetupWizardApiServicesList.qml | 6 ++++-- .../Pages2/PageSetupWizardConfigSource.qml | 2 -- 14 files changed, 100 insertions(+), 21 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 327a83f6..a13afb5f 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -27,6 +27,9 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") +add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") +add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}") + if(IOS) set(PACKAGES ${PACKAGES} Multimedia) endif() diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 8e5f8ed5..3f8684e0 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -9,8 +9,8 @@ #include "QRsa.h" #include "amnezia_application.h" -#include "core/enums/apiEnums.h" #include "configurators/wireguard_configurator.h" +#include "core/enums/apiEnums.h" #include "version.h" namespace @@ -42,7 +42,7 @@ namespace constexpr char keyPayload[] = "key_payload"; } - const QStringList proxyStorageUrl = {""}; + const QStringList proxyStorageUrl = { "" }; ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { @@ -65,7 +65,8 @@ namespace } } -ApiController::ApiController(const QString &gatewayEndpoint, QObject *parent) : QObject(parent), m_gatewayEndpoint(gatewayEndpoint) +ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent) + : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment) { } @@ -143,7 +144,7 @@ QStringList ApiController::getProxyUrls() QEventLoop wait; QList sslErrors; - QNetworkReply* reply; + QNetworkReply *reply; for (const auto &proxyStorageUrl : proxyStorageUrl) { request.setUrl(proxyStorageUrl); @@ -281,7 +282,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); - QNetworkReply* reply; + QNetworkReply *reply; reply = amnApp->manager()->get(request); QEventLoop wait; @@ -300,7 +301,8 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + if (reply->error() != QNetworkReply::NetworkError::TimeoutError + && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { break; } reply->deleteLater(); @@ -355,7 +357,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co EVP_PKEY *publicKey = nullptr; try { - QByteArray key = PROD_AGW_PUBLIC_KEY; + QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QSimpleCrypto::QRsa rsa; publicKey = rsa.getPublicKeyFromByteArray(key); } catch (...) { @@ -375,7 +377,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply* reply = manager.post(request, QJsonDocument(requestBody).toJson()); + QNetworkReply *reply = manager.post(request, QJsonDocument(requestBody).toJson()); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -395,7 +397,8 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + if (reply->error() != QNetworkReply::NetworkError::TimeoutError + && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { break; } reply->deleteLater(); diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index 6cfde983..a094233b 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -14,7 +14,7 @@ class ApiController : public QObject Q_OBJECT public: - explicit ApiController(const QString &gatewayEndpoint, QObject *parent = nullptr); + explicit ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent = nullptr); public slots: void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); @@ -44,6 +44,7 @@ private: QString m_gatewayEndpoint; QStringList m_proxyUrls; + bool m_isDevEnvironment; }; #endif // APICONTROLLER_H diff --git a/client/settings.cpp b/client/settings.cpp index 490ede52..8faf00c2 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -519,7 +519,22 @@ void Settings::setGatewayEndpoint(const QString &endpoint) m_gatewayEndpoint = endpoint; } +void Settings::setDevGatewayEndpoint() +{ + m_gatewayEndpoint = DEV_AGW_ENDPOINT; +} + QString Settings::getGatewayEndpoint() { return m_gatewayEndpoint; } + +bool Settings::isDevGatewayEnv() +{ + return m_isDevGatewayEnv; +} + +void Settings::toggleDevGatewayEnv(bool enabled) +{ + m_isDevGatewayEnv = enabled; +} diff --git a/client/settings.h b/client/settings.h index 55a3d057..c0ab0559 100644 --- a/client/settings.h +++ b/client/settings.h @@ -217,7 +217,10 @@ public: void resetGatewayEndpoint(); void setGatewayEndpoint(const QString &endpoint); + void setDevGatewayEndpoint(); QString getGatewayEndpoint(); + bool isDevGatewayEnv(); + void toggleDevGatewayEnv(bool enabled); signals: void saveLogsChanged(bool enabled); @@ -234,6 +237,7 @@ private: mutable SecureQSettings m_settings; QString m_gatewayEndpoint; + bool m_isDevGatewayEnv; }; #endif // SETTINGS_H diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 66e04520..c6f17057 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -799,7 +799,7 @@ void InstallController::addEmptyServer() bool InstallController::fillAvailableServices() { - ApiController apiController(m_settings->getGatewayEndpoint()); + ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); QByteArray responseBody; ErrorCode errorCode = apiController.getServicesList(responseBody); @@ -821,7 +821,7 @@ bool InstallController::installServiceFromApi() return false; } - ApiController apiController(m_settings->getGatewayEndpoint()); + ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); QJsonObject serverConfig; ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(), @@ -849,7 +849,7 @@ bool InstallController::installServiceFromApi() bool InstallController::updateServiceFromApi(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig) { - ApiController apiController(m_settings->getGatewayEndpoint()); + ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); @@ -885,7 +885,7 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin void InstallController::updateServiceFromTelegram(const int serverIndex) { - ApiController *apiController = new ApiController(m_settings->getGatewayEndpoint()); + ApiController *apiController = new ApiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); auto serverConfig = m_serversModel->getServerConfig(serverIndex); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 911bcb16..7a95589b 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -283,7 +283,24 @@ void SettingsController::setGatewayEndpoint(const QString &endpoint) QString SettingsController::getGatewayEndpoint() { - return m_settings->getGatewayEndpoint(); + return m_settings->isDevGatewayEnv() ? "Dev endpoint" : m_settings->getGatewayEndpoint(); +} + +bool SettingsController::isDevGatewayEnv() +{ + return m_settings->isDevGatewayEnv(); +} + +void SettingsController::toggleDevGatewayEnv(bool enabled) +{ + m_settings->toggleDevGatewayEnv(enabled); + if (enabled) { + m_settings->setDevGatewayEndpoint(); + } else { + m_settings->resetGatewayEndpoint(); + } + emit gatewayEndpointChanged(m_settings->getGatewayEndpoint()); + emit devGatewayEnvChanged(enabled); } bool SettingsController::isOnTv() diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 89de1bf8..d64e63cf 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -27,6 +27,7 @@ public: Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled) Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged) + Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged) public slots: void toggleAmneziaDns(bool enable); @@ -81,6 +82,8 @@ public slots: void resetGatewayEndpoint(); void setGatewayEndpoint(const QString &endpoint); QString getGatewayEndpoint(); + bool isDevGatewayEnv(); + void toggleDevGatewayEnv(bool enabled); bool isOnTv(); @@ -105,6 +108,7 @@ signals: void devModeEnabled(); void gatewayEndpointChanged(const QString &endpoint); + void devGatewayEnvChanged(bool enabled); private: QSharedPointer m_serversModel; diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 3e74d259..2a87bde3 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -25,6 +25,8 @@ namespace constexpr char availableCountries[] = "available_countries"; constexpr char storeEndpoint[] = "store_endpoint"; + + constexpr char isAvailable[] = "is_available"; } namespace serviceType @@ -63,8 +65,12 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); - } else { - return tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); + } else if (serviceType == serviceType::amneziaFree){ + QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); + if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + description += tr("

    Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); + } + return description; } } case ServiceDescriptionRole: { @@ -75,6 +81,14 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const return tr("Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship"); } } + case IsServiceAvailableRole: { + if (serviceType == serviceType::amneziaFree) { + if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + return false; + } + } + return true; + } case SpeedRole: { auto speed = serviceInfo.value(configKey::speed).toString(); return tr("%1 MBit/s").arg(speed); @@ -193,6 +207,7 @@ QHash ApiServicesModel::roleNames() const roles[NameRole] = "name"; roles[CardDescriptionRole] = "cardDescription"; roles[ServiceDescriptionRole] = "serviceDescription"; + roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; roles[WorkPeriodRole] = "workPeriod"; roles[RegionRole] = "region"; diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 64676be6..49918940 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -13,6 +13,7 @@ public: NameRole = Qt::UserRole + 1, CardDescriptionRole, ServiceDescriptionRole, + IsServiceAvailableRole, SpeedRole, WorkPeriodRole, RegionRole, diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index 8630434b..fea65116 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -79,6 +79,7 @@ Button { visible: text !== "" color: AmneziaStyle.color.mutedGray + textFormat: Text.RichText Layout.fillWidth: true Layout.rightMargin: 16 diff --git a/client/ui/qml/Pages2/PageDevMenu.qml b/client/ui/qml/Pages2/PageDevMenu.qml index af6f773a..5da40eff 100644 --- a/client/ui/qml/Pages2/PageDevMenu.qml +++ b/client/ui/qml/Pages2/PageDevMenu.qml @@ -89,6 +89,21 @@ PageType { // KeyNavigation.tab: saveButton } + + SwitcherType { + id: switcher + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.topMargin: 16 + + text: qsTr("Dev gateway environment") + checked: SettingsController.isDevGatewayEnv + onToggled: function() { + SettingsController.isDevGatewayEnv = checked + } + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index cb79f19e..85a50393 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -88,8 +88,10 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" onClicked: { - ApiServicesModel.setServiceIndex(index) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 3febca4c..3c56b52e 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -119,8 +119,6 @@ PageType { CardWithIconsType { id: apiInstalling - visible: false - Layout.fillWidth: true Layout.rightMargin: 16 Layout.leftMargin: 16 From 9cab51fb0025ec03c30b596ef27328da711611ac Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Sep 2024 20:53:44 +0400 Subject: [PATCH 11/90] added open service logs to logs page (#951) * added open service logs to logs page * redesign of log saving buttons * hide service logs buttons for mobile platforms * refactoring: moved logger to common folder * feature: added the ability to enable logs to the start screen --- client/CMakeLists.txt | 4 +- client/amnezia_application.cpp | 2 +- client/core/controllers/serverController.cpp | 2 - client/logger.h | 107 ------- client/settings.cpp | 2 +- client/ui/controllers/settingsController.cpp | 18 +- client/ui/controllers/settingsController.h | 2 + client/ui/property_helper.h | 27 -- .../ui/qml/Controls2/LabelWithButtonType.qml | 9 +- client/ui/qml/Controls2/SwitcherType.qml | 3 +- client/ui/qml/Pages2/PageSettingsLogging.qml | 299 +++++++++++------- .../Pages2/PageSetupWizardConfigSource.qml | 45 +++ client/ui/qml/Pages2/PageStart.qml | 8 + client/utilities.cpp | 16 - client/utilities.h | 1 - {client => common/logger}/logger.cpp | 188 ++++++----- common/logger/logger.h | 114 +++++++ ipc/ipcserver.cpp | 88 ++---- service/server/CMakeLists.txt | 5 +- service/server/logger.cpp | 185 ----------- service/server/logger.h | 83 ----- service/server/main.cpp | 2 +- 22 files changed, 519 insertions(+), 691 deletions(-) delete mode 100644 client/logger.h delete mode 100644 client/ui/property_helper.h rename {client => common/logger}/logger.cpp (56%) create mode 100644 common/logger/logger.h delete mode 100644 service/server/logger.cpp delete mode 100644 service/server/logger.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a13afb5f..4d0c1c2a 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -113,6 +113,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake) include_directories( ${CMAKE_CURRENT_LIST_DIR}/../ipc + ${CMAKE_CURRENT_LIST_DIR}/../common/logger ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) @@ -134,7 +135,6 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h ${CMAKE_CURRENT_LIST_DIR}/ui/pages.h - ${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h ${CMAKE_CURRENT_BINARY_DIR}/version.h @@ -143,6 +143,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h ${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h ${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h + ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h ) # Mozilla headres @@ -193,6 +194,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp + ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp ) # Mozilla sources diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index b8ce5b00..526b9fa9 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -164,7 +164,7 @@ void AmneziaApplication::init() bool enabled = m_settings->isSaveLogs(); #ifndef Q_OS_ANDROID if (enabled) { - if (!Logger::init()) { + if (!Logger::init(false)) { qWarning() << "Initialization of debug subsystem failed"; } } diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 233d66d4..9a745e3d 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -83,7 +83,6 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr } qDebug().noquote() << lineToExec; - Logger::appendSshLog("Run command:" + lineToExec); error = m_sshClient.executeCommand(lineToExec, cbReadStdOut, cbReadStdErr); if (error != ErrorCode::NoError) { @@ -100,7 +99,6 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti const std::function &cbReadStdErr) { QString fileName = "/opt/amnezia/" + Utils::getRandomString(16) + ".sh"; - Logger::appendSshLog("Run container script for " + ContainerProps::containerToString(container) + ":\n" + script); ErrorCode e = uploadTextFileToContainer(container, credentials, script, fileName); if (e) diff --git a/client/logger.h b/client/logger.h deleted file mode 100644 index 0dcbd35c..00000000 --- a/client/logger.h +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include -#include - -#include "ui/property_helper.h" - -#include "mozilla/shared/loglevel.h" - -class Logger : public QObject -{ - Q_OBJECT - AUTO_PROPERTY(QString, sshLog) - AUTO_PROPERTY(QString, allLog) - -public: - static Logger& Instance(); - - static void appendSshLog(const QString &log); - static void appendAllLog(const QString &log); - - - static bool init(); - static void deInit(); - static bool setServiceLogsEnabled(bool enabled); - static bool openLogsFolder(); - static bool openServiceLogsFolder(); - static QString appLogFileNamePath(); - static void clearLogs(); - static void clearServiceLogs(); - static void cleanUp(); - - static QString userLogsFilePath(); - static QString getLogFile(); - - // compat with Mozilla logger - Logger(const QString &className) { m_className = className; } - const QString& className() const { return m_className; } - - class Log { - public: - Log(Logger* logger, LogLevel level); - ~Log(); - - Log& operator<<(uint64_t t); - Log& operator<<(const char* t); - Log& operator<<(const QString& t); - Log& operator<<(const QStringList& t); - Log& operator<<(const QByteArray& t); - Log& operator<<(const QJsonObject& t); - Log& operator<<(QTextStreamFunction t); - Log& operator<<(const void* t); - - // Q_ENUM - template - typename std::enable_if::Value, Log&>::type - operator<<(T t) { - const QMetaObject* meta = qt_getEnumMetaObject(t); - const char* name = qt_getEnumName(t); - addMetaEnum(typename QFlags::Int(t), meta, name); - return *this; - } - - private: - void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name); - - Logger* m_logger; - LogLevel m_logLevel; - - struct Data { - Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {} - - QString m_buffer; - QTextStream m_ts; - }; - - Data* m_data; - }; - - Log error(); - Log warning(); - Log info(); - Log debug(); - QString sensitive(const QString& input); - -private: - Logger() {} - Logger(Logger const &) = delete; - Logger& operator= (Logger const&) = delete; - - static QString userLogsDir(); - - static QFile m_file; - static QTextStream m_textStream; - static QString m_logFileName; - - friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - // compat with Mozilla logger - QString m_className; -}; - -#endif // LOGGER_H diff --git a/client/settings.cpp b/client/settings.cpp index 8faf00c2..7a572a13 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -227,7 +227,7 @@ void Settings::setSaveLogs(bool enabled) if (!isSaveLogs()) { Logger::deInit(); } else { - if (!Logger::init()) { + if (!Logger::init(false)) { qWarning() << "Initialization of debug subsystem failed"; } } diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 7a95589b..c3945512 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -88,7 +88,12 @@ void SettingsController::toggleLogging(bool enable) void SettingsController::openLogsFolder() { - Logger::openLogsFolder(); + Logger::openLogsFolder(false); +} + +void SettingsController::openServiceLogsFolder() +{ + Logger::openLogsFolder(true); } void SettingsController::exportLogsFile(const QString &fileName) @@ -100,12 +105,21 @@ void SettingsController::exportLogsFile(const QString &fileName) #endif } +void SettingsController::exportServiceLogsFile(const QString &fileName) +{ +#ifdef Q_OS_ANDROID + AndroidController::instance()->exportLogsFile(fileName); +#else + SystemController::saveFile(fileName, Logger::getServiceLogFile()); +#endif +} + void SettingsController::clearLogs() { #ifdef Q_OS_ANDROID AndroidController::instance()->clearLogs(); #else - Logger::clearLogs(); + Logger::clearLogs(false); Logger::clearServiceLogs(); #endif } diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index d64e63cf..efc18a7d 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -43,7 +43,9 @@ public slots: void toggleLogging(bool enable); void openLogsFolder(); + void openServiceLogsFolder(); void exportLogsFile(const QString &fileName); + void exportServiceLogsFile(const QString &fileName); void clearLogs(); void backupAppConfig(const QString &fileName); diff --git a/client/ui/property_helper.h b/client/ui/property_helper.h deleted file mode 100644 index 927105b3..00000000 --- a/client/ui/property_helper.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef PROPERTY_HELPER_H -#define PROPERTY_HELPER_H - -#include - -#define AUTO_PROPERTY(TYPE, NAME) \ - Q_PROPERTY(TYPE NAME READ NAME WRITE set_ ## NAME NOTIFY NAME ## Changed ) \ - public: \ - TYPE NAME() const { return m_ ## NAME ; } \ - void set_ ## NAME(TYPE value) { \ - if (m_ ## NAME == value) return; \ - m_ ## NAME = value; \ - emit NAME ## Changed(value); \ - } \ - Q_SIGNAL void NAME ## Changed(TYPE value);\ - private: \ - TYPE m_ ## NAME{}; - -#define READONLY_PROPERTY(TYPE, NAME) \ - Q_PROPERTY(TYPE NAME READ NAME CONSTANT ) \ - public: \ - TYPE NAME() const { return m_ ## NAME ; } \ - private: \ - void NAME(TYPE value) {m_ ## NAME = value; } \ - TYPE m_ ## NAME{}; - -#endif // PROPERTY_HELPER_H diff --git a/client/ui/qml/Controls2/LabelWithButtonType.qml b/client/ui/qml/Controls2/LabelWithButtonType.qml index 3b1609f7..41faf108 100644 --- a/client/ui/qml/Controls2/LabelWithButtonType.qml +++ b/client/ui/qml/Controls2/LabelWithButtonType.qml @@ -20,7 +20,8 @@ Item { property string buttonImageSource property string rightImageSource property string leftImageSource - property bool isLeftImageHoverEnabled: true //todo separete this qml file to 3 + property bool isLeftImageHoverEnabled: true + property bool isSmallLeftImage: false property alias rightButton: rightImage property alias eyeButton: eyeImage @@ -114,9 +115,9 @@ Item { visible: leftImageSource ? true : false - Layout.preferredHeight: rightImageSource || !isLeftImageHoverEnabled ? leftImage.implicitHeight : 56 - Layout.preferredWidth: rightImageSource || !isLeftImageHoverEnabled ? leftImage.implicitWidth : 56 - Layout.rightMargin: rightImageSource || !isLeftImageHoverEnabled ? 16 : 0 + Layout.preferredHeight: (rightImageSource || !isLeftImageHoverEnabled || isSmallLeftImage) ? 40 : 56 + Layout.preferredWidth: (rightImageSource || !isLeftImageHoverEnabled || isSmallLeftImage)? 40 : 56 + Layout.rightMargin: isSmallLeftImage ? 8 : (rightImageSource || !isLeftImageHoverEnabled) ? 16 : 0 radius: 12 color: AmneziaStyle.color.transparent diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index 9b2885ea..43c35778 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -102,8 +102,7 @@ Switch { contentItem: ColumnLayout { id: content - anchors.top: parent.top - anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left ListItemTitleType { diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index 3ab0df8a..9abfc453 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -16,18 +16,6 @@ import "../Controls2/TextTypes" PageType { id: root - Connections { - target: SettingsController - - function onLoggingStateChanged() { - if (SettingsController.isLoggingEnabled) { - var message = qsTr("Logging is enabled. Note that logs will be automatically \ -disabled after 14 days, and all log files will be deleted.") - PageController.showNotificationMessage(message) - } - } - } - defaultActiveFocusItem: focusItem Item { @@ -58,13 +46,12 @@ disabled after 14 days, and all log files will be deleted.") anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - spacing: 16 + spacing: 0 HeaderType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 headerText: qsTr("Logging") descriptionText: qsTr("Enabling this function will save application's logs automatically. " + @@ -75,11 +62,13 @@ disabled after 14 days, and all log files will be deleted.") id: switcher Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - text: qsTr("Save logs") + text: qsTr("Enable logs") checked: SettingsController.isLoggingEnabled - KeyNavigation.tab: openFolderButton + //KeyNavigation.tab: openFolderButton onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { SettingsController.isLoggingEnabled = checked @@ -87,132 +76,200 @@ disabled after 14 days, and all log files will be deleted.") } } - RowLayout { + DividerType {} + + LabelWithButtonType { + // id: labelWithButton2 Layout.fillWidth: true + Layout.topMargin: -8 - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: GC.isMobile() ? 0 : root.width / 3 - visible: !GC.isMobile() + text: qsTr("Clear logs") + leftImageSource: "qrc:/images/controls/trash.svg" + isSmallLeftImage: true - ImageButtonType { - id: openFolderButton - Layout.alignment: Qt.AlignHCenter + // KeyNavigation.tab: labelWithButton3 - implicitWidth: 56 - implicitHeight: 56 + clickedFunction: function() { + var headerText = qsTr("Clear logs?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - image: "qrc:/images/controls/folder-open.svg" - KeyNavigation.tab: saveButton - - onClicked: SettingsController.openLogsFolder() - Keys.onReturnPressed: openFolderButton.clicked() - Keys.onEnterPressed: openFolderButton.clicked() + var yesButtonFunction = function() { + PageController.showBusyIndicator(true) + SettingsController.clearLogs() + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } - - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - - text: qsTr("Open folder with logs") - color: AmneziaStyle.color.paleGray - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) - - ImageButtonType { - id: saveButton - Layout.alignment: Qt.AlignHCenter - - implicitWidth: 56 - implicitHeight: 56 - - image: "qrc:/images/controls/save.svg" - KeyNavigation.tab: clearButton - - Keys.onReturnPressed: saveButton.clicked() - Keys.onEnterPressed: saveButton.clicked() - onClicked: { - var fileName = "" - if (GC.isMobile()) { - fileName = "AmneziaVPN.log" - } else { - fileName = SystemController.getFileName(qsTr("Save"), - qsTr("Logs files (*.log)"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN", - true, - ".log") - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - SettingsController.exportLogsFile(fileName) - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs file saved")) - } + var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() } } - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } - text: qsTr("Save logs to file") - color: AmneziaStyle.color.paleGray + ListItemTitleType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Client logs") + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + color: AmneziaStyle.color.mutedGray + text: qsTr("AmneziaVPN logs") + } + + LabelWithButtonType { + // id: labelWithButton2 + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Open logs folder") + leftImageSource: "qrc:/images/controls/folder-open.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + SettingsController.openLogsFolder() + } + } + + DividerType {} + + LabelWithButtonType { + // id: labelWithButton2 + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Export logs") + leftImageSource: "qrc:/images/controls/save.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN", + true, + ".log") + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } } + } - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) + DividerType {} - ImageButtonType { - id: clearButton - Layout.alignment: Qt.AlignHCenter + ListItemTitleType { + visible: !GC.isMobile() - implicitWidth: 56 - implicitHeight: 56 + Layout.fillWidth: true + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - image: "qrc:/images/controls/delete.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) + text: qsTr("Service logs") + } - Keys.onReturnPressed: clearButton.clicked() - Keys.onEnterPressed: clearButton.clicked() - onClicked: function() { - var headerText = qsTr("Clear logs?") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") + ParagraphTextType { + visible: !GC.isMobile() - var yesButtonFunction = function() { - PageController.showBusyIndicator(true) - SettingsController.clearLogs() - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } + color: AmneziaStyle.color.mutedGray + text: qsTr("AmneziaVPN-service logs") + } + + LabelWithButtonType { + // id: labelWithButton2 + + visible: !GC.isMobile() + + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Open logs folder") + leftImageSource: "qrc:/images/controls/folder-open.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + SettingsController.openServiceLogsFolder() + } + } + + DividerType { + visible: !GC.isMobile() + } + + LabelWithButtonType { + // id: labelWithButton2 + + visible: !GC.isMobile() + + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 + + text: qsTr("Export logs") + leftImageSource: "qrc:/images/controls/save.svg" + isSmallLeftImage: true + + // KeyNavigation.tab: labelWithButton3 + + clickedFunction: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN-service.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service", + true, + ".log") } - - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - - text: qsTr("Clear logs") - color: AmneziaStyle.color.paleGray + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportServiceLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } } } + + DividerType { + visible: !GC.isMobile() + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 3c56b52e..7f7cf9e1 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -55,6 +55,51 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Connection") + + actionButtonImage: PageController.isStartPageVisible() ? "qrc:/images/controls/more-vertical.svg" : "" + actionButtonFunction: function() { + moreActionsDrawer.open() + } + + DrawerType2 { + id: moreActionsDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.35 + + expandedContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 32 + + headerText: qsTr("Settings") + } + + SwitcherType { + id: switcher + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Enable logs") + + checked: SettingsController.isLoggingEnabled + onCheckedChanged: { + if (checked !== SettingsController.isLoggingEnabled) { + SettingsController.isLoggingEnabled = checked + } + } + } + + } + } } ParagraphTextType { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 770347ca..bb6663fb 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -202,6 +202,14 @@ PageType { PageController.showNotificationMessage(qsTr("Settings restored from backup file")) PageController.goToPageHome() } + + function onLoggingStateChanged() { + if (SettingsController.isLoggingEnabled) { + var message = qsTr("Logging is enabled. Note that logs will be automatically" + + "disabled after 14 days, and all log files will be deleted.") + PageController.showNotificationMessage(message) + } + } } StackViewType { diff --git a/client/utilities.cpp b/client/utilities.cpp index a2f3d021..4047365f 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -69,22 +69,6 @@ QString Utils::JsonToString(const QJsonArray &array, QJsonDocument::JsonFormat f return doc.toJson(format); } -QString Utils::systemLogPath() -{ -#ifdef Q_OS_WIN - QStringList locationList = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); - QString primaryLocation = "ProgramData"; - foreach (const QString &location, locationList) { - if (location.contains(primaryLocation)) { - return QString("%1/%2/log").arg(location).arg(APPLICATION_NAME); - } - } - return QString(); -#else - return QString("/var/log/%1").arg(APPLICATION_NAME); -#endif -} - bool Utils::initializePath(const QString &path) { QDir dir; diff --git a/client/utilities.h b/client/utilities.h index b85c5b3b..9bf8c82a 100644 --- a/client/utilities.h +++ b/client/utilities.h @@ -23,7 +23,6 @@ public: static QJsonObject JsonFromString(const QString &string); static QString executable(const QString &baseName, bool absPath); static QString usrExecutable(const QString &baseName); - static QString systemLogPath(); static bool createEmptyFile(const QString &path); static bool initializePath(const QString &path); diff --git a/client/logger.cpp b/common/logger/logger.cpp similarity index 56% rename from client/logger.cpp rename to common/logger/logger.cpp index c76bc698..747590b9 100644 --- a/client/logger.cpp +++ b/common/logger/logger.cpp @@ -4,18 +4,18 @@ #include #include #include -#include #include +#include #include #include #include -#include "version.h" #include "utilities.h" +#include "version.h" #ifdef AMNEZIA_DESKTOP -#include + #include #endif #ifdef Q_OS_IOS @@ -25,8 +25,9 @@ QFile Logger::m_file; QTextStream Logger::m_textStream; QString Logger::m_logFileName = QString("%1.log").arg(APPLICATION_NAME); +QString Logger::m_serviceLogFileName = QString("%1.log").arg(SERVICE_NAME); -void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) +void debugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { if (msg.simplified().isEmpty()) { return; @@ -37,12 +38,12 @@ void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, cons return; } - if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") || msg.startsWith("stale focus object")) { + if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") + || msg.startsWith("stale focus object")) { return; } Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; - Logger::appendAllLog(qFormatLogMessage(type, context, msg)); std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; } @@ -53,36 +54,24 @@ Logger &Logger::Instance() return s; } -void Logger::appendSshLog(const QString &log) +bool Logger::init(bool isServiceLogger) { - QString dt = QDateTime::currentDateTime().toString(); - Instance().m_sshLog.append(dt + ": " + log + "\n"); - emit Instance().sshLogChanged(Instance().sshLog()); -} - -void Logger::appendAllLog(const QString &log) -{ - Instance().m_allLog.append(log + "\n"); - emit Instance().allLogChanged(Instance().allLog()); -} - -bool Logger::init() -{ - qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); - - QString path = userLogsDir(); + QString path = isServiceLogger ? systemLogDir() : userLogsDir(); + QString logFileName = isServiceLogger ? m_serviceLogFileName : m_logFileName ; QDir appDir(path); if (!appDir.mkpath(path)) { return false; } - m_file.setFileName(appDir.filePath(m_logFileName)); + m_file.setFileName(appDir.filePath(logFileName)); if (!m_file.open(QIODevice::Append)) { - qWarning() << "Cannot open log file:" << m_logFileName; + qWarning() << "Cannot open log file:" << logFileName; return false; } + m_file.setTextModeEnabled(true); m_textStream.setDevice(&m_file); + qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); #if !defined(QT_DEBUG) || defined(Q_OS_IOS) qInstallMessageHandler(debugMessageHandler); @@ -99,7 +88,8 @@ void Logger::deInit() m_file.close(); } -bool Logger::setServiceLogsEnabled(bool enabled) { +bool Logger::setServiceLogsEnabled(bool enabled) +{ #ifdef AMNEZIA_DESKTOP IpcClient *m_IpcClient = new IpcClient; @@ -112,8 +102,7 @@ bool Logger::setServiceLogsEnabled(bool enabled) { if (m_IpcClient->Interface()) { m_IpcClient->Interface()->setLogsEnabled(enabled); - } - else { + } else { qWarning() << "Error occurred setting up service logs"; return false; } @@ -127,11 +116,32 @@ QString Logger::userLogsDir() return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/log"; } +QString Logger::systemLogDir() +{ +#ifdef Q_OS_WIN + QStringList locationList = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + QString primaryLocation = "ProgramData"; + foreach (const QString &location, locationList) { + if (location.contains(primaryLocation)) { + return QString("%1/%2/log").arg(location).arg(APPLICATION_NAME); + } + } + return QString(); +#else + return QString("/var/log/%1").arg(APPLICATION_NAME); +#endif +} + QString Logger::userLogsFilePath() { return userLogsDir() + QDir::separator() + m_logFileName; } +QString Logger::serviceLogsFilePath() +{ + return systemLogDir() + QDir::separator() + m_serviceLogFileName; +} + QString Logger::getLogFile() { m_file.flush(); @@ -139,18 +149,32 @@ QString Logger::getLogFile() file.open(QIODevice::ReadOnly); QString qtLog = file.readAll(); - + #ifdef Q_OS_IOS return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); #else return qtLog; #endif - } -bool Logger::openLogsFolder() +QString Logger::getServiceLogFile() { - QString path = userLogsDir(); + m_file.flush(); + QFile file(serviceLogsFilePath()); + + file.open(QIODevice::ReadOnly); + QString qtLog = file.readAll(); + +#ifdef Q_OS_IOS + return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); +#else + return qtLog; +#endif +} + +bool Logger::openLogsFolder(bool isServiceLogger) +{ + QString path = isServiceLogger ? systemLogDir() : userLogsDir(); #ifdef Q_OS_WIN path = "file:///" + path; #endif @@ -161,38 +185,23 @@ bool Logger::openLogsFolder() return true; } -bool Logger::openServiceLogsFolder() -{ - QString path = Utils::systemLogPath(); -#ifdef Q_OS_WIN - path = "file:///" + path; -#endif - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); - return true; -} - -QString Logger::appLogFileNamePath() -{ - return m_file.fileName(); -} - -void Logger::clearLogs() +void Logger::clearLogs(bool isServiceLogger) { bool isLogActive = m_file.isOpen(); m_file.close(); - QFile file(userLogsFilePath()); + QFile file(isServiceLogger ? serviceLogsFilePath() : userLogsFilePath()); file.open(QIODevice::WriteOnly | QIODevice::Truncate); file.resize(0); file.close(); - + #ifdef Q_OS_IOS AmneziaVPN::swiftDeleteLog(); #endif - + if (isLogActive) { - init(); + init(isServiceLogger); } } @@ -210,8 +219,7 @@ void Logger::clearServiceLogs() if (m_IpcClient->Interface()) { m_IpcClient->Interface()->clearLogs(); - } - else { + } else { qWarning() << "Error occurred cleaning up service logs"; } #endif @@ -219,26 +227,41 @@ void Logger::clearServiceLogs() void Logger::cleanUp() { - clearLogs(); + clearLogs(false); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.removeRecursively(); - clearServiceLogs(); + clearLogs(true); } -Logger::Log::Log(Logger* logger, LogLevel logLevel) - : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) {} +Logger::Log::Log(Logger *logger, LogLevel logLevel) : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) +{ +} -Logger::Log::~Log() { +Logger::Log::~Log() +{ qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); delete m_data; } -Logger::Log Logger::error() { return Log(this, LogLevel::Error); } -Logger::Log Logger::warning() { return Log(this, LogLevel::Warning); } -Logger::Log Logger::info() { return Log(this, LogLevel::Info); } -Logger::Log Logger::debug() { return Log(this, LogLevel::Debug); } -QString Logger::sensitive(const QString& input) { +Logger::Log Logger::error() +{ + return Log(this, LogLevel::Error); +} +Logger::Log Logger::warning() +{ + return Log(this, LogLevel::Warning); +} +Logger::Log Logger::info() +{ + return Log(this, LogLevel::Info); +} +Logger::Log Logger::debug() +{ + return Log(this, LogLevel::Debug); +} +QString Logger::sensitive(const QString &input) +{ #ifdef Q_DEBUG return input; #else @@ -247,48 +270,51 @@ QString Logger::sensitive(const QString& input) { #endif } - -#define CREATE_LOG_OP_REF(x) \ -Logger::Log& Logger::Log::operator<<(x t) { \ - m_data->m_ts << t << ' '; \ - return *this; \ -} +#define CREATE_LOG_OP_REF(x) \ + Logger::Log &Logger::Log::operator<<(x t) \ + { \ + m_data->m_ts << t << ' '; \ + return *this; \ + } CREATE_LOG_OP_REF(uint64_t); -CREATE_LOG_OP_REF(const char*); -CREATE_LOG_OP_REF(const QString&); -CREATE_LOG_OP_REF(const QByteArray&); -CREATE_LOG_OP_REF(const void*); +CREATE_LOG_OP_REF(const char *); +CREATE_LOG_OP_REF(const QString &); +CREATE_LOG_OP_REF(const QByteArray &); +CREATE_LOG_OP_REF(const void *); #undef CREATE_LOG_OP_REF -Logger::Log& Logger::Log::operator<<(const QStringList& t) { +Logger::Log &Logger::Log::operator<<(const QStringList &t) +{ m_data->m_ts << '[' << t.join(",") << ']' << ' '; return *this; } -Logger::Log& Logger::Log::operator<<(const QJsonObject& t) { +Logger::Log &Logger::Log::operator<<(const QJsonObject &t) +{ m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; return *this; } -Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { +Logger::Log &Logger::Log::operator<<(QTextStreamFunction t) +{ m_data->m_ts << t; return *this; } -void Logger::Log::addMetaEnum(quint64 value, const QMetaObject* meta, - const char* name) { +void Logger::Log::addMetaEnum(quint64 value, const QMetaObject *meta, const char *name) +{ QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); QString out; QTextStream ts(&out); - if (const char* scope = me.scope()) { + if (const char *scope = me.scope()) { ts << scope << "::"; } - const char* key = me.valueToKey(static_cast(value)); + const char *key = me.valueToKey(static_cast(value)); const bool scoped = me.isScoped(); if (scoped || !key) { ts << me.enumName() << (!key ? "(" : "::"); diff --git a/common/logger/logger.h b/common/logger/logger.h new file mode 100644 index 00000000..7dff7ede --- /dev/null +++ b/common/logger/logger.h @@ -0,0 +1,114 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include +#include +#include + +#include "mozilla/shared/loglevel.h" + +class Logger : public QObject +{ + Q_OBJECT + +public: + static Logger &Instance(); + + static bool init(bool isServiceLogger); + static void deInit(); + + static bool setServiceLogsEnabled(bool enabled); + + static bool openLogsFolder(bool isServiceLogger); + + static void clearLogs(bool isServiceLogger); + static void clearServiceLogs(); + static void cleanUp(); + + static QString userLogsFilePath(); + static QString serviceLogsFilePath(); + static QString systemLogDir(); + + static QString getLogFile(); + static QString getServiceLogFile(); + + // compat with Mozilla logger + Logger(const QString &className) + { + m_className = className; + } + const QString &className() const + { + return m_className; + } + + class Log + { + public: + Log(Logger *logger, LogLevel level); + ~Log(); + + Log &operator<<(uint64_t t); + Log &operator<<(const char *t); + Log &operator<<(const QString &t); + Log &operator<<(const QStringList &t); + Log &operator<<(const QByteArray &t); + Log &operator<<(const QJsonObject &t); + Log &operator<<(QTextStreamFunction t); + Log &operator<<(const void *t); + + // Q_ENUM + template typename std::enable_if::Value, Log &>::type operator<<(T t) + { + const QMetaObject *meta = qt_getEnumMetaObject(t); + const char *name = qt_getEnumName(t); + addMetaEnum(typename QFlags::Int(t), meta, name); + return *this; + } + + private: + void addMetaEnum(quint64 value, const QMetaObject *meta, const char *name); + + Logger *m_logger; + LogLevel m_logLevel; + + struct Data + { + Data() : m_ts(&m_buffer, QIODevice::WriteOnly) + { + } + + QString m_buffer; + QTextStream m_ts; + }; + + Data *m_data; + }; + + Log error(); + Log warning(); + Log info(); + Log debug(); + QString sensitive(const QString &input); + +private: + Logger() {}; + Logger(Logger const &) = delete; + Logger &operator=(Logger const &) = delete; + + static QString userLogsDir(); + + static QFile m_file; + static QTextStream m_textStream; + static QString m_logFileName; + static QString m_serviceLogFileName; + + friend void debugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + // compat with Mozilla logger + QString m_className; +}; + +#endif // LOGGER_H diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 32fc5f30..cee2c398 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -1,32 +1,32 @@ #include "ipcserver.h" -#include #include -#include #include +#include +#include -#include "router.h" #include "logger.h" +#include "router.h" #include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN -#include "tapcontroller_win.h" -#include "../client/platforms/windows/daemon/windowsfirewall.h" -#include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsfirewall.h" + #include "tapcontroller_win.h" #endif #ifdef Q_OS_LINUX -#include "../client/platforms/linux/daemon/linuxfirewall.h" + #include "../client/platforms/linux/daemon/linuxfirewall.h" #endif #ifdef Q_OS_MACOS -#include "../client/platforms/macos/daemon/macosfirewall.h" + #include "../client/platforms/macos/daemon/macosfirewall.h" #endif -IpcServer::IpcServer(QObject *parent): - IpcInterfaceSource(parent) +IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent) -{} +{ +} int IpcServer::createPrivilegedProcess() { @@ -58,23 +58,10 @@ int IpcServer::createPrivilegedProcess() } }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, [pd](QRemoteObjectNode::ErrorCode errorCode) { - qDebug() << "QRemoteObjectHost::error" << errorCode; - }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, + [pd](QRemoteObjectNode::ErrorCode errorCode) { qDebug() << "QRemoteObjectHost::error" << errorCode; }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { - qDebug() << "QRemoteObjectHost::destroyed"; - }); - -// connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, QProcess::ExitStatus exitStatus){ -// qDebug() << "IpcServerProcess finished" << exitCode << exitStatus; -//// if (m_processes.contains(pid)) { -//// m_processes[pid].ipcProcess.reset(); -//// m_processes[pid].serverNode.reset(); -//// m_processes[pid].localServer.reset(); -//// m_processes.remove(pid); -//// } -// }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); m_processes.insert(m_localpid, pd); @@ -105,7 +92,7 @@ bool IpcServer::routeDeleteList(const QString &gw, const QStringList &ips) qDebug() << "IpcServer::routeDeleteList"; #endif - return Router::routeDeleteList(gw ,ips); + return Router::routeDeleteList(gw, ips); } void IpcServer::flushDns() @@ -158,12 +145,13 @@ void IpcServer::cleanUp() qDebug() << "IpcServer::cleanUp"; #endif - Logger::deinit(); + Logger::deInit(); Logger::cleanUp(); } -void IpcServer::clearLogs() { - Logger::clearLogs(); +void IpcServer::clearLogs() +{ + Logger::clearLogs(true); } bool IpcServer::createTun(const QString &dev, const QString &subnet) @@ -176,7 +164,7 @@ bool IpcServer::deleteTun(const QString &dev) return Router::deleteTun(dev); } -bool IpcServer::updateResolvers(const QString& ifname, const QList& resolvers) +bool IpcServer::updateResolvers(const QString &ifname, const QList &resolvers) { return Router::updateResolvers(ifname, resolvers); } @@ -197,14 +185,12 @@ void IpcServer::setLogsEnabled(bool enabled) #endif if (enabled) { - Logger::init(); - } - else { - Logger::deinit(); + Logger::init(true); + } else { + Logger::deInit(); } } - bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { #ifdef Q_OS_WIN @@ -220,13 +206,11 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd QStringList allownets; QStringList blocknets; - if (splitTunnelType == 0) - { + if (splitTunnelType == 0) { blockAll = true; allowNets = true; allownets.append(configStr.value(amnezia::config_key::hostName).toString()); - } else if (splitTunnelType == 1) - { + } else if (splitTunnelType == 1) { blockNets = true; for (auto v : splitTunnelSites) { blocknets.append(v.toString()); @@ -268,18 +252,17 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd // double-check + ensure our firewall is installed and enabled. This is necessary as // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) - if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + if (!MacOSFirewall::isInstalled()) + MacOSFirewall::install(); MacOSFirewall::ensureRootAnchorPriority(); MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), blockAll); MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), allowNets); - MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, - QStringLiteral("allownets"), allownets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, QStringLiteral("allownets"), allownets); MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), blockNets); - MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, - QStringLiteral("blocknets"), blocknets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, QStringLiteral("blocknets"), blocknets); MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); @@ -330,10 +313,8 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) // Use APP split tunnel if (splitTunnelType == 0 || splitTunnelType == 2) { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("0.0.0.0"), 0)); - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("::"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("0.0.0.0"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("::"), 0)); } if (splitTunnelType == 1) { @@ -341,10 +322,9 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) QString ipRange = v.toString(); if (ipRange.split('/').size() > 1) { config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); + IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); } else { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange), 32)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress(ipRange), 32)); } } } @@ -357,7 +337,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) } } - for (const QJsonValue& i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { + for (const QJsonValue &i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { if (!i.isString()) { break; } diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 234dfafe..c41e57f5 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -19,7 +19,7 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h ${CMAKE_CURRENT_LIST_DIR}/localserver.h - ${CMAKE_CURRENT_LIST_DIR}/logger.h + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h ${CMAKE_CURRENT_LIST_DIR}/router.h ${CMAKE_CURRENT_LIST_DIR}/systemservice.h ${CMAKE_CURRENT_BINARY_DIR}/version.h @@ -31,7 +31,7 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp ${CMAKE_CURRENT_LIST_DIR}/localserver.cpp - ${CMAKE_CURRENT_LIST_DIR}/logger.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp ${CMAKE_CURRENT_LIST_DIR}/router.cpp ${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp @@ -238,6 +238,7 @@ include_directories( ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/../../client ${CMAKE_CURRENT_LIST_DIR}/../../ipc + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger ${CMAKE_CURRENT_BINARY_DIR} ) diff --git a/service/server/logger.cpp b/service/server/logger.cpp deleted file mode 100644 index ab658796..00000000 --- a/service/server/logger.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "logger.h" - -#include -#include -#include -#include - -#include - -#include "version.h" -#include "utilities.h" - -QFile Logger::m_file; -QTextStream Logger::m_textStream; -QString Logger::m_logFileName = QString("%1.log").arg(SERVICE_NAME); - -void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) -{ - if (msg.simplified().isEmpty()) { - return; - } - - Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; - - std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; -} - -bool Logger::init() -{ - if (m_file.isOpen()) return true; - - QString path = Utils::systemLogPath(); - QDir appDir(path); - if (!appDir.mkpath(path)) { - return false; - } - - qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); - - m_file.setFileName(appDir.filePath(m_logFileName)); - if (!m_file.open(QIODevice::Append)) { - qWarning() << "Cannot open log file:" << m_logFileName; - return false; - } - m_file.setTextModeEnabled(true); - m_textStream.setDevice(&m_file); - qInstallMessageHandler(debugMessageHandler); - - return true; -} - -void Logger::deinit() -{ - m_file.close(); - m_textStream.setDevice(nullptr); - qInstallMessageHandler(nullptr); -} - -QString Logger::serviceLogFileNamePath() -{ - return m_file.fileName(); -} - -void Logger::clearLogs() -{ - bool isLogActive = m_file.isOpen(); - m_file.close(); - - - QString path = Utils::systemLogPath(); - QDir appDir(path); - QFile file; - file.setFileName(appDir.filePath(m_logFileName)); - - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.resize(0); - file.close(); - - if (isLogActive) { - init(); - } -} - -void Logger::cleanUp() -{ - clearLogs(); - deinit(); - - QString path = Utils::systemLogPath(); - QDir appDir(path); - - { - QFile file; - file.setFileName(appDir.filePath(m_logFileName)); - file.remove(); - } - { - QFile file; - file.setFileName(appDir.filePath("openvpn.log")); - file.remove(); - } - -#ifdef Q_OS_WINDOWS - QDir dir(Utils::systemLogPath()); - dir.removeRecursively(); -#endif -} - - -Logger::Log::Log(Logger* logger, LogLevel logLevel) - : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) {} - -Logger::Log::~Log() { - qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); - delete m_data; -} - -Logger::Log Logger::error() { return Log(this, LogLevel::Error); } -Logger::Log Logger::warning() { return Log(this, LogLevel::Warning); } -Logger::Log Logger::info() { return Log(this, LogLevel::Info); } -Logger::Log Logger::debug() { return Log(this, LogLevel::Debug); } -QString Logger::sensitive(const QString& input) { -#ifdef Q_DEBUG - return input; -#else - Q_UNUSED(input); - return QString(8, 'X'); -#endif -} - - -#define CREATE_LOG_OP_REF(x) \ -Logger::Log& Logger::Log::operator<<(x t) { \ - m_data->m_ts << t << ' '; \ - return *this; \ -} - -CREATE_LOG_OP_REF(uint64_t); -CREATE_LOG_OP_REF(const char*); -CREATE_LOG_OP_REF(const QString&); -CREATE_LOG_OP_REF(const QByteArray&); -CREATE_LOG_OP_REF(const void*); - -#undef CREATE_LOG_OP_REF - -Logger::Log& Logger::Log::operator<<(const QStringList& t) { - m_data->m_ts << '[' << t.join(",") << ']' << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(const QJsonObject& t) { - m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { - m_data->m_ts << t; - return *this; -} - -void Logger::Log::addMetaEnum(quint64 value, const QMetaObject* meta, - const char* name) { - QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); - - QString out; - QTextStream ts(&out); - - if (const char* scope = me.scope()) { - ts << scope << "::"; - } - - const char* key = me.valueToKey(static_cast(value)); - const bool scoped = me.isScoped(); - if (scoped || !key) { - ts << me.enumName() << (!key ? "(" : "::"); - } - - if (key) { - ts << key; - } else { - ts << value << ")"; - } - - m_data->m_ts << out; -} diff --git a/service/server/logger.h b/service/server/logger.h deleted file mode 100644 index bb920931..00000000 --- a/service/server/logger.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include - -#include "mozilla/shared/loglevel.h" - -class Logger -{ -public: - static bool init(); - static void deinit(); - - static QString serviceLogFileNamePath(); - - static void clearLogs(); - static void cleanUp(); - - // compat with Mozilla logger - Logger(const QString &className) { m_className = className; } - const QString& className() const { return m_className; } - - class Log { - public: - Log(Logger* logger, LogLevel level); - ~Log(); - - Log& operator<<(uint64_t t); - Log& operator<<(const char* t); - Log& operator<<(const QString& t); - Log& operator<<(const QStringList& t); - Log& operator<<(const QByteArray& t); - Log& operator<<(const QJsonObject& t); - Log& operator<<(QTextStreamFunction t); - Log& operator<<(const void* t); - - // Q_ENUM - template - typename std::enable_if::Value, Log&>::type - operator<<(T t) { - const QMetaObject* meta = qt_getEnumMetaObject(t); - const char* name = qt_getEnumName(t); - addMetaEnum(typename QFlags::Int(t), meta, name); - return *this; - } - - private: - void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name); - - Logger* m_logger; - LogLevel m_logLevel; - - struct Data { - Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {} - - QString m_buffer; - QTextStream m_ts; - }; - - Data* m_data; - }; - - Log error(); - Log warning(); - Log info(); - Log debug(); - QString sensitive(const QString& input); - -private: - friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - static QFile m_file; - static QString m_logFileName; - static QTextStream m_textStream; - - // compat with Mozilla logger - QString m_className; -}; - -#endif // LOGGER_H diff --git a/service/server/main.cpp b/service/server/main.cpp index 144ddf60..cee33d72 100644 --- a/service/server/main.cpp +++ b/service/server/main.cpp @@ -44,7 +44,7 @@ int runApplication(int argc, char** argv) int main(int argc, char **argv) { - Utils::initializePath(Utils::systemLogPath()); + Utils::initializePath(Logger::systemLogDir()); if (argc >= 2) { qInfo() << "Started as console application"; From 46058f614e9fd8786967de4d4ae2e717cec4fee3 Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 9 Sep 2024 22:08:06 +0300 Subject: [PATCH 12/90] Add connection checking for WG/AWG via logs (#1056) --- .../amnezia/vpn/protocol/openvpn/OpenVpn.kt | 2 +- .../protocolApi/src/main/kotlin/Protocol.kt | 2 +- .../src/org/amnezia/vpn/AmneziaVpnService.kt | 7 +++- .../vpn/protocol/wireguard/Wireguard.kt | 39 ++++++++++++++++++- client/android/xray/src/main/kotlin/Xray.kt | 2 +- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index abe46245..dd096c60 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -58,7 +58,7 @@ open class OpenVpn : Protocol() { scope = CoroutineScope(Dispatchers.IO) } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val configBuilder = OpenVpnConfig.Builder() openVpnClient = OpenVpnClient( diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index a475a2fc..24cbc595 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -42,7 +42,7 @@ abstract class Protocol { protected abstract fun internalInit() - abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) + abstract suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) abstract fun stopVpn() diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index b30f1503..54330861 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.drop @@ -111,6 +112,10 @@ open class AmneziaVpnService : VpnService() { get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME } private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> + connectionJob?.cancel() + connectionJob = null + disconnectionJob?.cancel() + disconnectionJob = null protocolState.value = DISCONNECTED when (e) { is IllegalArgumentException, @@ -531,7 +536,7 @@ open class AmneziaVpnService : VpnService() { protocolState.value = DISCONNECTING disconnectionJob = connectionScope.launch { - connectionJob?.join() + connectionJob?.cancelAndJoin() connectionJob = null vpnProto?.protocol?.stopVpn() diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 690510eb..b5804a6d 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,7 +1,12 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder +import java.io.IOException +import java.util.Locale import java.util.TreeMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED @@ -79,12 +84,44 @@ open class Wireguard : Protocol() { if (!isInitialized) loadSharedLibrary(context, "wg-go") } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) + val startTime = System.currentTimeMillis() start(wireguardConfig, vpnBuilder, protect) + waitForConnection(startTime) state.value = CONNECTED } + private suspend fun waitForConnection(startTime: Long) { + Log.d(TAG, "Waiting for connection") + withContext(Dispatchers.IO) { + val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0) + try { + delay(1000) + var log = getLogcat(time) + Log.d(TAG, "First waiting log: $log") + // check that there is a connection log, + // to avoid infinite connection + if (!log.contains("Attaching to interface")) { + Log.w(TAG, "Logs do not contain a connection log") + return@withContext + } + while (!log.contains("Received handshake response")) { + delay(1000) + log = getLogcat(time) + } + } catch (e: IOException) { + Log.e(TAG, "Failed to get logcat: $e") + } + } + } + + private fun getLogcat(time: String): String = + ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time) + .redirectErrorStream(true) + .start() + .inputStream.reader().readText() + protected open fun parseConfig(config: JSONObject): WireguardConfig { val configDataJson = config.getJSONObject("wireguard_config_data") val configData = parseConfigData(configDataJson.getString("config")) diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index 3e5f9f7c..94afec2a 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -109,7 +109,7 @@ class Xray : Protocol() { } } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { if (isRunning) { Log.w(TAG, "XRay already running") return From 581773ce039c476d0d086e1fb0b01c7705409c4e Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 9 Sep 2024 22:10:48 +0300 Subject: [PATCH 13/90] Bump version to 4.8.0.0 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41e05838..4f22016a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.7.0.0 +project(${PROJECT} VERSION 4.8.0.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 57) +set(APP_ANDROID_VERSION_CODE 58) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 7571bbc36e7023c944427ba2f4917037d3110c65 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 10 Sep 2024 22:03:10 +0400 Subject: [PATCH 14/90] chore: added dev key to deploy workflow - added m_isDevEnvironment initialization --- .github/workflows/deploy.yml | 5 +++++ .github/workflows/tag-deploy.yml | 1 + client/core/controllers/apiController.h | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index be4d068b..e8ad8fdf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,6 +16,7 @@ jobs: QT_VERSION: 6.6.2 QIF_VERSION: 4.7 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} steps: - name: 'Install Qt' @@ -82,6 +83,7 @@ jobs: QIF_VERSION: 4.7 BUILD_ARCH: 64 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} steps: - name: 'Get sources' @@ -144,6 +146,7 @@ jobs: CC: cc CXX: c++ PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} steps: - name: 'Setup xcode' @@ -235,6 +238,7 @@ jobs: QT_VERSION: 6.4.3 QIF_VERSION: 4.6 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} steps: - name: 'Setup xcode' @@ -300,6 +304,7 @@ jobs: QT_VERSION: 6.7.2 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} steps: - name: 'Install desktop Qt' diff --git a/.github/workflows/tag-deploy.yml b/.github/workflows/tag-deploy.yml index e117a6c6..dffb3ab1 100644 --- a/.github/workflows/tag-deploy.yml +++ b/.github/workflows/tag-deploy.yml @@ -16,6 +16,7 @@ jobs: QT_VERSION: 6.4.1 QIF_VERSION: 4.5 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} steps: - name: 'Install desktop Qt' diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index a094233b..1f811498 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -44,7 +44,7 @@ private: QString m_gatewayEndpoint; QStringList m_proxyUrls; - bool m_isDevEnvironment; + bool m_isDevEnvironment = false; }; #endif // APICONTROLLER_H From fff15fffe2b8f98cd2e11e452ba9a546eed4620c Mon Sep 17 00:00:00 2001 From: pokamest Date: Wed, 11 Sep 2024 09:51:07 -0700 Subject: [PATCH 15/90] Bug fix for iOS --- client/platforms/ios/ios_controller.mm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index f4ba2798..6abae584 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -351,8 +351,6 @@ void IosController::vpnStatusDidChange(void *pNotification) } } } - } else { - qDebug() << "Disconnect error is absent"; } }]; } else { @@ -835,7 +833,7 @@ QString IosController::openFile() { void IosController::requestInetAccess() { NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"]; - if (url) { + if (!url) { qDebug() << "IosController::requestInetAccess URL error"; return; } @@ -847,7 +845,6 @@ void IosController::requestInetAccess() { } else { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length); - qDebug() << "IosController::requestInetAccess server response:" << httpResponse.statusCode << "\n\n" < Date: Fri, 13 Sep 2024 12:38:48 +0400 Subject: [PATCH 16/90] feature/mtu connection config (#833) * added the ability to change mtu for connection-only configs * added replacing MTU with default when importing awg/wg configs in amnezia --- .../vpnConfigurationController.cpp | 8 + client/resources.qrc | 2 + client/ui/controllers/importController.cpp | 36 +- client/ui/controllers/importController.h | 2 + client/ui/controllers/pageController.h | 3 + client/ui/models/protocols/awgConfigModel.cpp | 218 +++++++----- client/ui/models/protocols/awgConfigModel.h | 55 +-- .../models/protocols/wireguardConfigModel.cpp | 60 ++-- .../models/protocols/wireguardConfigModel.h | 9 +- client/ui/models/protocols_model.cpp | 30 +- client/ui/models/protocols_model.h | 9 +- .../Pages2/PageProtocolAwgClientSettings.qml | 312 ++++++++++++++++++ .../ui/qml/Pages2/PageProtocolAwgSettings.qml | 113 +++---- .../PageProtocolWireGuardClientSettings.qml | 179 ++++++++++ .../Pages2/PageProtocolWireGuardSettings.qml | 60 ++-- .../qml/Pages2/PageSettingsServerProtocol.qml | 56 +++- 16 files changed, 903 insertions(+), 249 deletions(-) create mode 100644 client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml diff --git a/client/core/controllers/vpnConfigurationController.cpp b/client/core/controllers/vpnConfigurationController.cpp index 818cf57e..291c429c 100644 --- a/client/core/controllers/vpnConfigurationController.cpp +++ b/client/core/controllers/vpnConfigurationController.cpp @@ -101,6 +101,14 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPairserver_scripts/socks5_proxy/Dockerfile server_scripts/socks5_proxy/configure_container.sh server_scripts/socks5_proxy/start.sh + ui/qml/Pages2/PageProtocolAwgClientSettings.qml + ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml ui/qml/Pages2/PageSetupWizardApiServicesList.qml ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml ui/qml/Controls2/CardWithIconsType.qml diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 7ffcedd7..a50e793f 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -173,6 +173,7 @@ bool ImportController::extractConfigFromData(QString data) } case ConfigTypes::Amnezia: { m_config = QJsonDocument::fromJson(config.toUtf8()).object(); + processAmneziaConfig(m_config); if (!m_config.empty()) { checkForMaliciousStrings(m_config); return true; @@ -391,10 +392,6 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) return QJsonObject(); } - if (!configMap.value("MTU").isEmpty()) { - lastConfig[config_key::mtu] = configMap.value("MTU"); - } - QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(",")); lastConfig[config_key::allowed_ips] = allowedIpsJsonArray; @@ -419,6 +416,12 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) m_configType = ConfigTypes::Awg; } + if (!configMap.value("MTU").isEmpty()) { + lastConfig[config_key::mtu] = configMap.value("MTU"); + } else { + lastConfig[config_key::mtu] = protocolName == "awg" ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; + } + QJsonObject wireguardConfig; wireguardConfig[config_key::last_config] = QString(QJsonDocument(lastConfig).toJson()); wireguardConfig[config_key::isThirdPartyConfig] = true; @@ -646,3 +649,28 @@ void ImportController::checkForMaliciousStrings(const QJsonObject &serverConfig) } } } + +void ImportController::processAmneziaConfig(QJsonObject &config) +{ + auto containers = config.value(config_key::containers).toArray(); + for (auto i = 0; i < containers.size(); i++) { + auto container = containers.at(i).toObject(); + auto dockerContainer = ContainerProps::containerFromString(container.value(config_key::container).toString()); + if (dockerContainer == DockerContainer::Awg || dockerContainer == DockerContainer::WireGuard) { + auto containerConfig = container.value(ContainerProps::containerTypeToString(dockerContainer)).toObject(); + auto protocolConfig = containerConfig.value(config_key::last_config).toString(); + if (protocolConfig.isEmpty()) { + return; + } + + QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object(); + jsonConfig[config_key::mtu] = dockerContainer == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; + + containerConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); + + container[ContainerProps::containerTypeToString(dockerContainer)] = containerConfig; + containers.replace(i, container); + config.insert(config_key::containers, containers); + } + } +} diff --git a/client/ui/controllers/importController.h b/client/ui/controllers/importController.h index ea1ba6b0..61205253 100644 --- a/client/ui/controllers/importController.h +++ b/client/ui/controllers/importController.h @@ -68,6 +68,8 @@ private: void checkForMaliciousStrings(const QJsonObject &protocolConfig); + void processAmneziaConfig(QJsonObject &config); + #if defined Q_OS_ANDROID || defined Q_OS_IOS void stopDecodingQr(); #endif diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 2cc2d983..f89d39a1 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -59,6 +59,9 @@ namespace PageLoader PageProtocolIKev2Settings, PageProtocolRaw, + PageProtocolWireGuardClientSettings, + PageProtocolAwgClientSettings, + PageShareFullAccess, PageDevMenu diff --git a/client/ui/models/protocols/awgConfigModel.cpp b/client/ui/models/protocols/awgConfigModel.cpp index 658658df..3a245ebe 100644 --- a/client/ui/models/protocols/awgConfigModel.cpp +++ b/client/ui/models/protocols/awgConfigModel.cpp @@ -21,17 +21,30 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in } switch (role) { - case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break; - case Roles::MtuRole: m_protocolConfig.insert(config_key::mtu, value.toString()); break; - case Roles::JunkPacketCountRole: m_protocolConfig.insert(config_key::junkPacketCount, value.toString()); break; - case Roles::JunkPacketMinSizeRole: m_protocolConfig.insert(config_key::junkPacketMinSize, value.toString()); break; - case Roles::JunkPacketMaxSizeRole: m_protocolConfig.insert(config_key::junkPacketMaxSize, value.toString()); break; - case Roles::InitPacketJunkSizeRole: m_protocolConfig.insert(config_key::initPacketJunkSize, value.toString()); break; - case Roles::ResponsePacketJunkSizeRole: m_protocolConfig.insert(config_key::responsePacketJunkSize, value.toString()); break; - case Roles::InitPacketMagicHeaderRole: m_protocolConfig.insert(config_key::initPacketMagicHeader, value.toString()); break; - case Roles::ResponsePacketMagicHeaderRole: m_protocolConfig.insert(config_key::responsePacketMagicHeader, value.toString()); break; - case Roles::UnderloadPacketMagicHeaderRole: m_protocolConfig.insert(config_key::underloadPacketMagicHeader, value.toString()); break; - case Roles::TransportPacketMagicHeaderRole: m_protocolConfig.insert(config_key::transportPacketMagicHeader, value.toString()); break; + case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break; + + case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break; + case Roles::ClientJunkPacketCountRole: m_clientProtocolConfig.insert(config_key::junkPacketCount, value.toString()); break; + case Roles::ClientJunkPacketMinSizeRole: m_clientProtocolConfig.insert(config_key::junkPacketMinSize, value.toString()); break; + case Roles::ClientJunkPacketMaxSizeRole: m_clientProtocolConfig.insert(config_key::junkPacketMaxSize, value.toString()); break; + + case Roles::ServerJunkPacketCountRole: m_serverProtocolConfig.insert(config_key::junkPacketCount, value.toString()); break; + case Roles::ServerJunkPacketMinSizeRole: m_serverProtocolConfig.insert(config_key::junkPacketMinSize, value.toString()); break; + case Roles::ServerJunkPacketMaxSizeRole: m_serverProtocolConfig.insert(config_key::junkPacketMaxSize, value.toString()); break; + case Roles::ServerInitPacketJunkSizeRole: m_serverProtocolConfig.insert(config_key::initPacketJunkSize, value.toString()); break; + case Roles::ServerResponsePacketJunkSizeRole: + m_serverProtocolConfig.insert(config_key::responsePacketJunkSize, value.toString()); + break; + case Roles::ServerInitPacketMagicHeaderRole: m_serverProtocolConfig.insert(config_key::initPacketMagicHeader, value.toString()); break; + case Roles::ServerResponsePacketMagicHeaderRole: + m_serverProtocolConfig.insert(config_key::responsePacketMagicHeader, value.toString()); + break; + case Roles::ServerUnderloadPacketMagicHeaderRole: + m_serverProtocolConfig.insert(config_key::underloadPacketMagicHeader, value.toString()); + break; + case Roles::ServerTransportPacketMagicHeaderRole: + m_serverProtocolConfig.insert(config_key::transportPacketMagicHeader, value.toString()); + break; } emit dataChanged(index, index, QList { role }); @@ -45,17 +58,22 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const } switch (role) { - case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(); - case Roles::MtuRole: return m_protocolConfig.value(config_key::mtu).toString(); - case Roles::JunkPacketCountRole: return m_protocolConfig.value(config_key::junkPacketCount); - case Roles::JunkPacketMinSizeRole: return m_protocolConfig.value(config_key::junkPacketMinSize); - case Roles::JunkPacketMaxSizeRole: return m_protocolConfig.value(config_key::junkPacketMaxSize); - case Roles::InitPacketJunkSizeRole: return m_protocolConfig.value(config_key::initPacketJunkSize); - case Roles::ResponsePacketJunkSizeRole: return m_protocolConfig.value(config_key::responsePacketJunkSize); - case Roles::InitPacketMagicHeaderRole: return m_protocolConfig.value(config_key::initPacketMagicHeader); - case Roles::ResponsePacketMagicHeaderRole: return m_protocolConfig.value(config_key::responsePacketMagicHeader); - case Roles::UnderloadPacketMagicHeaderRole: return m_protocolConfig.value(config_key::underloadPacketMagicHeader); - case Roles::TransportPacketMagicHeaderRole: return m_protocolConfig.value(config_key::transportPacketMagicHeader); + case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString(); + + case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu); + case Roles::ClientJunkPacketCountRole: return m_clientProtocolConfig.value(config_key::junkPacketCount); + case Roles::ClientJunkPacketMinSizeRole: return m_clientProtocolConfig.value(config_key::junkPacketMinSize); + case Roles::ClientJunkPacketMaxSizeRole: return m_clientProtocolConfig.value(config_key::junkPacketMaxSize); + + case Roles::ServerJunkPacketCountRole: return m_serverProtocolConfig.value(config_key::junkPacketCount); + case Roles::ServerJunkPacketMinSizeRole: return m_serverProtocolConfig.value(config_key::junkPacketMinSize); + case Roles::ServerJunkPacketMaxSizeRole: return m_serverProtocolConfig.value(config_key::junkPacketMaxSize); + case Roles::ServerInitPacketJunkSizeRole: return m_serverProtocolConfig.value(config_key::initPacketJunkSize); + case Roles::ServerResponsePacketJunkSizeRole: return m_serverProtocolConfig.value(config_key::responsePacketJunkSize); + case Roles::ServerInitPacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::initPacketMagicHeader); + case Roles::ServerResponsePacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::responsePacketMagicHeader); + case Roles::ServerUnderloadPacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::underloadPacketMagicHeader); + case Roles::ServerTransportPacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::transportPacketMagicHeader); } return QVariant(); @@ -68,51 +86,63 @@ void AwgConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; - QJsonObject protocolConfig = config.value(config_key::awg).toObject(); + QJsonObject serverProtocolConfig = config.value(config_key::awg).toObject(); auto defaultTransportProto = ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::Awg), Proto::Awg); - m_protocolConfig.insert(config_key::transport_proto, protocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); - m_protocolConfig[config_key::last_config] = protocolConfig.value(config_key::last_config); - m_protocolConfig[config_key::port] = protocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); - m_protocolConfig[config_key::mtu] = protocolConfig.value(config_key::mtu).toString(protocols::awg::defaultMtu); - m_protocolConfig[config_key::junkPacketCount] = - protocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); - m_protocolConfig[config_key::junkPacketMinSize] = - protocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); - m_protocolConfig[config_key::junkPacketMaxSize] = - protocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); - m_protocolConfig[config_key::initPacketJunkSize] = - protocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); - m_protocolConfig[config_key::responsePacketJunkSize] = - protocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); - m_protocolConfig[config_key::initPacketMagicHeader] = - protocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); - m_protocolConfig[config_key::responsePacketMagicHeader] = - protocolConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader); - m_protocolConfig[config_key::underloadPacketMagicHeader] = - protocolConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader); - m_protocolConfig[config_key::transportPacketMagicHeader] = - protocolConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader); + m_serverProtocolConfig.insert(config_key::transport_proto, + serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config); + m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); + m_serverProtocolConfig[config_key::junkPacketCount] = + serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); + m_serverProtocolConfig[config_key::junkPacketMinSize] = + serverProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); + m_serverProtocolConfig[config_key::junkPacketMaxSize] = + serverProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + m_serverProtocolConfig[config_key::initPacketJunkSize] = + serverProtocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); + m_serverProtocolConfig[config_key::responsePacketJunkSize] = + serverProtocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); + m_serverProtocolConfig[config_key::initPacketMagicHeader] = + serverProtocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); + m_serverProtocolConfig[config_key::responsePacketMagicHeader] = + serverProtocolConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader); + m_serverProtocolConfig[config_key::underloadPacketMagicHeader] = + serverProtocolConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader); + m_serverProtocolConfig[config_key::transportPacketMagicHeader] = + serverProtocolConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + m_clientProtocolConfig[config_key::mtu] = clientProtocolConfig[config_key::mtu].toString(protocols::awg::defaultMtu); + m_clientProtocolConfig[config_key::junkPacketCount] = + clientProtocolConfig.value(config_key::junkPacketCount).toString(m_serverProtocolConfig[config_key::junkPacketCount].toString()); + m_clientProtocolConfig[config_key::junkPacketMinSize] = + clientProtocolConfig.value(config_key::junkPacketMinSize).toString(m_serverProtocolConfig[config_key::junkPacketMinSize].toString()); + m_clientProtocolConfig[config_key::junkPacketMaxSize] = + clientProtocolConfig.value(config_key::junkPacketMaxSize).toString(m_serverProtocolConfig[config_key::junkPacketMaxSize].toString()); endResetModel(); } QJsonObject AwgConfigModel::getConfig() { const AwgConfig oldConfig(m_fullConfig.value(config_key::awg).toObject()); - const AwgConfig newConfig(m_protocolConfig); + const AwgConfig newConfig(m_serverProtocolConfig); if (!oldConfig.hasEqualServerSettings(newConfig)) { - m_protocolConfig.remove(config_key::last_config); + m_serverProtocolConfig.remove(config_key::last_config); } else { - auto lastConfig = m_protocolConfig.value(config_key::last_config).toString(); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); QJsonObject jsonConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); - jsonConfig[config_key::mtu] = newConfig.mtu; + jsonConfig[config_key::mtu] = m_clientProtocolConfig[config_key::mtu]; + jsonConfig[config_key::junkPacketCount] = m_clientProtocolConfig[config_key::junkPacketCount]; + jsonConfig[config_key::junkPacketMinSize] = m_clientProtocolConfig[config_key::junkPacketMinSize]; + jsonConfig[config_key::junkPacketMaxSize] = m_clientProtocolConfig[config_key::junkPacketMaxSize]; - m_protocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); + m_serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); } - m_fullConfig.insert(config_key::awg, m_protocolConfig); + m_fullConfig.insert(config_key::awg, m_serverProtocolConfig); return m_fullConfig; } @@ -126,50 +156,73 @@ bool AwgConfigModel::isPacketSizeEqual(const int s1, const int s2) return (AwgConstant::messageInitiationSize + s1 == AwgConstant::messageResponseSize + s2); } +bool AwgConfigModel::isServerSettingsEqual() +{ + const AwgConfig oldConfig(m_fullConfig.value(config_key::awg).toObject()); + const AwgConfig newConfig(m_serverProtocolConfig); + + return oldConfig.hasEqualServerSettings(newConfig); +} + QHash AwgConfigModel::roleNames() const { QHash roles; roles[PortRole] = "port"; - roles[MtuRole] = "mtu"; - roles[JunkPacketCountRole] = "junkPacketCount"; - roles[JunkPacketMinSizeRole] = "junkPacketMinSize"; - roles[JunkPacketMaxSizeRole] = "junkPacketMaxSize"; - roles[InitPacketJunkSizeRole] = "initPacketJunkSize"; - roles[ResponsePacketJunkSizeRole] = "responsePacketJunkSize"; - roles[InitPacketMagicHeaderRole] = "initPacketMagicHeader"; - roles[ResponsePacketMagicHeaderRole] = "responsePacketMagicHeader"; - roles[UnderloadPacketMagicHeaderRole] = "underloadPacketMagicHeader"; - roles[TransportPacketMagicHeaderRole] = "transportPacketMagicHeader"; + + roles[ClientMtuRole] = "clientMtu"; + roles[ClientJunkPacketCountRole] = "clientJunkPacketCount"; + roles[ClientJunkPacketMinSizeRole] = "clientJunkPacketMinSize"; + roles[ClientJunkPacketMaxSizeRole] = "clientJunkPacketMaxSize"; + + roles[ServerJunkPacketCountRole] = "serverJunkPacketCount"; + roles[ServerJunkPacketMinSizeRole] = "serverJunkPacketMinSize"; + roles[ServerJunkPacketMaxSizeRole] = "serverJunkPacketMaxSize"; + roles[ServerInitPacketJunkSizeRole] = "serverInitPacketJunkSize"; + roles[ServerResponsePacketJunkSizeRole] = "serverResponsePacketJunkSize"; + roles[ServerInitPacketMagicHeaderRole] = "serverInitPacketMagicHeader"; + roles[ServerResponsePacketMagicHeaderRole] = "serverResponsePacketMagicHeader"; + roles[ServerUnderloadPacketMagicHeaderRole] = "serverUnderloadPacketMagicHeader"; + roles[ServerTransportPacketMagicHeaderRole] = "serverTransportPacketMagicHeader"; return roles; } -AwgConfig::AwgConfig(const QJsonObject &jsonConfig) +AwgConfig::AwgConfig(const QJsonObject &serverProtocolConfig) { - port = jsonConfig.value(config_key::port).toString(protocols::awg::defaultPort); - mtu = jsonConfig.value(config_key::mtu).toString(protocols::awg::defaultMtu); - junkPacketCount = jsonConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); - junkPacketMinSize = jsonConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); - junkPacketMaxSize = jsonConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); - initPacketJunkSize = jsonConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); - responsePacketJunkSize = jsonConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); - initPacketMagicHeader = jsonConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); - responsePacketMagicHeader = - jsonConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader); - underloadPacketMagicHeader = - jsonConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader); - transportPacketMagicHeader = - jsonConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader); + auto lastConfig = serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + clientMtu = clientProtocolConfig[config_key::mtu].toString(protocols::awg::defaultMtu); + clientJunkPacketCount = clientProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); + clientJunkPacketMinSize = clientProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); + clientJunkPacketMaxSize = clientProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + + port = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); + serverJunkPacketCount = serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); + serverJunkPacketMinSize = serverProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); + serverJunkPacketMaxSize = serverProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + serverInitPacketJunkSize = serverProtocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); + serverResponsePacketJunkSize = + serverProtocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); + serverInitPacketMagicHeader = + serverProtocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); + serverResponsePacketMagicHeader = + serverProtocolConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader); + serverUnderloadPacketMagicHeader = + serverProtocolConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader); + serverTransportPacketMagicHeader = + serverProtocolConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader); } bool AwgConfig::hasEqualServerSettings(const AwgConfig &other) const { - if (port != other.port || junkPacketCount != other.junkPacketCount || junkPacketMinSize != other.junkPacketMinSize - || junkPacketMaxSize != other.junkPacketMaxSize || initPacketJunkSize != other.initPacketJunkSize - || responsePacketJunkSize != other.responsePacketJunkSize || initPacketMagicHeader != other.initPacketMagicHeader - || responsePacketMagicHeader != other.responsePacketMagicHeader || underloadPacketMagicHeader != other.underloadPacketMagicHeader - || transportPacketMagicHeader != other.transportPacketMagicHeader) { + if (port != other.port || serverJunkPacketCount != other.serverJunkPacketCount + || serverJunkPacketMinSize != other.serverJunkPacketMinSize || serverJunkPacketMaxSize != other.serverJunkPacketMaxSize + || serverInitPacketJunkSize != other.serverInitPacketJunkSize || serverResponsePacketJunkSize != other.serverResponsePacketJunkSize + || serverInitPacketMagicHeader != other.serverInitPacketMagicHeader + || serverResponsePacketMagicHeader != other.serverResponsePacketMagicHeader + || serverUnderloadPacketMagicHeader != other.serverUnderloadPacketMagicHeader + || serverTransportPacketMagicHeader != other.serverTransportPacketMagicHeader) { return false; } return true; @@ -177,7 +230,8 @@ bool AwgConfig::hasEqualServerSettings(const AwgConfig &other) const bool AwgConfig::hasEqualClientSettings(const AwgConfig &other) const { - if (mtu != other.mtu) { + if (clientMtu != other.clientMtu || clientJunkPacketCount != other.clientJunkPacketCount + || clientJunkPacketMinSize != other.clientJunkPacketMinSize || clientJunkPacketMaxSize != other.clientJunkPacketMaxSize) { return false; } return true; diff --git a/client/ui/models/protocols/awgConfigModel.h b/client/ui/models/protocols/awgConfigModel.h index 80375d38..06475bf5 100644 --- a/client/ui/models/protocols/awgConfigModel.h +++ b/client/ui/models/protocols/awgConfigModel.h @@ -16,16 +16,21 @@ struct AwgConfig AwgConfig(const QJsonObject &jsonConfig); QString port; - QString mtu; - QString junkPacketCount; - QString junkPacketMinSize; - QString junkPacketMaxSize; - QString initPacketJunkSize; - QString responsePacketJunkSize; - QString initPacketMagicHeader; - QString responsePacketMagicHeader; - QString underloadPacketMagicHeader; - QString transportPacketMagicHeader; + + QString clientMtu; + QString clientJunkPacketCount; + QString clientJunkPacketMinSize; + QString clientJunkPacketMaxSize; + + QString serverJunkPacketCount; + QString serverJunkPacketMinSize; + QString serverJunkPacketMaxSize; + QString serverInitPacketJunkSize; + QString serverResponsePacketJunkSize; + QString serverInitPacketMagicHeader; + QString serverResponsePacketMagicHeader; + QString serverUnderloadPacketMagicHeader; + QString serverTransportPacketMagicHeader; bool hasEqualServerSettings(const AwgConfig &other) const; bool hasEqualClientSettings(const AwgConfig &other) const; @@ -39,16 +44,21 @@ class AwgConfigModel : public QAbstractListModel public: enum Roles { PortRole = Qt::UserRole + 1, - MtuRole, - JunkPacketCountRole, - JunkPacketMinSizeRole, - JunkPacketMaxSizeRole, - InitPacketJunkSizeRole, - ResponsePacketJunkSizeRole, - InitPacketMagicHeaderRole, - ResponsePacketMagicHeaderRole, - UnderloadPacketMagicHeaderRole, - TransportPacketMagicHeaderRole + + ClientMtuRole, + ClientJunkPacketCountRole, + ClientJunkPacketMinSizeRole, + ClientJunkPacketMaxSizeRole, + + ServerJunkPacketCountRole, + ServerJunkPacketMinSizeRole, + ServerJunkPacketMaxSizeRole, + ServerInitPacketJunkSizeRole, + ServerResponsePacketJunkSizeRole, + ServerInitPacketMagicHeaderRole, + ServerResponsePacketMagicHeaderRole, + ServerUnderloadPacketMagicHeaderRole, + ServerTransportPacketMagicHeaderRole }; explicit AwgConfigModel(QObject *parent = nullptr); @@ -65,12 +75,15 @@ public slots: bool isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4); bool isPacketSizeEqual(const int s1, const int s2); + bool isServerSettingsEqual(); + protected: QHash roleNames() const override; private: DockerContainer m_container; - QJsonObject m_protocolConfig; + QJsonObject m_serverProtocolConfig; + QJsonObject m_clientProtocolConfig; QJsonObject m_fullConfig; }; diff --git a/client/ui/models/protocols/wireguardConfigModel.cpp b/client/ui/models/protocols/wireguardConfigModel.cpp index 65bf2bb3..555915de 100644 --- a/client/ui/models/protocols/wireguardConfigModel.cpp +++ b/client/ui/models/protocols/wireguardConfigModel.cpp @@ -21,8 +21,8 @@ bool WireGuardConfigModel::setData(const QModelIndex &index, const QVariant &val } switch (role) { - case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break; - case Roles::MtuRole: m_protocolConfig.insert(config_key::mtu, value.toString()); break; + case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break; + case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break; } emit dataChanged(index, index, QList { role }); @@ -36,8 +36,8 @@ QVariant WireGuardConfigModel::data(const QModelIndex &index, int role) const } switch (role) { - case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(); - case Roles::MtuRole: return m_protocolConfig.value(config_key::mtu).toString(); + case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString(); + case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu); } return QVariant(); @@ -49,17 +49,18 @@ void WireGuardConfigModel::updateModel(const QJsonObject &config) m_container = ContainerProps::containerFromString(config.value(config_key::container).toString()); m_fullConfig = config; - QJsonObject protocolConfig = config.value(config_key::wireguard).toObject(); + QJsonObject serverProtocolConfig = config.value(config_key::wireguard).toObject(); - auto defaultTransportProto = ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::WireGuard), Proto::WireGuard); - m_protocolConfig.insert(config_key::transport_proto, - protocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); - m_protocolConfig[config_key::last_config] = protocolConfig.value(config_key::last_config); - m_protocolConfig[config_key::port] = - protocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); + auto defaultTransportProto = + ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::WireGuard), Proto::WireGuard); + m_serverProtocolConfig.insert(config_key::transport_proto, + serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config); + m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); - m_protocolConfig[config_key::mtu] = - protocolConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + m_clientProtocolConfig[config_key::mtu] = clientProtocolConfig[config_key::mtu].toString(protocols::wireguard::defaultMtu); endResetModel(); } @@ -67,36 +68,47 @@ void WireGuardConfigModel::updateModel(const QJsonObject &config) QJsonObject WireGuardConfigModel::getConfig() { const WgConfig oldConfig(m_fullConfig.value(config_key::wireguard).toObject()); - const WgConfig newConfig(m_protocolConfig); + const WgConfig newConfig(m_serverProtocolConfig); if (!oldConfig.hasEqualServerSettings(newConfig)) { - m_protocolConfig.remove(config_key::last_config); + m_serverProtocolConfig.remove(config_key::last_config); } else { - auto lastConfig = m_protocolConfig.value(config_key::last_config).toString(); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); QJsonObject jsonConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); - jsonConfig[config_key::mtu] = newConfig.mtu; + jsonConfig[config_key::mtu] = m_clientProtocolConfig[config_key::mtu]; - m_protocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); + m_serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); } - m_fullConfig.insert(config_key::wireguard, m_protocolConfig); + m_fullConfig.insert(config_key::wireguard, m_serverProtocolConfig); return m_fullConfig; } +bool WireGuardConfigModel::isServerSettingsEqual() +{ + const WgConfig oldConfig(m_fullConfig.value(config_key::wireguard).toObject()); + const WgConfig newConfig(m_serverProtocolConfig); + + return oldConfig.hasEqualServerSettings(newConfig); +} + QHash WireGuardConfigModel::roleNames() const { QHash roles; roles[PortRole] = "port"; - roles[MtuRole] = "mtu"; + roles[ClientMtuRole] = "clientMtu"; return roles; } -WgConfig::WgConfig(const QJsonObject &jsonConfig) +WgConfig::WgConfig(const QJsonObject &serverProtocolConfig) { - port = jsonConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); - mtu = jsonConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); + auto lastConfig = serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + clientMtu = clientProtocolConfig[config_key::mtu].toString(protocols::wireguard::defaultMtu); + + port = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); } bool WgConfig::hasEqualServerSettings(const WgConfig &other) const @@ -109,7 +121,7 @@ bool WgConfig::hasEqualServerSettings(const WgConfig &other) const bool WgConfig::hasEqualClientSettings(const WgConfig &other) const { - if (mtu != other.mtu) { + if (clientMtu != other.clientMtu) { return false; } return true; diff --git a/client/ui/models/protocols/wireguardConfigModel.h b/client/ui/models/protocols/wireguardConfigModel.h index 6cec76dd..a02bea5a 100644 --- a/client/ui/models/protocols/wireguardConfigModel.h +++ b/client/ui/models/protocols/wireguardConfigModel.h @@ -11,7 +11,7 @@ struct WgConfig WgConfig(const QJsonObject &jsonConfig); QString port; - QString mtu; + QString clientMtu; bool hasEqualServerSettings(const WgConfig &other) const; bool hasEqualClientSettings(const WgConfig &other) const; @@ -25,7 +25,7 @@ class WireGuardConfigModel : public QAbstractListModel public: enum Roles { PortRole = Qt::UserRole + 1, - MtuRole + ClientMtuRole }; explicit WireGuardConfigModel(QObject *parent = nullptr); @@ -39,12 +39,15 @@ public slots: void updateModel(const QJsonObject &config); QJsonObject getConfig(); + bool isServerSettingsEqual(); + protected: QHash roleNames() const override; private: DockerContainer m_container; - QJsonObject m_protocolConfig; + QJsonObject m_serverProtocolConfig; + QJsonObject m_clientProtocolConfig; QJsonObject m_fullConfig; }; diff --git a/client/ui/models/protocols_model.cpp b/client/ui/models/protocols_model.cpp index 32447cd4..019b2d2f 100644 --- a/client/ui/models/protocols_model.cpp +++ b/client/ui/models/protocols_model.cpp @@ -16,9 +16,11 @@ QHash ProtocolsModel::roleNames() const QHash roles; roles[ProtocolNameRole] = "protocolName"; - roles[ProtocolPageRole] = "protocolPage"; + roles[ServerProtocolPageRole] = "serverProtocolPage"; + roles[ClientProtocolPageRole] = "clientProtocolPage"; roles[ProtocolIndexRole] = "protocolIndex"; roles[RawConfigRole] = "rawConfig"; + roles[IsClientProtocolExistsRole] = "isClientProtocolExists"; return roles; } @@ -34,8 +36,10 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const amnezia::Proto proto = ProtocolProps::protoFromString(m_content.keys().at(index.row())); return ProtocolProps::protocolHumanNames().value(proto); } - case ProtocolPageRole: - return static_cast(protocolPage(ProtocolProps::protoFromString(m_content.keys().at(index.row())))); + case ServerProtocolPageRole: + return static_cast(serverProtocolPage(ProtocolProps::protoFromString(m_content.keys().at(index.row())))); + case ClientProtocolPageRole: + return static_cast(clientProtocolPage(ProtocolProps::protoFromString(m_content.keys().at(index.row())))); case ProtocolIndexRole: return ProtocolProps::protoFromString(m_content.keys().at(index.row())); case RawConfigRole: { auto protocolConfig = m_content.value(ContainerProps::containerTypeToString(m_container)).toObject(); @@ -50,6 +54,15 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const } return rawConfig; } + case IsClientProtocolExistsRole: { + auto protocolConfig = m_content.value(ContainerProps::containerTypeToString(m_container)).toObject(); + auto lastConfigJsonDoc = + QJsonDocument::fromJson(protocolConfig.value(config_key::last_config).toString().toUtf8()); + auto lastConfigJson = lastConfigJsonDoc.object(); + + auto configString = lastConfigJson.value(config_key::config).toString(); + return !configString.isEmpty(); + } } return QVariant(); @@ -70,7 +83,7 @@ QJsonObject ProtocolsModel::getConfig() return config; } -PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const +PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const { switch (protocol) { case Proto::OpenVpn: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; @@ -90,3 +103,12 @@ PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; } } + +PageLoader::PageEnum ProtocolsModel::clientProtocolPage(Proto protocol) const +{ + switch (protocol) { + case Proto::WireGuard: return PageLoader::PageEnum::PageProtocolWireGuardClientSettings; + case Proto::Awg: return PageLoader::PageEnum::PageProtocolAwgClientSettings; + default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; + } +} diff --git a/client/ui/models/protocols_model.h b/client/ui/models/protocols_model.h index 5ee8a3dd..5c52ee86 100644 --- a/client/ui/models/protocols_model.h +++ b/client/ui/models/protocols_model.h @@ -13,9 +13,11 @@ class ProtocolsModel : public QAbstractListModel public: enum Roles { ProtocolNameRole = Qt::UserRole + 1, - ProtocolPageRole, + ServerProtocolPageRole, + ClientProtocolPageRole, ProtocolIndexRole, - RawConfigRole + RawConfigRole, + IsClientProtocolExistsRole }; ProtocolsModel(std::shared_ptr settings, QObject *parent = nullptr); @@ -33,7 +35,8 @@ protected: QHash roleNames() const override; private: - PageLoader::PageEnum protocolPage(Proto protocol) const; + PageLoader::PageEnum serverProtocolPage(Proto protocol) const; + PageLoader::PageEnum clientProtocolPage(Proto protocol) const; std::shared_ptr m_settings; diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml new file mode 100644 index 00000000..2b912f18 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -0,0 +1,312 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + + +PageType { + id: root + + defaultActiveFocusItem: listview.currentItem.mtuTextField.textField + + Item { + id: focusItem + onFocusChanged: { + if (activeFocus) { + fl.ensureVisible(focusItem) + } + } + KeyNavigation.tab: backButton + } + + ColumnLayout { + id: backButtonLayout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.mtuTextField.textField + } + } + + FlickableType { + id: fl + anchors.top: backButtonLayout.bottom + anchors.bottom: parent.bottom + contentHeight: content.implicitHeight + saveButton.implicitHeight + saveButton.anchors.bottomMargin + saveButton.anchors.topMargin + + Column { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + ListView { + id: listview + + width: parent.width + height: listview.contentItem.height + + clip: true + interactive: false + + model: AwgConfigModel + + delegate: Item { + id: delegateItem + implicitWidth: listview.width + implicitHeight: col.implicitHeight + + property alias mtuTextField: mtuTextField + property bool isSaveButtonEnabled: mtuTextField.errorText === "" && + junkPacketMaxSizeTextField.errorText === "" && + junkPacketMinSizeTextField.errorText === "" && + junkPacketCountTextField.errorText === "" + + ColumnLayout { + id: col + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("AmneziaWG settings") + } + + TextFieldWithHeaderType { + id: mtuTextField + Layout.fillWidth: true + Layout.topMargin: 40 + + headerText: qsTr("MTU") + textFieldText: clientMtu + textField.validator: IntValidator { bottom: 576; top: 65535 } + + textField.onEditingFinished: { + if (textFieldText !== clientMtu) { + clientMtu = textFieldText + } + } + checkEmptyText: true + KeyNavigation.tab: junkPacketCountTextField.textField + } + + TextFieldWithHeaderType { + id: junkPacketCountTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: "Jc - Junk packet count" + textFieldText: clientJunkPacketCount + textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl + + textField.onEditingFinished: { + if (textFieldText !== clientJunkPacketCount) { + clientJunkPacketCount = textFieldText + } + } + + checkEmptyText: true + + KeyNavigation.tab: junkPacketMinSizeTextField.textField + } + + TextFieldWithHeaderType { + id: junkPacketMinSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: "Jmin - Junk packet minimum size" + textFieldText: clientJunkPacketMinSize + textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl + + textField.onEditingFinished: { + if (textFieldText !== clientJunkPacketMinSize) { + clientJunkPacketMinSize = textFieldText + } + } + + checkEmptyText: true + + KeyNavigation.tab: junkPacketMaxSizeTextField.textField + } + + TextFieldWithHeaderType { + id: junkPacketMaxSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: "Jmax - Junk packet maximum size" + textFieldText: clientJunkPacketMaxSize + textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl + + textField.onEditingFinished: { + if (textFieldText !== clientJunkPacketMaxSize) { + clientJunkPacketMaxSize = textFieldText + } + } + + checkEmptyText: true + + Keys.onTabPressed: saveButton.forceActiveFocus() + } + + Header2TextType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Server settings") + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 8 + + enabled: false + + headerText: qsTr("Port") + textFieldText: port + } + + TextFieldWithHeaderType { + id: initPacketJunkSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "S1 - Init packet junk size" + textFieldText: serverInitPacketJunkSize + } + + TextFieldWithHeaderType { + id: responsePacketJunkSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "S2 - Response packet junk size" + textFieldText: serverResponsePacketJunkSize + } + + TextFieldWithHeaderType { + id: initPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H1 - Init packet magic header" + textFieldText: serverInitPacketMagicHeader + } + + TextFieldWithHeaderType { + id: responsePacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H2 - Response packet magic header" + textFieldText: serverResponsePacketMagicHeader + } + + TextFieldWithHeaderType { + id: underloadPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + parentFlickable: fl + + enabled: false + + headerText: "H3 - Underload packet magic header" + textFieldText: serverUnderloadPacketMagicHeader + } + + TextFieldWithHeaderType { + id: transportPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H4 - Transport packet magic header" + textFieldText: serverTransportPacketMagicHeader + } + } + } + } + } + } + + BasicButtonType { + id: saveButton + + anchors.right: root.right + anchors.left: root.left + anchors.bottom: root.bottom + + anchors.topMargin: 24 + anchors.bottomMargin: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + enabled: listview.currentItem.isSaveButtonEnabled + + text: qsTr("Save") + + Keys.onTabPressed: lastItemTabClicked(focusItem) + + clickedFunc: function() { + forceActiveFocus() + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("Only the settings for this device will be changed") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(AwgConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index 8651fa27..27ea66f9 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -57,8 +57,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right - enabled: ServersModel.isProcessedServerHasWriteAccess() - ListView { id: listview @@ -71,12 +69,12 @@ PageType { model: AwgConfigModel delegate: Item { - id: _delegate - + id: delegateItem implicitWidth: listview.width implicitHeight: col.implicitHeight - property alias portTextField:portTextField + property alias portTextField: portTextField + property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() ColumnLayout { id: col @@ -101,6 +99,8 @@ PageType { Layout.fillWidth: true Layout.topMargin: 40 + enabled: delegateItem.isEnabled + headerText: qsTr("Port") textFieldText: port textField.maximumLength: 5 @@ -115,27 +115,6 @@ PageType { checkEmptyText: true - KeyNavigation.tab: mtuTextField.textField - } - - TextFieldWithHeaderType { - id: mtuTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: qsTr("MTU") - textFieldText: mtu - textField.validator: IntValidator { bottom: 576; top: 65535 } - - textField.onEditingFinished: { - if (textFieldText === "") { - textFieldText = "0" - } - if (textFieldText !== mtu) { - mtu = textFieldText - } - } - checkEmptyText: true KeyNavigation.tab: junkPacketCountTextField.textField } @@ -145,7 +124,7 @@ PageType { Layout.topMargin: 16 headerText: qsTr("Jc - Junk packet count") - textFieldText: junkPacketCount + textFieldText: serverJunkPacketCount textField.validator: IntValidator { bottom: 0 } parentFlickable: fl @@ -154,8 +133,8 @@ PageType { textFieldText = "0" } - if (textFieldText !== junkPacketCount) { - junkPacketCount = textFieldText + if (textFieldText !== serverJunkPacketCount) { + serverJunkPacketCount = textFieldText } } @@ -170,13 +149,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("Jmin - Junk packet minimum size") - textFieldText: junkPacketMinSize + textFieldText: serverJunkPacketMinSize textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== junkPacketMinSize) { - junkPacketMinSize = textFieldText + if (textFieldText !== serverJunkPacketMinSize) { + serverJunkPacketMinSize = textFieldText } } @@ -191,13 +170,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("Jmax - Junk packet maximum size") - textFieldText: junkPacketMaxSize + textFieldText: serverJunkPacketMaxSize textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== junkPacketMaxSize) { - junkPacketMaxSize = textFieldText + if (textFieldText !== serverJunkPacketMaxSize) { + serverJunkPacketMaxSize = textFieldText } } @@ -212,13 +191,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("S1 - Init packet junk size") - textFieldText: initPacketJunkSize + textFieldText: serverInitPacketJunkSize textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== initPacketJunkSize) { - initPacketJunkSize = textFieldText + if (textFieldText !== serverInitPacketJunkSize) { + serverInitPacketJunkSize = textFieldText } } @@ -233,13 +212,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("S2 - Response packet junk size") - textFieldText: responsePacketJunkSize + textFieldText: serverResponsePacketJunkSize textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== responsePacketJunkSize) { - responsePacketJunkSize = textFieldText + if (textFieldText !== serverResponsePacketJunkSize) { + serverResponsePacketJunkSize = textFieldText } } @@ -254,13 +233,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("H1 - Init packet magic header") - textFieldText: initPacketMagicHeader + textFieldText: serverInitPacketMagicHeader textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== initPacketMagicHeader) { - initPacketMagicHeader = textFieldText + if (textFieldText !== serverInitPacketMagicHeader) { + serverInitPacketMagicHeader = textFieldText } } @@ -275,13 +254,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("H2 - Response packet magic header") - textFieldText: responsePacketMagicHeader + textFieldText: serverResponsePacketMagicHeader textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== responsePacketMagicHeader) { - responsePacketMagicHeader = textFieldText + if (textFieldText !== serverResponsePacketMagicHeader) { + serverResponsePacketMagicHeader = textFieldText } } @@ -296,13 +275,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("H4 - Transport packet magic header") - textFieldText: transportPacketMagicHeader + textFieldText: serverTransportPacketMagicHeader textField.validator: IntValidator { bottom: 0 } parentFlickable: fl textField.onEditingFinished: { - if (textFieldText !== transportPacketMagicHeader) { - transportPacketMagicHeader = textFieldText + if (textFieldText !== serverTransportPacketMagicHeader) { + serverTransportPacketMagicHeader = textFieldText } } @@ -318,12 +297,12 @@ PageType { parentFlickable: fl headerText: qsTr("H3 - Underload packet magic header") - textFieldText: underloadPacketMagicHeader + textFieldText: serverUnderloadPacketMagicHeader textField.validator: IntValidator { bottom: 0 } textField.onEditingFinished: { - if (textFieldText !== underloadPacketMagicHeader) { - underloadPacketMagicHeader = textFieldText + if (textFieldText !== serverUnderloadPacketMagicHeader) { + serverUnderloadPacketMagicHeader = textFieldText } } @@ -356,18 +335,22 @@ PageType { Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { - if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text, - transportPacketMagicHeaderTextField.textField.text, - responsePacketMagicHeaderTextField.textField.text, - initPacketMagicHeaderTextField.textField.text)) { - PageController.showErrorMessage(qsTr("The values of the H1-H4 fields must be unique")) - return - } + forceActiveFocus() - if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text), - parseInt(responsePacketJunkSizeTextField.textField.text))) { - PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)")) - return + if (delegateItem.isEnabled) { + if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text, + transportPacketMagicHeaderTextField.textField.text, + responsePacketMagicHeaderTextField.textField.text, + initPacketMagicHeaderTextField.textField.text)) { + PageController.showErrorMessage(qsTr("The values of the H1-H4 fields must be unique")) + return + } + + if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text), + parseInt(responsePacketJunkSizeTextField.textField.text))) { + PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)")) + return + } } var headerText = qsTr("Save settings?") @@ -376,8 +359,6 @@ PageType { var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { - forceActiveFocus() - if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) return diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml new file mode 100644 index 00000000..007de5ca --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + + +PageType { + id: root + + defaultActiveFocusItem: listview.currentItem.mtuTextField.textField + + Item { + id: focusItem + onFocusChanged: { + if (activeFocus) { + fl.ensureVisible(focusItem) + } + } + KeyNavigation.tab: backButton + } + + ColumnLayout { + id: backButtonLayout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.mtuTextField.textField + } + } + + FlickableType { + id: fl + anchors.top: backButtonLayout.bottom + anchors.bottom: parent.bottom + contentHeight: content.implicitHeight + saveButton.implicitHeight + saveButton.anchors.bottomMargin + saveButton.anchors.topMargin + + Column { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + ListView { + id: listview + + width: parent.width + height: listview.contentItem.height + + clip: true + interactive: false + + model: WireGuardConfigModel + + delegate: Item { + id: delegateItem + implicitWidth: listview.width + implicitHeight: col.implicitHeight + + property alias mtuTextField: mtuTextField + property bool isSaveButtonEnabled: mtuTextField.errorText === "" + + ColumnLayout { + id: col + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("WG settings") + } + + TextFieldWithHeaderType { + id: mtuTextField + Layout.fillWidth: true + Layout.topMargin: 40 + + headerText: qsTr("MTU") + textFieldText: clientMtu + textField.validator: IntValidator { bottom: 576; top: 65535 } + + textField.onEditingFinished: { + if (textFieldText !== clientMtu) { + clientMtu = textFieldText + } + } + checkEmptyText: true + KeyNavigation.tab: saveButton + } + + Header2TextType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Server settings") + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 8 + + enabled: false + + headerText: qsTr("Port") + textFieldText: port + } + } + } + } + } + } + + BasicButtonType { + id: saveButton + + anchors.right: root.right + anchors.left: root.left + anchors.bottom: root.bottom + + anchors.topMargin: 24 + anchors.bottomMargin: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + enabled: listview.currentItem.isSaveButtonEnabled + + text: qsTr("Save") + + Keys.onTabPressed: lastItemTabClicked(focusItem) + + clickedFunc: function() { + forceActiveFocus() + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("Only the settings for this device will be changed") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(WireGuardConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml index 758375b1..b5d08132 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml @@ -72,7 +72,10 @@ PageType { } delegate: Item { + id: delegateItem + property alias focusItemId: portTextField.textField + property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() implicitWidth: listview.width implicitHeight: col.implicitHeight @@ -99,12 +102,14 @@ PageType { Layout.fillWidth: true Layout.topMargin: 40 + enabled: delegateItem.isEnabled + headerText: qsTr("Port") textFieldText: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } - KeyNavigation.tab: mtuTextField.textField + KeyNavigation.tab: saveButton textField.onEditingFinished: { if (textFieldText !== port) { @@ -115,52 +120,41 @@ PageType { checkEmptyText: true } - TextFieldWithHeaderType { - id: mtuTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: qsTr("MTU") - textFieldText: mtu - textField.validator: IntValidator { bottom: 576; top: 65535 } - - KeyNavigation.tab: saveButton - - textField.onEditingFinished: { - if (textFieldText === "") { - textFieldText = "0" - } - if (textFieldText !== mtu) { - mtu = textFieldText - } - } - checkEmptyText: true - } - BasicButtonType { id: saveButton Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 - enabled: mtuTextField.errorText === "" && - portTextField.errorText === "" + enabled: portTextField.errorText === "" text: qsTr("Save") Keys.onTabPressed: lastItemTabClicked(focusItem) - onClicked: { + onClicked: function() { forceActiveFocus() - if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) - return - } + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(WireGuardConfigModel.getConfig()) - focusItem.forceActiveFocus() + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(WireGuardConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveRestartButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } Keys.onEnterPressed: saveButton.clicked() diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index 6410156d..dcdf01af 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -79,7 +79,7 @@ PageType { } delegate: Item { - property var focusItem: button.rightButton + property var focusItem: clientSettings.rightButton implicitWidth: protocols.width implicitHeight: delegateContent.implicitHeight @@ -89,13 +89,49 @@ PageType { anchors.fill: parent + property bool isClientSettingsVisible: protocolIndex === ProtocolEnum.WireGuard || protocolIndex === ProtocolEnum.Awg + property bool isServerSettingsVisible: ServersModel.isProcessedServerHasWriteAccess() + LabelWithButtonType { - id: button + id: clientSettings Layout.fillWidth: true - text: protocolName + text: protocolName + qsTr(" connection settings") rightImageSource: "qrc:/images/controls/chevron-right.svg" + visible: delegateContent.isClientSettingsVisible + + clickedFunction: function() { + if (isClientProtocolExists) { + switch (protocolIndex) { + case ProtocolEnum.WireGuard: WireGuardConfigModel.updateModel(ProtocolsModel.getConfig()); break; + case ProtocolEnum.Awg: AwgConfigModel.updateModel(ProtocolsModel.getConfig()); break; + } + PageController.goToPage(clientProtocolPage); + } else { + PageController.showNotificationMessage(qsTr("Click the \"connect\" button to create a connection configuration")) + } + } + + MouseArea { + anchors.fill: clientSettings + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + + DividerType { + visible: delegateContent.isClientSettingsVisible + } + + LabelWithButtonType { + id: serverSettings + + Layout.fillWidth: true + + text: protocolName + qsTr(" server settings") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + visible: delegateContent.isServerSettingsVisible clickedFunction: function() { switch (protocolIndex) { @@ -109,17 +145,19 @@ PageType { case ProtocolEnum.Ipsec: Ikev2ConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Socks5Proxy: Socks5ProxyConfigModel.updateModel(ProtocolsModel.getConfig()); break; } - PageController.goToPage(protocolPage); + PageController.goToPage(serverProtocolPage); } MouseArea { - anchors.fill: button + anchors.fill: serverSettings cursorShape: Qt.PointingHandCursor enabled: false } } - DividerType {} + DividerType { + visible: delegateContent.isServerSettingsVisible + } } } } @@ -132,11 +170,11 @@ PageType { visible: root.isClearCacheVisible KeyNavigation.tab: removeButton - text: qsTr("Clear %1 profile").arg(ContainersModel.getProcessedContainerName()) + text: qsTr("Clear profile") clickedFunction: function() { var headerText = qsTr("Clear %1 profile?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("") + var descriptionText = qsTr("The connection configuration will be deleted for this device only") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") @@ -183,7 +221,7 @@ PageType { visible: ServersModel.isProcessedServerHasWriteAccess() Keys.onTabPressed: lastItemTabClicked(focusItem) - text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() + text: qsTr("Remove ") textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { From aae3cdcac1bb95ee6c05cc25b0b2a0afabc010e7 Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 13 Sep 2024 13:53:21 +0400 Subject: [PATCH 17/90] added saving allowed_ips to the array of strings for old configs (#926) * added saving allowed_ips to the array of strings for old configs * Remove config string processing, add getting all AWG, WG parameters from JSON * fixed checking of default routes when adding split tunneling from the application * added check when processing siteBasedSplitTunneling --- client/android/awg/src/main/kotlin/Awg.kt | 77 +++------------- client/android/cloak/src/main/kotlin/Cloak.kt | 30 ------ .../amnezia/vpn/protocol/openvpn/OpenVpn.kt | 17 ---- .../android/utils/src/main/kotlin/JsonExt.kt | 9 ++ .../vpn/protocol/wireguard/Wireguard.kt | 92 +++++-------------- client/android/xray/src/main/kotlin/Xray.kt | 63 ------------- .../configurators/wireguard_configurator.cpp | 4 + client/mozilla/localsocketcontroller.cpp | 2 +- client/ui/controllers/importController.cpp | 10 +- client/vpnconnection.cpp | 89 ++++++++++++------ 10 files changed, 120 insertions(+), 273 deletions(-) create mode 100644 client/android/utils/src/main/kotlin/JsonExt.kt diff --git a/client/android/awg/src/main/kotlin/Awg.kt b/client/android/awg/src/main/kotlin/Awg.kt index f4175763..fbd1cce0 100644 --- a/client/android/awg/src/main/kotlin/Awg.kt +++ b/client/android/awg/src/main/kotlin/Awg.kt @@ -1,81 +1,28 @@ package org.amnezia.vpn.protocol.awg import org.amnezia.vpn.protocol.wireguard.Wireguard +import org.amnezia.vpn.util.optStringOrNull import org.json.JSONObject -/** - * Config example: - * { - * "protocol": "awg", - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "splitTunnelSites": [ - * ], - * "splitTunnelType": 0, - * "awg_config_data": { - * "H1": "969537490", - * "H2": "481688153", - * "H3": "2049399200", - * "H4": "52029755", - * "Jc": "3", - * "Jmax": "1000", - * "Jmin": "50", - * "S1": "49", - * "S2": "60", - * "client_ip": "10.8.1.1", - * "hostName": "100.100.100.0", - * "port": 12345, - * "client_pub_key": "clientPublicKeyBase64", - * "client_priv_key": "privateKeyBase64", - * "psk_key": "presharedKeyBase64", - * "server_pub_key": "publicKeyBase64", - * "config": "[Interface] - * Address = 10.8.1.1/32 - * DNS = 1.1.1.1, 1.0.0.1 - * PrivateKey = privateKeyBase64 - * Jc = 3 - * Jmin = 50 - * Jmax = 1000 - * S1 = 49 - * S2 = 60 - * H1 = 969537490 - * H2 = 481688153 - * H3 = 2049399200 - * H4 = 52029755 - * - * [Peer] - * PublicKey = publicKeyBase64 - * PresharedKey = presharedKeyBase64 - * AllowedIPs = 0.0.0.0/0, ::/0 - * Endpoint = 100.100.100.0:12345 - * PersistentKeepalive = 25 - * " - * } - * } - */ - class Awg : Wireguard() { override val ifName: String = "awg0" override fun parseConfig(config: JSONObject): AwgConfig { - val configDataJson = config.getJSONObject("awg_config_data") - val configData = parseConfigData(configDataJson.getString("config")) + val configData = config.getJSONObject("awg_config_data") return AwgConfig.build { - configWireguard(configData, configDataJson) + configWireguard(config, configData) configSplitTunneling(config) configAppSplitTunneling(config) - configData["Jc"]?.let { setJc(it.toInt()) } - configData["Jmin"]?.let { setJmin(it.toInt()) } - configData["Jmax"]?.let { setJmax(it.toInt()) } - configData["S1"]?.let { setS1(it.toInt()) } - configData["S2"]?.let { setS2(it.toInt()) } - configData["H1"]?.let { setH1(it.toLong()) } - configData["H2"]?.let { setH2(it.toLong()) } - configData["H3"]?.let { setH3(it.toLong()) } - configData["H4"]?.let { setH4(it.toLong()) } + configData.optStringOrNull("Jc")?.let { setJc(it.toInt()) } + configData.optStringOrNull("Jmin")?.let { setJmin(it.toInt()) } + configData.optStringOrNull("Jmax")?.let { setJmax(it.toInt()) } + configData.optStringOrNull("S1")?.let { setS1(it.toInt()) } + configData.optStringOrNull("S2")?.let { setS2(it.toInt()) } + configData.optStringOrNull("H1")?.let { setH1(it.toLong()) } + configData.optStringOrNull("H2")?.let { setH2(it.toLong()) } + configData.optStringOrNull("H3")?.let { setH3(it.toLong()) } + configData.optStringOrNull("H4")?.let { setH4(it.toLong()) } } } } diff --git a/client/android/cloak/src/main/kotlin/Cloak.kt b/client/android/cloak/src/main/kotlin/Cloak.kt index 5a549130..18a5e6c7 100644 --- a/client/android/cloak/src/main/kotlin/Cloak.kt +++ b/client/android/cloak/src/main/kotlin/Cloak.kt @@ -5,36 +5,6 @@ import net.openvpn.ovpn3.ClientAPI_Config import org.amnezia.vpn.protocol.openvpn.OpenVpn import org.json.JSONObject -/** - * Config Example: - * { - * "protocol": "cloak", - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "splitTunnelSites": [ - * ], - * "splitTunnelType": 0, - * "openvpn_config_data": { - * "config": "openVpnConfig" - * } - * "cloak_config_data": { - * "BrowserSig": "chrome", - * "EncryptionMethod": "aes-gcm", - * "NumConn": 1, - * "ProxyMethod": "openvpn", - * "PublicKey": "PublicKey=", - * "RemoteHost": "100.100.100.0", - * "RemotePort": "443", - * "ServerName": "servername", - * "StreamTimeout": 300, - * "Transport": "direct", - * "UID": "UID=" - * } - * } - */ - class Cloak : OpenVpn() { override fun parseConfig(config: JSONObject): ClientAPI_Config { diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index dd096c60..fa0e19f1 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -16,23 +16,6 @@ import org.amnezia.vpn.util.net.getLocalNetworks import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject -/** - * Config Example: - * { - * "protocol": "openvpn", - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "splitTunnelSites": [ - * ], - * "splitTunnelType": 0, - * "openvpn_config_data": { - * "config": "openVpnConfig" - * } - * } - */ - open class OpenVpn : Protocol() { private var openVpnClient: OpenVpnClient? = null diff --git a/client/android/utils/src/main/kotlin/JsonExt.kt b/client/android/utils/src/main/kotlin/JsonExt.kt new file mode 100644 index 00000000..45c5bacd --- /dev/null +++ b/client/android/utils/src/main/kotlin/JsonExt.kt @@ -0,0 +1,9 @@ +package org.amnezia.vpn.util + +import org.json.JSONArray +import org.json.JSONObject + +inline fun JSONArray.asSequence(): Sequence = + (0.., configDataJson: JSONObject) { - configData["Address"]?.split(",")?.map { address -> + protected fun WireguardConfig.Builder.configWireguard(config: JSONObject, configData: JSONObject) { + configData.getString("client_ip").split(",").map { address -> InetNetwork.parse(address.trim()) - }?.forEach(::addAddress) + }.forEach(::addAddress) - configData["DNS"]?.split(",")?.map { dns -> - parseInetAddress(dns.trim()) - }?.forEach(::addDnsServer) + config.optStringOrNull("dns1")?.let { dns -> + addDnsServer(parseInetAddress(dns.trim())) + } + + config.optStringOrNull("dns2")?.let { dns -> + addDnsServer(parseInetAddress(dns.trim())) + } val defRoutes = hashSetOf( InetNetwork("0.0.0.0", 0), InetNetwork("::", 0) ) val routes = hashSetOf() - configData["AllowedIPs"]?.split(",")?.map { route -> + configData.getJSONArray("allowed_ips").asSequence().map { route -> InetNetwork.parse(route.trim()) - }?.forEach(routes::add) + }.forEach(routes::add) // if the allowed IPs list contains at least one non-default route, disable global split tunneling if (routes.any { it !in defRoutes }) disableSplitTunneling() addRoutes(routes) - configDataJson.optString("mtu").let { mtu -> - if (mtu.isNotEmpty()) { - setMtu(mtu.toInt()) - } else { - configData["MTU"]?.let { setMtu(it.toInt()) } - } - } + configData.optStringOrNull("mtu")?.let { setMtu(it.toInt()) } - configData["Endpoint"]?.let { setEndpoint(InetEndpoint.parse(it)) } - configData["PersistentKeepalive"]?.let { setPersistentKeepalive(it.toInt()) } - configData["PrivateKey"]?.let { setPrivateKeyHex(it.base64ToHex()) } - configData["PublicKey"]?.let { setPublicKeyHex(it.base64ToHex()) } - configData["PresharedKey"]?.let { setPreSharedKeyHex(it.base64ToHex()) } - } + val host = configData.getString("hostName").let { parseInetAddress(it.trim()) } + val port = configData.getInt("port") + setEndpoint(InetEndpoint(host, port)) - protected fun parseConfigData(data: String): Map { - val parsedData = TreeMap(String.CASE_INSENSITIVE_ORDER) - data.lineSequence() - .filter { it.isNotEmpty() && !it.startsWith('[') } - .forEach { line -> - val attr = line.split("=", limit = 2) - parsedData[attr.first().trim()] = attr.last().trim() - } - return parsedData + configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) } + 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()) } } private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) { diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index 94afec2a..ec22a4bd 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -20,69 +20,6 @@ import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject -/** - * Config example: - * { - * "appSplitTunnelType": 0, - * "config_version": 0, - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "protocol": "xray", - * "splitTunnelApps": [], - * "splitTunnelSites": [], - * "splitTunnelType": 0, - * "xray_config_data": { - * "inbounds": [ - * { - * "listen": "127.0.0.1", - * "port": 8080, - * "protocol": "socks", - * "settings": { - * "udp": true - * } - * } - * ], - * "log": { - * "loglevel": "error" - * }, - * "outbounds": [ - * { - * "protocol": "vless", - * "settings": { - * "vnext": [ - * { - * "address": "100.100.100.0", - * "port": 443, - * "users": [ - * { - * "encryption": "none", - * "flow": "xtls-rprx-vision", - * "id": "id" - * } - * ] - * } - * ] - * }, - * "streamSettings": { - * "network": "tcp", - * "realitySettings": { - * "fingerprint": "chrome", - * "publicKey": "publicKey", - * "serverName": "google.com", - * "shortId": "id", - * "spiderX": "" - * }, - * "security": "reality" - * } - * } - * ] - * } - * } - * - */ - private const val TAG = "Xray" private const val LIBXRAY_TAG = "libXray" diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index f7faaa52..27db92ff 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -187,6 +187,10 @@ QString WireguardConfigurator::createConfig(const ServerCredentials &credentials jConfig[config_key::server_pub_key] = connData.serverPubKey; jConfig[config_key::mtu] = wireguarConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); + jConfig[config_key::persistent_keep_alive] = 25; + QJsonArray allowedIps { "0.0.0.0/0", "::/0" }; + jConfig[config_key::allowed_ips] = allowedIps; + jConfig[config_key::clientId] = connData.clientPubKey; return QJsonDocument(jConfig).toJson(); diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 0502facc..4d040288 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -149,7 +149,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { QJsonArray jsAllowedIPAddesses; QJsonArray plainAllowedIP = wgConfig.value(amnezia::config_key::allowed_ips).toArray(); - QJsonArray defaultAllowedIP = QJsonArray::fromStringList(QString("0.0.0.0/0, ::/0").split(",")); + QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" }; if (plainAllowedIP != defaultAllowedIP && !plainAllowedIP.isEmpty()) { // Use AllowedIP list from WG config because of higher priority diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index a50e793f..f64ba36e 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -392,7 +392,15 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) return QJsonObject(); } - QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(",")); + if (!configMap.value("MTU").isEmpty()) { + lastConfig[config_key::mtu] = configMap.value("MTU"); + } + + if (!configMap.value("PersistentKeepalive").isEmpty()) { + lastConfig[config_key::persistent_keep_alive] = configMap.value("PersistentKeepalive"); + } + + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(", ")); lastConfig[config_key::allowed_ips] = allowedIpsJsonArray; diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index daff1187..c4022be6 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -1,16 +1,16 @@ #include "qtimer.h" #include +#include #include #include #include -#include +#include "core/controllers/serverController.h" #include #include #include #include -#include "core/controllers/serverController.h" #ifdef AMNEZIA_DESKTOP #include "core/ipcclient.h" @@ -34,8 +34,7 @@ VpnConnection::VpnConnection(std::shared_ptr settings, QObject *parent { m_checkTimer.setInterval(1000); #ifdef Q_OS_IOS - connect(IosController::Instance(), &IosController::connectionStateChanged, this, - &VpnConnection::onConnectionStateChanged); + connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged); connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged); #endif @@ -58,7 +57,7 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) #ifdef AMNEZIA_DESKTOP QString proto = m_settings->defaultContainerName(m_settings->defaultServerIndex()); - + if (IpcClient::Interface()) { if (state == Vpn::ConnectionState::Connected) { IpcClient::Interface()->resetIpStack(); @@ -72,7 +71,7 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) if (m_settings->isSitesSplitTunnelingEnabled()) { IpcClient::Interface()->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); - // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); + // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { QTimer::singleShot(1000, m_vpnProtocol.data(), [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); @@ -291,27 +290,62 @@ void VpnConnection::appendKillSwitchConfig() void VpnConnection::appendSplitTunnelingConfig() { - if (m_vpnConfiguration.value(config_key::configVersion).toInt()) { - auto protocolName = m_vpnConfiguration.value(config_key::vpnproto).toString(); - if (protocolName == ProtocolProps::protoToString(Proto::Awg)) { - auto configData = m_vpnConfiguration.value(protocolName + "_config_data").toObject(); - QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configData.value("allowed_ips").toString().split(",")); - QJsonArray defaultAllowedIP = QJsonArray::fromStringList(QString("0.0.0.0/0, ::/0").split(",")); + bool allowSiteBasedSplitTunneling = true; - if (allowedIpsJsonArray != defaultAllowedIP) { - allowedIpsJsonArray.append(m_vpnConfiguration.value(config_key::dns1).toString()); - allowedIpsJsonArray.append(m_vpnConfiguration.value(config_key::dns2).toString()); - - m_vpnConfiguration.insert(config_key::splitTunnelType, Settings::RouteMode::VpnOnlyForwardSites); - m_vpnConfiguration.insert(config_key::splitTunnelSites, allowedIpsJsonArray); + // this block is for old native configs and for old self-hosted configs + auto protocolName = m_vpnConfiguration.value(config_key::vpnproto).toString(); + if (protocolName == ProtocolProps::protoToString(Proto::Awg) || protocolName == ProtocolProps::protoToString(Proto::WireGuard)) { + allowSiteBasedSplitTunneling = false; + auto configData = m_vpnConfiguration.value(protocolName + "_config_data").toObject(); + if (configData.value(config_key::allowed_ips).isString()) { + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configData.value(config_key::allowed_ips).toString().split(", ")); + configData.insert(config_key::allowed_ips, allowedIpsJsonArray); + m_vpnConfiguration.insert(protocolName + "_config_data", configData); + } else if (configData.value(config_key::allowed_ips).isUndefined()) { + auto nativeConfig = configData.value(config_key::config).toString(); + auto nativeConfigLines = nativeConfig.split("\n"); + for (auto &line : nativeConfigLines) { + if (line.contains("AllowedIPs")) { + auto allowedIpsString = line.split(" = "); + if (allowedIpsString.size() < 1) { + break; + } + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(allowedIpsString.at(1).split(", ")); + configData.insert(config_key::allowed_ips, allowedIpsJsonArray); + m_vpnConfiguration.insert(protocolName + "_config_data", configData); + break; + } } } - } else { - Settings::RouteMode routeMode = Settings::RouteMode::VpnAllSites; - QJsonArray sitesJsonArray; - if (m_settings->isSitesSplitTunnelingEnabled()) { - routeMode = m_settings->routeMode(); + if (configData.value(config_key::persistent_keep_alive).isUndefined()) { + auto nativeConfig = configData.value(config_key::config).toString(); + auto nativeConfigLines = nativeConfig.split("\n"); + for (auto &line : nativeConfigLines) { + if (line.contains("PersistentKeepalive")) { + auto persistentKeepaliveString = line.split(" = "); + if (persistentKeepaliveString.size() < 1) { + break; + } + configData.insert(config_key::persistent_keep_alive, persistentKeepaliveString.at(1)); + m_vpnConfiguration.insert(protocolName + "_config_data", configData); + break; + } + } + } + + QJsonArray allowedIpsJsonArray = configData.value(config_key::allowed_ips).toArray(); + if (allowedIpsJsonArray.contains("0.0.0.0/0") && allowedIpsJsonArray.contains("::/0")) { + allowSiteBasedSplitTunneling = true; + } + } + + Settings::RouteMode routeMode = Settings::RouteMode::VpnAllSites; + QJsonArray sitesJsonArray; + if (m_settings->isSitesSplitTunnelingEnabled()) { + routeMode = m_settings->routeMode(); + + if (allowSiteBasedSplitTunneling) { auto sites = m_settings->getVpnIps(routeMode); for (const auto &site : sites) { sitesJsonArray.append(site); @@ -323,11 +357,11 @@ void VpnConnection::appendSplitTunnelingConfig() sitesJsonArray.append(m_vpnConfiguration.value(config_key::dns2).toString()); } } - - m_vpnConfiguration.insert(config_key::splitTunnelType, routeMode); - m_vpnConfiguration.insert(config_key::splitTunnelSites, sitesJsonArray); } + m_vpnConfiguration.insert(config_key::splitTunnelType, routeMode); + m_vpnConfiguration.insert(config_key::splitTunnelSites, sitesJsonArray); + Settings::AppsRouteMode appsRouteMode = Settings::AppsRouteMode::VpnAllApps; QJsonArray appsJsonArray; if (m_settings->isAppsSplitTunnelingEnabled()) { @@ -359,8 +393,7 @@ void VpnConnection::createAndroidConnections() connect(AndroidController::instance(), &AndroidController::connectionStateChanged, androidVpnProtocol, &AndroidVpnProtocol::setConnectionState); - connect(AndroidController::instance(), &AndroidController::statisticsUpdated, androidVpnProtocol, - &AndroidVpnProtocol::setBytesChanged); + connect(AndroidController::instance(), &AndroidController::statisticsUpdated, androidVpnProtocol, &AndroidVpnProtocol::setBytesChanged); } AndroidVpnProtocol *VpnConnection::createDefaultAndroidVpnProtocol() From eebf7eccec1d13d70b821e6ae72886c455ca0bb0 Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 13 Sep 2024 18:14:25 +0300 Subject: [PATCH 18/90] Fix window hiding on startup on Android --- client/ui/controllers/pageController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 9daca272..bbcc55a1 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -112,7 +112,7 @@ void PageController::showOnStartup() if (!m_settings->isStartMinimized()) { emit raiseMainWindow(); } else { -#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) +#if defined(Q_OS_WIN) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) emit hideMainWindow(); #elif defined Q_OS_MACX setDockIconVisible(false); From c6b131aa4cc45fb8c7bf267c62537d322577e3fc Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 13 Sep 2024 18:25:04 +0100 Subject: [PATCH 19/90] Bump version to 4.8.0.1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f22016a..287d92f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.0.0 +project(${PROJECT} VERSION 4.8.0.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 58) +set(APP_ANDROID_VERSION_CODE 59) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 76e5039578b6d0e97c43d160a1642fe4feb03cef Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Sun, 15 Sep 2024 11:09:59 +0100 Subject: [PATCH 20/90] Update translations --- client/translations/amneziavpn_ar_EG.ts | 438 +++++++++++++++++------ client/translations/amneziavpn_fa_IR.ts | 436 +++++++++++++++++------ client/translations/amneziavpn_hi_IN.ts | 440 +++++++++++++++++------ client/translations/amneziavpn_my_MM.ts | 440 +++++++++++++++++------ client/translations/amneziavpn_ru_RU.ts | 436 +++++++++++++++++------ client/translations/amneziavpn_uk_UA.ts | 446 ++++++++++++++++++------ client/translations/amneziavpn_ur_PK.ts | 442 +++++++++++++++++------ client/translations/amneziavpn_zh_CN.ts | 446 +++++++++++++++++------- 8 files changed, 2622 insertions(+), 902 deletions(-) diff --git a/client/translations/amneziavpn_ar_EG.ts b/client/translations/amneziavpn_ar_EG.ts index be37275f..e176d8eb 100644 --- a/client/translations/amneziavpn_ar_EG.ts +++ b/client/translations/amneziavpn_ar_EG.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s شبكة VPN كلاسيكية للعمل المريح وتنزيل الملفات الكبيرة ومشاهدة مقاطع الفيديو. تعمل مع أي موقع. تصل السرعة إلى %1 ميجابت/ثانية - + VPN to access blocked sites in regions with high levels of Internet censorship. شبكة VPN للولوج للمواقع المحظورة في بلاد ذو مستوي عالي من الرقابة علي الانترنت. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amenzia Premium - شبكة VPN للعمل المريح, تحميل ملفات كبيرة الحجم, ومشاهدة مقاطع الفيديو ب جودة عالية. تعمل لجميع المواقع, حتي في البلاد ذو مستوي عالي من الرقابة علي الانترنت - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free هو VPN مجاني لتخطي الحظر في البلاد ذو مستوي عالي من الرقابة علي الانترنت - + %1 MBit/s %1 ميجابت/ثانية - + %1 days %1 ايام - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> سيقوم VPN فقط بفتح المواقع المشهورة المحظورة في بلدك, مثل Instagram, Facebook, Twitter و مواقع اخري. المواقع الاخري ستٌفتح من عنوان ال IP الحقيقي الخاص بك, <a href="%1/free" style="color: #FBB26A;">معلومات اخري علي الموقع.</a> - + Free مجاني - + %1 $/month %1 دولار/الشهر @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation غير قادر علي قطع الاتصال اثناء إعداد التكوين @@ -186,9 +191,8 @@ ExportController - Access error! - خطأ في الوصول! + خطأ في الوصول! @@ -254,18 +258,18 @@ Can't be disabled for current server غير قادر علي فتح الملف - - + + Invalid configuration file ملف تكوين غير صحيح - + Scanned %1 of %2. تم فحص%1 من %2. - + In the imported configuration, potentially dangerous lines were found: في التكوين المستورد، تم العثور على سطور يحتمل أن تكون خطرة: @@ -443,6 +447,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint نقطة نهاية البوابة + + + Dev gateway environment + + PageHome @@ -477,10 +486,63 @@ Already installed containers were found on the server. All installed containers لا يمكن تغير الخادم بينما هناك اتصال مفعل + + PageProtocolAwgClientSettings + + + AmneziaWG settings + اعدادات AmneziaWG + + + + MTU + + + + + Server settings + + + + + Port + منفذ + + + + Save + احفظ + + + + Save settings? + احفظ الإعدادات؟ + + + + Only the settings for this device will be changed + + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + + PageProtocolAwgSettings - + AmneziaWG settings اعدادات AmneziaWG @@ -490,92 +552,87 @@ Already installed containers were found on the server. All installed containers منفذ - - MTU - - - - + All users with whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - + Save احفظ - + Jc - Junk packet count Jc - عدد الحزم غير المرغوب فيها - + Jmin - Junk packet minimum size Jmin - الحجم الادني للحزم الغير مرغوب فيها - + Jmax - Junk packet maximum size Jmax - الحجم الاقصي للحزم الغير مرغوب فيها - + S1 - Init packet junk size S1 - حجم حزمة البيانات العشوائية الأولية - + S2 - Response packet junk size S2 - حجم حزمة الاستجابة غير المرغوب فيها - + H1 - Init packet magic header H1 - حزمة رأس سحرية مبدئية - + H2 - Response packet magic header H2 - رأس حزمة الاستجابة السحرية - + H4 - Transport packet magic header H4 - رأس حزمة النقل السحرية - + H3 - Underload packet magic header H3 - رأس حزمة السحر غير المحمل - + The values of the H1-H4 fields must be unique يجب أن تكون قيم الحقول H1-H4 فريدة - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) يجب ألا تساوي قيمة الحقل S1 + حجم بدء الرسالة (148) S2 + حجم استجابة الرسالة (92) - + Save settings? احفظ الإعدادات؟ - + Continue واصل - + Cancel إلغاء - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -862,30 +919,98 @@ Already installed containers were found on the server. All installed containers لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + + PageProtocolWireGuardClientSettings + + + WG settings + إعدادات WG + + + + MTU + + + + + Server settings + + + + + Port + منفذ + + + + Save + احفظ + + + + Save settings? + احفظ الإعدادات؟ + + + + Only the settings for this device will be changed + + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + + PageProtocolWireGuardSettings - + WG settings إعدادات WG - + Port منفذ - - MTU - + + Save settings? + احفظ الإعدادات؟ - + + All users with whom you shared a connection with will no longer be able to connect to it. + جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. + + + + Continue + واصل + + + + Cancel + إلغاء + + + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط - + Save احفظ @@ -1205,9 +1330,13 @@ Already installed containers were found on the server. All installed containers - Mail - البريد + البريد + + + + support@amnezia.org + @@ -1215,32 +1344,37 @@ Already installed containers were found on the server. All installed containers لل مراجعات والابلاغات عن المشاكل - + + Copied + + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client - + Website موقع - + Software version: %1 %1 :إصدار البرنامج - + Check for updates تحقق من وجود تحديثات - + Privacy Policy سياسات الخصوصية @@ -1689,72 +1823,108 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - تم تمكين التسجيل. لاحظ أنه سيتم تعطيل السجلات تلقائيًا بعد 14 يومًا، وسيتم حذف جميع ملفات السجل. + تم تمكين التسجيل. لاحظ أنه سيتم تعطيل السجلات تلقائيًا بعد 14 يومًا، وسيتم حذف جميع ملفات السجل. - + Logging التسجيل - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. سيتم حفظ سجلات البرنامج بشكل تلقائي عند تفعيل هذه الميزة, بشكل افتراضي, هذه الميزة مٌعطلة. قم بتفعيل هذه الميزة في حالة هناك خلل في التطبيق. - Save logs - احفظ السجلات + احفظ السجلات - Open folder with logs - افتح مجلد يحتوي علي سجلات + افتح مجلد يحتوي علي سجلات - + + Save احفظ - + + Logs files (*.log) ملفات الولوج (*.log) - + + Logs file saved تم حفظ ملف السجل - Save logs to file - احفظ السجلات في ملف + احفظ السجلات في ملف - + + Enable logs + + + + Clear logs? مسح السجلات؟ - + Continue واصل - + Cancel إلغاء - + Logs have been cleaned up تم مسح السجلات - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs احذف السجلات @@ -1914,12 +2084,11 @@ Already installed containers were found on the server. All installed containers الإعدادات - Clear %1 profile - مسح ملف تعريف %1 + مسح ملف تعريف %1 - + Clear %1 profile? مسح ملف تعريف %1؟ @@ -1929,39 +2098,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection غير قادر على مسح ملف تعريف %1 أثناء وجود اتصال نشط - + Remove احذف - + Remove %1 from server? احذف %1 من الخادم؟ - + All users with whom you shared a connection will no longer be able to connect to it. جميع المستخدمين الذين شاركت معاهم اتصال لن يستطيعو الاتصال بعد الان. - + Cannot remove active container لا يمكن إزالة الحاوية النشطة - - + + Continue واصل - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Cancel إلغاء @@ -2129,82 +2323,92 @@ Already installed containers were found on the server. All installed containers الاتصال - + + Settings + إعدادات + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code أدخل المفتاح، أضف ملف تكوين أو امسح رمز الاستجابة السريعة - + Insert key أدخل مفتاح - + Insert أدخل - + Continue واصل - + Other connection options اختيارات اتصال اخري - + VPN by Amnezia VPN بواسطة Amnezia - + Connect to classic paid and free VPN services from Amnezia اتصل بخدمات VPN الكلاسيكية المدفوعة والمجانية من Amnezia - + Self-hosted VPN VPN ذاتية الاستضافة - + Configure Amnezia VPN on your own server قم بتكوين Amnezia VPN على الخادم الخاص بك - + Restore from backup استرجاع من ملف يحتوي علي نسخة احتياطية - + Open backup file افتح ملف نسخ احتياطي - + Backup files (*.backup) ملفات نٌسخ احتياطية (*.backup) - + File with connection settings ملف إعدادات اتصال - + Open config file افتح ملف تكوين - + QR code رمز QR - + I have nothing ليس لدي اي شئ @@ -2717,12 +2921,17 @@ Already installed containers were found on the server. All installed containers مشاركة - + + Access error! + خطأ في الوصول! + + + Connection to اتصال إلي - + File with connection settings to معلف مع إعدادات الاتصال إلي @@ -2739,6 +2948,11 @@ Already installed containers were found on the server. All installed containers Settings restored from backup file تم تحميل الإعدادات من ملف نسخة احتياطية + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -2777,12 +2991,12 @@ Already installed containers were found on the server. All installed containers لم يتم العثور علي كلمة المرور - + Could not open keystore فشل فتح مخزن المفاتيح - + Could not remove private key from keystore فشل حذف المفتاح الخاص من مخزن المفاتيح @@ -2958,27 +3172,27 @@ Already installed containers were found on the server. All installed containers فشل في فتح مخزن المفاتيح - + Could not create private key generator فشل ف إنشاء مولد المفاتيح الخاصة - + Could not generate new private key فشل في إنشاء مفتاح خاص جديد - + Could not retrieve private key from keystore فشل في استرداد مفتاح خاص من مخزن المفاتيح - + Could not create encryption cipher فشل في إنشاء شفرة التشفير - + Could not encrypt data فشل في تشفير الداتا @@ -3675,12 +3889,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted ملف النسخه الاحتياطيه تالف - + All settings have been reset to default values تم استرجاع جميع الإعدادات للإعدادات الافتراضية @@ -3812,7 +4026,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts index 26c4f810..6cd78e77 100644 --- a/client/translations/amneziavpn_fa_IR.ts +++ b/client/translations/amneziavpn_fa_IR.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیوها، از VPN کلاسیک استفاده کنید. این VPN برای هر سایتی کار می‌کند و سرعت آن تا %1 مگابیت بر ثانیه است. - + VPN to access blocked sites in regions with high levels of Internet censorship. وی پی ان برای دسترسی به سایت‌های مسدود شده در مناطق با سانسور شدید اینترنت. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. امنزیا پریمیوم - یک وی پی ان کلاسیک برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیو با کیفیت بالا. قابل استفاده برای تمامی سایت‌ها، حتی در کشورهایی با بالاترین سطح سانسور اینترنت. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship امنزیا رایگان یک وی پی ان رایگان برای دور زدن مسدودیت‌ها در کشورهایی با سطح بالای سانسور اینترنت است. - + %1 MBit/s %1 MBit/s - + %1 days %1 روز - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> وی پی ان فقط سایت‌های محبوبی را که در منطقه شما مسدود شده‌اند، مانند اینستاگرام، فیسبوک، توییتر و غیره باز می‌کند. سایر سایت‌ها با آدرس آی‌پی واقعی شما باز خواهند شد. <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free رایگان - + %1 $/month %1 $/ماه @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation در هنگام آماده‌سازی پیکربندی، نمی‌توان از اتصال خارج شد. @@ -188,9 +193,8 @@ ExportController - Access error! - خطای دسترسی! + خطای دسترسی! @@ -259,18 +263,18 @@ Can't be disabled for current server نمی‌توان فایل را باز کرد. - - + + Invalid configuration file فایل پیکربندی نامعتبر است. - + Scanned %1 of %2. ارزیابی %1 از %2. - + In the imported configuration, potentially dangerous lines were found: در پیکربندی وارد شده، خطوطی که ممکن است خطرناک باشند، یافت شدند: @@ -447,6 +451,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint + + + Dev gateway environment + + PageHome @@ -481,10 +490,63 @@ Already installed containers were found on the server. All installed containers امکان تغییر سرور در هنگام متصل بودن وجود ندارد + + PageProtocolAwgClientSettings + + + AmneziaWG settings + تنظیمات AmneziaWG + + + + MTU + + + + + Server settings + + + + + Port + پورت + + + + Save + ذخیره + + + + Save settings? + تنظیمات را ذخیره کن? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + PageProtocolAwgSettings - + AmneziaWG settings تنظیمات AmneziaWG @@ -493,11 +555,6 @@ Already installed containers were found on the server. All installed containers Port پورت - - - MTU - - Remove AmneziaWG حذف AmneziaWG @@ -507,87 +564,87 @@ Already installed containers were found on the server. All installed containers آیا میخواهید AmneziaWG از سرور حذف شود؟ - + All users with whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - + Save ذخیره - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + Save settings? تنظیمات را ذخیره کن? - + Continue ادامه - + Cancel کنسل - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -898,25 +955,88 @@ Already installed containers were found on the server. All installed containers نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + PageProtocolWireGuardClientSettings + + + WG settings + تنظیمات WG + + + + MTU + + + + + Server settings + + + + + Port + پورت + + + + Save + ذخیره + + + + Save settings? + تنظیمات را ذخیره کن? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + PageProtocolWireGuardSettings - + WG settings تنظیمات WG - + Port پورت - - MTU - + + Save settings? + تنظیمات را ذخیره کن? - + + All users with whom you shared a connection with will no longer be able to connect to it. + همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. + + + + Continue + + + + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -925,11 +1045,12 @@ Already installed containers were found on the server. All installed containers تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. + Cancel - کنسل + کنسل - + Save ذخیره @@ -1289,8 +1410,12 @@ Already installed containers were found on the server. All installed containers + support@amnezia.org + + + Mail - ایمیل + ایمیل @@ -1298,17 +1423,22 @@ Already installed containers were found on the server. All installed containers برای ارائه نظرات و گزارشات باگ - + + Copied + کپی شد + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website وب سایت @@ -1317,17 +1447,17 @@ Already installed containers were found on the server. All installed containers https://amnezia.org - + Software version: %1 %1 :نسخه نرم‎افزار - + Check for updates بررسی بروز‎رسانی - + Privacy Policy @@ -1776,72 +1906,108 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - ثبت وقایع فعال است. توجه داشته باشید که ثبت وقایع به‌طور خودکار پس از ۱۴ روز غیرفعال شده و تمام فایل‌های ثبت وقایع حذف خواهند شد. + ثبت وقایع فعال است. توجه داشته باشید که ثبت وقایع به‌طور خودکار پس از ۱۴ روز غیرفعال شده و تمام فایل‌های ثبت وقایع حذف خواهند شد. - + Logging گزارشات - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. فعال کردن این عملکرد باعث ذخیره خودکار لاگ‌های برنامه می‌شود. به طور پیش‌فرض، قابلیت ثبت لاگ غیرفعال است. در صورت بروز خطا در برنامه، ذخیره لاگ را فعال کنید. - Save logs - ذخیره گزارشات + ذخیره گزارشات - Open folder with logs - باز کردن پوشه گزارشات + باز کردن پوشه گزارشات - + + Save ذخیره - + + Logs files (*.log) Logs files (*.log) - + + Logs file saved فایل گزارشات ذخیره شد - Save logs to file - ذخیره گزارشات در فایل + ذخیره گزارشات در فایل - + + Enable logs + + + + Clear logs? پاک کردن گزارشات؟ - + Continue ادامه - + Cancel کنسل - + Logs have been cleaned up گزارشات پاک شدند - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs پاک کردن گزارشات @@ -2013,12 +2179,11 @@ Already installed containers were found on the server. All installed containers تنظیمات - Clear %1 profile - پاک کردن پروفایل %1 + پاک کردن پروفایل %1 - + Clear %1 profile? آیا می‌خواهید پروفایل %1 را پاک کنید؟ @@ -2028,39 +2193,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection نمی‌توان پروفایل %1 را در حین اتصال فعال پاک کرد. - + Remove حذف - + Remove %1 from server? حذف %1 از سرور؟ - + All users with whom you shared a connection will no longer be able to connect to it. تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. - + Cannot remove active container نمی‌توان کانتینر فعال را حذف کرد. - - + + Continue ادامه - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Cancel کنسل @@ -2243,7 +2433,7 @@ It's okay as long as it's from someone you trust. چی داری؟ - + File with connection settings فایل شامل تنظیمات اتصال @@ -2257,77 +2447,87 @@ It's okay as long as it's from someone you trust. ارتباط - + + Settings + تنظیمات + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code کلید را وارد کنید، فایل پیکربندی را اضافه کنید یا کد QR را اسکن کنید - + Insert key کلید را وارد کنید - + Insert وارد کردن - + Continue ادامه دهید - + Other connection options گزینه‌های اتصال دیگر - + VPN by Amnezia VPN توسط Amnezia - + Connect to classic paid and free VPN services from Amnezia اتصال به سرویس‌های VPN کلاسیک پولی و رایگان از Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server پیکربندی VPN Amnezia بر روی سرور خودتان - + Restore from backup بازیابی از پشتیبان - + Open backup file باز کردن فایل پشتیبان - + Backup files (*.backup) Backup files (*.backup) - + Open config file باز کردن فایل تنظیمات - + QR code QR-Code - + I have nothing من هیچی ندارم @@ -2499,7 +2699,7 @@ It's okay as long as it's from someone you trust. نصب - + The port must be in the range of 1 to 65535 پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد @@ -2867,12 +3067,17 @@ It's okay as long as it's from someone you trust. اشتراک‎گذاری - + + Access error! + خطای دسترسی! + + + Connection to ارتباط با - + File with connection settings to فایل شامل تنظیمات ارتباط با @@ -2889,6 +3094,11 @@ It's okay as long as it's from someone you trust. Settings restored from backup file تنظیمات از فایل پشتیبان بازیابی شد + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -2927,12 +3137,12 @@ It's okay as long as it's from someone you trust. Password not found - + Could not open keystore Could not open keystore - + Could not remove private key from keystore Could not remove private key from keystore @@ -3108,27 +3318,27 @@ It's okay as long as it's from someone you trust. Could not open keystore - + Could not create private key generator Could not create private key generator - + Could not generate new private key Could not generate new private key - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create encryption cipher Could not create encryption cipher - + Could not encrypt data Could not encrypt data @@ -3883,7 +4093,7 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values تمام تنظیمات به مقادیر پیش فرض ریست شد @@ -3892,7 +4102,7 @@ For more detailed information, you can پروفایل ذخیره شده پاک شد - + Backup file is corrupted فایل بک‎آپ خراب شده است @@ -4024,7 +4234,7 @@ For more detailed information, you can VpnConnection - + Mbps Mbps diff --git a/client/translations/amneziavpn_hi_IN.ts b/client/translations/amneziavpn_hi_IN.ts index e5cd57d8..ab459b7c 100644 --- a/client/translations/amneziavpn_hi_IN.ts +++ b/client/translations/amneziavpn_hi_IN.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + %1 MBit/s - + %1 days - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free - + %1 $/month @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation कॉन्फ़िगरेशन तैयारी के दौरान डिस्कनेक्ट करने में असमर्थ @@ -187,9 +192,8 @@ ExportController - Access error! - प्रवेश त्रुटि! + प्रवेश त्रुटि! @@ -255,18 +259,18 @@ Can't be disabled for current server फाइल खोलने में असमर्थ - - + + Invalid configuration file अमान्य कॉन्फ़िगरेशन फ़ाइल - + Scanned %1 of %2. %2 में से %1 स्कैन किया गया. - + In the imported configuration, potentially dangerous lines were found: @@ -443,6 +447,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint + + + Dev gateway environment + + PageHome @@ -477,10 +486,63 @@ Already installed containers were found on the server. All installed containers सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ + + PageProtocolAwgClientSettings + + + AmneziaWG settings + Amneziaडब्ल्यूजी सेटिंग्स + + + + MTU + एमटीयू + + + + Server settings + + + + + Port + + + + + Save + सहेजें + + + + Save settings? + सेटिंग्स सेव करें? + + + + Only the settings for this device will be changed + + + + + Continue + जारी रखना + + + + Cancel + रद्द करना + + + + Unable change settings while there is an active connection + सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ + + PageProtocolAwgSettings - + AmneziaWG settings Amneziaडब्ल्यूजी सेटिंग्स @@ -490,92 +552,91 @@ Already installed containers were found on the server. All installed containers पोर्ट - MTU - एमटीयू + एमटीयू - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + Save सहेजें - + The values of the H1-H4 fields must be unique H1-H4 फ़ील्ड का मान अद्वितीय होना चाहिए - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) फ़ील्ड S1 + संदेश आरंभ आकार (148) का मान S2 + संदेश प्रतिक्रिया आकार (92) के बराबर नहीं होना चाहिए - + Save settings? सेटिंग्स सेव करें? - + All users with whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ - + Continue जारी रखना - + Cancel रद्द करना @@ -862,30 +923,102 @@ Already installed containers were found on the server. All installed containers सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ + + PageProtocolWireGuardClientSettings + + + WG settings + डब्ल्यूजी सेटिंग्स + + + + MTU + एमटीयू + + + + Server settings + + + + + Port + + + + + Save + सहेजें + + + + Save settings? + सेटिंग्स सेव करें? + + + + Only the settings for this device will be changed + + + + + Continue + जारी रखना + + + + Cancel + रद्द करना + + + + Unable change settings while there is an active connection + सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ + + PageProtocolWireGuardSettings - + WG settings डब्ल्यूजी सेटिंग्स - + Port बंदरगाह - MTU - एमटीयू + एमटीयू - + Save सहेजें - + + Save settings? + सेटिंग्स सेव करें? + + + + All users with whom you shared a connection with will no longer be able to connect to it. + वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. + + + + Continue + जारी रखना + + + + Cancel + रद्द करना + + + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -1237,9 +1370,13 @@ Already installed containers were found on the server. All installed containers - Mail - मेल + मेल + + + + support@amnezia.org + @@ -1247,32 +1384,37 @@ Already installed containers were found on the server. All installed containers समीक्षाओं और बग रिपोर्टों के लिए - + + Copied + कॉपी किया गया + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website वेबसाइट - + Software version: %1 सॉफ़्टवेयर संस्करण: %1 - + Check for updates अद्यतन के लिए जाँच - + Privacy Policy गोपनीयता नीति @@ -1729,72 +1871,108 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - लॉगिंग सक्षम है. ध्यान दें कि 14 दिनों के बाद लॉग स्वचालित रूप से अक्षम हो जाएंगे, और सभी लॉग फ़ाइलें हटा दी जाएंगी. + लॉगिंग सक्षम है. ध्यान दें कि 14 दिनों के बाद लॉग स्वचालित रूप से अक्षम हो जाएंगे, और सभी लॉग फ़ाइलें हटा दी जाएंगी. - + Logging लॉगिंग - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. इस फ़ंक्शन को सक्षम करने से एप्लिकेशन के लॉग स्वचालित रूप से सहेजे जाएंगे, डिफ़ॉल्ट रूप से, लॉगिंग कार्यक्षमता अक्षम है। एप्लिकेशन की खराबी की स्थिति में लॉग सेविंग सक्षम करें. - Save logs - लॉग सहेजें + लॉग सहेजें - Open folder with logs - लॉग के साथ फ़ोल्डर खोलें + लॉग के साथ फ़ोल्डर खोलें - + + Save सहेजें - + + Logs files (*.log) लॉग फ़ाइलें (*.log) - + + Logs file saved लॉग फ़ाइल सहेजी गई - Save logs to file - फ़ाइल में लॉग सहेजें + फ़ाइल में लॉग सहेजें - + + Enable logs + + + + Clear logs? लॉग साफ़ करें? - + Continue जारी रखना - + Cancel रद्द करना - + Logs have been cleaned up लॉग साफ़ कर दिए गए हैं - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs लॉग साफ़ करें @@ -1954,12 +2132,11 @@ Already installed containers were found on the server. All installed containers समायोजन - Clear %1 profile - %1 प्रोफ़ाइल साफ़ करें + %1 प्रोफ़ाइल साफ़ करें - + Clear %1 profile? %1 प्रोफ़ाइल साफ़ करें? @@ -1969,39 +2146,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection सक्रिय कनेक्शन होने पर %1 प्रोफ़ाइल साफ़ करने में असमर्थ - + Remove निकालना - + All users with whom you shared a connection will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Cannot remove active container सक्रिय कंटेनर को हटाया नहीं जा सकता - + Remove %1 from server? सर्वर से %1 हटाएँ? - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Continue जारी रखना - - + + Cancel रद्द करना @@ -2185,82 +2387,92 @@ Already installed containers were found on the server. All installed containers कनेक्शन - + + Settings + समायोजन + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code - + Insert key - + Insert डालना - + Continue जारी रखना - + Other connection options - + VPN by Amnezia - + Connect to classic paid and free VPN services from Amnezia - + Self-hosted VPN - + Configure Amnezia VPN on your own server - + Restore from backup बैकअप से बहाल करना - + Open backup file बैकअप फ़ाइल खोलें - + Backup files (*.backup) बैकअप फ़ाइलें (*.backup) - + File with connection settings कनेक्शन सेटिंग्स वाली फ़ाइल - + Open config file कॉन्फ़िग फ़ाइल खोलें - + QR code क्यू आर संहिता - + I have nothing मेरे पास कुछ नहीं है @@ -2432,7 +2644,7 @@ Already installed containers were found on the server. All installed containers स्थापित करना - + The port must be in the range of 1 to 65535 @@ -2804,12 +3016,17 @@ Already installed containers were found on the server. All installed containers शेयर करना - + + Access error! + प्रवेश त्रुटि! + + + Connection to के लिए कनेक्शन - + File with connection settings to कनेक्शन सेटिंग्स वाली फ़ाइल @@ -2826,6 +3043,11 @@ Already installed containers were found on the server. All installed containers Settings restored from backup file बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -2864,12 +3086,12 @@ Already installed containers were found on the server. All installed containers पासवर्ड नहीं मिला - + Could not open keystore कीस्टोर नहीं खुल सका - + Could not remove private key from keystore कीस्टोर से निजी कुंजी नहीं हटाई जा सकी @@ -3045,27 +3267,27 @@ Already installed containers were found on the server. All installed containers कीस्टोर नहीं खुल सका - + Could not create private key generator निजी कुंजी जेनरेटर नहीं बनाया जा सका - + Could not generate new private key नई निजी कुंजी उत्पन्न नहीं हो सकी - + Could not retrieve private key from keystore कीस्टोर से निजी कुंजी पुनर्प्राप्त नहीं की जा सकी - + Could not create encryption cipher एन्क्रिप्शन सिफर नहीं बनाया जा सका - + Could not encrypt data डेटा एन्क्रिप्ट नहीं किया जा सका @@ -3763,12 +3985,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted बैकअप फ़ाइल दूषित है - + All settings have been reset to default values सभी सेटिंग्स को डिफ़ॉल्ट मानों पर रीसेट कर दिया गया है @@ -3900,7 +4122,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps diff --git a/client/translations/amneziavpn_my_MM.ts b/client/translations/amneziavpn_my_MM.ts index 473c97f8..3e964cc9 100644 --- a/client/translations/amneziavpn_my_MM.ts +++ b/client/translations/amneziavpn_my_MM.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ မည်သည့်ဆိုက်များအတွက်မဆိုအလုပ်လုပ်ပြီး လိုင်းအရှိန် %1 MBit/s အထိအသုံးပြုနိုင်ပါတယ်။ - + VPN to access blocked sites in regions with high levels of Internet censorship. အင်တာနက် ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသော ဒေသများရှိ ပိတ်ဆို့ထားသော ဆိုက်များကို ဝင်ရောက်ရန် VPN။. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amnezia Premium - သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကိုကြည်လင်ပြတ်သားစွာကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ အင်တာနက်ဆင်ဆာဖြတ်မှု အဆင့်အမြင့်ဆုံးနိုင်ငံများတွင်ပင် မည်သည့်ဆိုက်များအတွက်မဆို အလုပ်လုပ်ပါသည်။. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free သည် အင်တာနက်ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသောနိုင်ငံများတွင် ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်ရန်အတွက် အခမဲ့ VPN တစ်ခုဖြစ်ပါသည်။ - + %1 MBit/s %1 MBit/s - + %1 days %1 ရက် - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> ဤ VPN သည် သင့်ဒေသရှိ Instagram၊ Facebook၊ Twitter နှင့် အခြားသော လူကြိုက်များသော ဆိုက်များကိုသာ ဖွင့်ပေးပါမည်။ အခြားဝဘ်ဆိုက်များကိုမူ သင်၏ IP လိပ်စာအစစ်အမှန်ဖြင့်သာ ဖွင့်ပေးပါမည်၊ <a href="%1/free" style="color: #FBB26A;">နောက်ထပ်အသေးစိတ်အချက်အလက်များကို ဝဘ်ဆိုဒ်ပေါ်တွင်ကြည့်ရန်</a> - + Free အခမဲ့ - + %1 $/month %1 $/တစ်လ @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation Configuration ပြင်ဆင်ခြင်းလုပ်ဆောင်နေချိန်အတွင်း ချိတ်ဆက်မှုဖြတ်တောက်၍မရပါ @@ -187,9 +192,8 @@ ExportController - Access error! - အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! + အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! @@ -255,18 +259,18 @@ Can't be disabled for current server ဖိုင်ကိုဖွင့်၍မရပါ - - + + Invalid configuration file Configuration ဖိုင် မမှန်ကန်ပါ - + Scanned %1 of %2. %2 ၏ %1 ကို စကင်န်ဖတ်ထားသည်. - + In the imported configuration, potentially dangerous lines were found: တင်သွင်းသည့် configuration တွင်၊ အန္တရာယ်ရှိနိုင်သည့်စာလိုင်းများကို တွေ့ရှိခဲ့သည်: @@ -443,6 +447,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint Gateway အဆုံးမှတ် + + + Dev gateway environment + + PageHome @@ -477,10 +486,63 @@ Already installed containers were found on the server. All installed containers လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ + + PageProtocolAwgClientSettings + + + AmneziaWG settings + AmneziaWG ဆက်တင်များ + + + + MTU + MTU + + + + Server settings + + + + + Port + Port + + + + Save + သိမ်းဆည်းမည် + + + + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? + + + + Only the settings for this device will be changed + + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + PageProtocolAwgSettings - + AmneziaWG settings AmneziaWG ဆက်တင်များ @@ -490,92 +552,91 @@ Already installed containers were found on the server. All installed containers Port - MTU - MTU + MTU - + All users with whom you shared a connection with will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Save သိမ်းဆည်းမည် - + Jc - Junk packet count Jc - Junk packet အရေအတွက် - + Jmin - Junk packet minimum size Jmin - Junk packet အသေးငယ်ဆုံးလက်ခံနိုင်မှုအရွယ်အစား - + Jmax - Junk packet maximum size Jmax - Junk packet အကြီးဆုံးလက်ခံနိုင်မှုအရွယ်အစား - + S1 - Init packet junk size S1 - Init packet junk အရွယ်အစား - + S2 - Response packet junk size S2 - Response packet junk အရွယ်အစား - + H1 - Init packet magic header H1 - Init packet magic header - + H2 - Response packet magic header H2 - Response packet magic header - + H4 - Transport packet magic header H4 - Transport packet magic header - + H3 - Underload packet magic header H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique H1-H4 အကွက်များ၏ တန်ဖိုးများသည် အခြားတန်ဖိုးများနှင့်မတူ တမူထူးခြားနေရပါမည် - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) အကွက် S1 + မက်ဆေ့ချ် စတင်ခြင်း အရွယ်အစား (148) ၏ တန်ဖိုးသည် S2 + မက်ဆေ့ချ် တုံ့ပြန်မှု အရွယ်အစား (92) နှင့် မညီမျှရပါ - + Save settings? ဆက်တင်များကို သိမ်းဆည်းမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -862,30 +923,102 @@ Already installed containers were found on the server. All installed containers လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + PageProtocolWireGuardClientSettings + + + WG settings + WG ဆက်တင်များ + + + + MTU + MTU + + + + Server settings + + + + + Port + Port + + + + Save + သိမ်းဆည်းမည် + + + + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? + + + + Only the settings for this device will be changed + + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + PageProtocolWireGuardSettings - + WG settings WG ဆက်တင်များ - + Port Port - - MTU - MTU + + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? - + + All users with whom you shared a connection with will no longer be able to connect to it. + သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + MTU + MTU + + + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + Save သိမ်းဆည်းမည် @@ -1205,9 +1338,13 @@ Already installed containers were found on the server. All installed containers https://t.me/amnezia_vpn - Mail - မေးလ် + မေးလ် + + + + support@amnezia.org + @@ -1215,32 +1352,37 @@ Already installed containers were found on the server. All installed containers သုံးသပ်ချက်များနှင့် ချွတ်ယွင်းချက်အစီရင်ခံစာများအတွက် - + + Copied + ကူးယူပြီးပါပြီ + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website ဝဘ်ဆိုက် - + Software version: %1 ဆော့ဖ်ဝဲဗားရှင်း: %1 - + Check for updates အပ်ဒိတ်များရှိမရှိ စစ်ဆေးမည် - + Privacy Policy ကိုယ်ရေးအချက်အလက်မူဝါဒ @@ -1689,73 +1831,109 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - Logging ကို ဖွင့်ထားသည်။ မှတ်တမ်းများကို ၁၄ ရက်အကြာတွင် အလိုအလျောက်ပိတ်ထားမည်ဖြစ်ပြီး မှတ်တမ်းဖိုင်များအားလုံး ပျက်သွားမည်ဖြစ်ကြောင်း သတိပြုပါ။. + Logging ကို ဖွင့်ထားသည်။ မှတ်တမ်းများကို ၁၄ ရက်အကြာတွင် အလိုအလျောက်ပိတ်ထားမည်ဖြစ်ပြီး မှတ်တမ်းဖိုင်များအားလုံး ပျက်သွားမည်ဖြစ်ကြောင်း သတိပြုပါ။. - + Logging Logging - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. ဤလုပ်ဆောင်ချက်ကို ဖွင့်ခြင်းဖြင့် အပလီကေးရှင်း၏ မှတ်တမ်းများကို အလိုအလျောက် သိမ်းဆည်းပေးမည် ဖြစ်သည်။ ပုံမှန်အတိုင်းဆိုလျှင် Logging လုပ်ဆောင်ချက်ကို ပိတ်ထားမည်ဖြစ်သည်။ အပလီကေးရှင်းချို့ယွင်းချက်ရှိခဲ့ပါသော် မှတ်တမ်းကိုပြန်လည်ကြည့်ရှုနိုင်ရန် မှတ်တမ်းသိမ်းဆည်းမှုကို ဖွင့်ထားလိုက်ပါ။. - Save logs - မှတ်တမ်းများကိုသိမ်းဆည်းမည် + မှတ်တမ်းများကိုသိမ်းဆည်းမည် - Open folder with logs - မှတ်တမ်းများရှိသောဖိုင်တွဲကိုဖွင့်မည် + မှတ်တမ်းများရှိသောဖိုင်တွဲကိုဖွင့်မည် - + + Save သိမ်းဆည်းမည် - + + Logs files (*.log) မှတ်တမ်းဖိုင်များ (*.log) မှတ်တမ်းဖိုင်များ (*.log) - + + Logs file saved မှတ်တမ်းဖိုင်များသိမ်းဆည်းပြီးပါပြီ - Save logs to file - မှတ်တမ်းများကို ဖိုင်တွင်သိမ်းဆည်းမည် + မှတ်တမ်းများကို ဖိုင်တွင်သိမ်းဆည်းမည် - + + Enable logs + + + + Clear logs? မှတ်တမ်းများရှင်းလင်းမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Logs have been cleaned up မှတ်တမ်းများကို ရှင်းလင်းပြီးပါပြီ - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs မှတ်တမ်းများရှင်းလင်းမည် @@ -1915,12 +2093,11 @@ Already installed containers were found on the server. All installed containers ဆက်တင်များ - Clear %1 profile - %1 ပရိုဖိုင်ကို ရှင်းလင်းမည် + %1 ပရိုဖိုင်ကို ရှင်းလင်းမည် - + Clear %1 profile? %1 ပရိုဖိုင်ကို ရှင်းလင်းမည်လား? @@ -1930,39 +2107,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် %1 ပရိုဖိုင်ကို ရှင်းလင်း၍မရပါ - + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + All users with whom you shared a connection will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ဤချိတ်ဆက်မှုကိုချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Cannot remove active container Active container ကိုဖယ်ရှား၍မရပါ - - + + Continue ဆက်လက်လုပ်ဆောင်မည် - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Cancel ပယ်ဖျက်မည် @@ -2125,7 +2327,7 @@ Already installed containers were found on the server. All installed containers PageSetupWizardConfigSource - + File with connection settings ချိတ်ဆက်မှုဆက်တင်များပါဝင်သောဖိုင် @@ -2135,77 +2337,87 @@ Already installed containers were found on the server. All installed containers ချိတ်ဆက်မှု - + + Settings + ဆက်တင်များ + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code Key ကိုထည့်မည်၊ ဖွဲ့စည်းမှုဖိုင်တစ်ခုကိုထည့်မည် သို့မဟုတ် QR-ကုဒ်ကို စကင်န်ဖတ်မည် - + Insert key Key ကိုထည့်သွင်းမည် - + Insert ထည့်သွင်းမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Other connection options အခြားချိတ်ဆက်မှုရွေးချယ်စရာများ - + VPN by Amnezia Amnezia မှ VPN - + Connect to classic paid and free VPN services from Amnezia Amnezia မှ အခပေးနှင့် အခမဲ့ မူလ VPN ဝန်ဆောင်မှုများသို့ ချိတ်ဆက်မည် - + Self-hosted VPN ကိုယ်တိုင် host လုပ်ထားသော VPN - + Configure Amnezia VPN on your own server Amnezia VPN ကို သင်၏ကိုယ်ပိုင်ဆာဗာပေါ်တွင် စီစဥ်ချိန်ညှိမည် - + Restore from backup အရံဖိုင်မှ ပြန်လည်ရယူမည် - + Open backup file အရံဖိုင်ကို ဖွင့်မည် - + Backup files (*.backup) အရံဖိုင်များ (*.backup) - + Open config file config ဖိုင်ကိုဖွင့်မည် - + QR code QR-ကုဒ် - + I have nothing ကျွန်ုပ်တွင်ဘာမှမရှိပါ @@ -2717,12 +2929,17 @@ Already installed containers were found on the server. All installed containers မျှဝေမည် - + + Access error! + အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! + + + Connection to ဤဆာဗာသို့ချိတ်ဆက်မှု - + File with connection settings to ဤဆာဗာနှင့်ချိတ်ဆက်မှု ဆက်တင်များပါရှိသော ဖိုင် @@ -2739,6 +2956,11 @@ Already installed containers were found on the server. All installed containers Settings restored from backup file ဆက်တင်များကို အရံဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -2777,12 +2999,12 @@ Already installed containers were found on the server. All installed containers စကားဝှက်ကို ရှာမတွေ့ပါ - + Could not open keystore keystore ကို ဖွင့်၍မရပါ - + Could not remove private key from keystore Key store မှ ကိုယ်ပိုင် key ကို ဖယ်ရှား၍မရပါ @@ -2958,27 +3180,27 @@ Already installed containers were found on the server. All installed containers keystore ကို ဖွင့်၍မရပါ - + Could not create private key generator ကိုယ်ပိုင် key ဖန်တီးမှုစက်ကိုမဖန်တီးနိုင်ပါ - + Could not generate new private key ကိုယ်ပိုင် key အသစ် မထုတ်ပေးနိုင်ပါ - + Could not retrieve private key from keystore Key store မှ ကိုယ်ပိုင် key ကို ထုတ်ယူ၍မရပါ - + Could not create encryption cipher ကုတ်ဝှက်ဖြည်ခြင်းဖန်တီး၍မရပါ - + Could not encrypt data ဒေတာကို ကုတ်ဝှက်၍မရပါ @@ -3672,12 +3894,12 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values ဆက်တင်အားလုံးကို မူရင်းတန်ဖိုးများအဖြစ် ပြန်လည်သတ်မှတ်ထားသည် - + Backup file is corrupted အရံဖိုင်ပျက်ဆီးနေသည် @@ -3809,7 +4031,7 @@ For more detailed information, you can VpnConnection - + Mbps Mbps diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index f616c3ae..2fb21259 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с - + VPN to access blocked sites in regions with high levels of Internet censorship. VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amnezia Premium — классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры - + %1 MBit/s - + %1 days %1 дней - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> Через VPN будут открываться только популярные сайты, заблокированные в вашем регионе, такие как Instagram, Facebook, Twitter и другие. Остальные сайты будут открываться с вашего реального IP-адреса, <a href="%1/free" style="color: #FBB26A;">подробности на сайте.</a> - + Free Бесплатно - + %1 $/month %1 $/месяц @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation Невозможно отключиться во время подготовки конфигурации @@ -187,9 +192,8 @@ ExportController - Access error! - Ошибка доступа! + Ошибка доступа! @@ -259,18 +263,18 @@ Can't be disabled for current server Невозможно открыть файл - - + + Invalid configuration file Неверный файл конфигурации - + Scanned %1 of %2. Отсканировано %1 из %2. - + In the imported configuration, potentially dangerous lines were found: В импортированной конфигурации были обнаружены потенциально опасные строки: @@ -447,6 +451,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint + + + Dev gateway environment + + PageHome @@ -481,10 +490,63 @@ Already installed containers were found on the server. All installed containers Невозможно изменить сервер во время активного соединения + + PageProtocolAwgClientSettings + + + AmneziaWG settings + Настройки AmneziaWG + + + + MTU + MTU + + + + Server settings + + + + + Port + Порт + + + + Save + Сохранить + + + + Save settings? + Сохранить настройки? + + + + Only the settings for this device will be changed + + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + + PageProtocolAwgSettings - + AmneziaWG settings Настройки AmneziaWG @@ -494,9 +556,8 @@ Already installed containers were found on the server. All installed containers Порт - MTU - MTU + MTU Remove AmneziaWG @@ -507,87 +568,87 @@ Already installed containers were found on the server. All installed containers Удалить AmneziaWG с сервера? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Save Сохранить - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique Значения в полях H1-H4 должны быть уникальными - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) Значение в поле S1 + размер инициации сообщения (148) не должно равняться значению в поле S2 + размер ответа на сообщение (92) - + Save settings? Сохранить настройки? - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -898,25 +959,87 @@ Already installed containers were found on the server. All installed containers Невозможно изменить настройки во время активного соединения + + PageProtocolWireGuardClientSettings + + + WG settings + Настройки WG + + + + MTU + MTU + + + + Server settings + + + + + Port + Порт + + + + Save + Сохранить + + + + Save settings? + Сохранить настройки? + + + + Only the settings for this device will be changed + + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + + PageProtocolWireGuardSettings - + WG settings Настройки WG - + Port Порт - - MTU - MTU + + Save settings? + Сохранить настройки? - + + All users with whom you shared a connection with will no longer be able to connect to it. + Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. + + + MTU + MTU + + + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -933,15 +1056,17 @@ Already installed containers were found on the server. All installed containers Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. + Continue - Продолжить + Продолжить + Cancel - Отменить + Отменить - + Save Сохранить @@ -1305,8 +1430,12 @@ Already installed containers were found on the server. All installed containers + support@amnezia.org + + + Mail - Почта + Почта @@ -1314,17 +1443,22 @@ Already installed containers were found on the server. All installed containers Для отзывов и сообщений об ошибках - + + Copied + Скопировано + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт @@ -1333,17 +1467,17 @@ Already installed containers were found on the server. All installed containers https://amnezia.org - + Software version: %1 Версия ПО: %1 - + Check for updates Проверить обновления - + Privacy Policy Политика конфиденциальности @@ -1816,72 +1950,108 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены. + Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены. - + Logging Логирование - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения. - Save logs - Сохранять логи + Сохранять логи - Open folder with logs - Открыть папку с логами + Открыть папку с логами - + + Save Сохранить - + + Logs files (*.log) Файлы логов (*.log) - + + Logs file saved Файл с логами сохранен - Save logs to file - Сохранить логи в файл + Сохранить логи в файл - + + Enable logs + + + + Clear logs? Очистить логи? - + Continue Продолжить - + Cancel Отменить - + Logs have been cleaned up Логи очищены - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs Очистить логи @@ -2069,12 +2239,11 @@ Already installed containers were found on the server. All installed containers настройки - Clear %1 profile - Очистить профиль %1 + Очистить профиль %1 - + Clear %1 profile? Очистить профиль %1? @@ -2084,27 +2253,52 @@ Already installed containers were found on the server. All installed containers - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + Unable to clear %1 profile while there is an active connection Невозможно очистить профиль %1 во время активного соединения - + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + All users with whom you shared a connection will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Cannot remove active container Невозможно удалить активный контейнер @@ -2113,14 +2307,14 @@ Already installed containers were found on the server. All installed containers Все пользователи, с которыми вы поделились VPN, больше не смогут к нему подключаться. - - + + Continue Продолжить - - + + Cancel Отменить @@ -2311,7 +2505,7 @@ It's okay as long as it's from someone you trust. Что у вас есть? - + File with connection settings Файл с настройками подключения @@ -2325,77 +2519,87 @@ It's okay as long as it's from someone you trust. Соединение - + + Settings + Настройки + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code Вставьте ключ, добавьте файл конфигурации или отсканируйте QR-код - + Insert key Вставьте ключ - + Insert Вставить - + Continue Продолжить - + Other connection options Другие варианты подключения - + VPN by Amnezia VPN от Amnezia - + Connect to classic paid and free VPN services from Amnezia Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server Настроить VPN на собственном сервере - + Restore from backup Восстановить из резервной копии - + Open backup file Открыть резервную копию - + Backup files (*.backup) Файлы резервных копий (*.backup) - + Open config file Открыть файл с конфигурацией - + QR code QR-код - + I have nothing У меня ничего нет @@ -2600,7 +2804,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Установить - + The port must be in the range of 1 to 65535 Порт должен быть в диапазоне от 1 до 65535 @@ -2996,12 +3200,17 @@ and will not be shared or disclosed to the Amnezia or any third parties Поделиться - + + Access error! + Ошибка доступа! + + + Connection to Подключение к - + File with connection settings to Файл с настройками подключения к @@ -3018,6 +3227,11 @@ and will not be shared or disclosed to the Amnezia or any third parties Settings restored from backup file Настройки восстановлены из бэкап файла + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -3056,12 +3270,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Пароль не найден - + Could not open keystore Не удалось открыть хранилище ключей - + Could not remove private key from keystore Не удалось удалить закрытый ключ из хранилища ключей @@ -3237,27 +3451,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Не удалось открыть хранилище ключей - + Could not create private key generator Не удалось создать генератор закрытых ключей - + Could not generate new private key Не удалось сгенерировать новый закрытый ключ - + Could not retrieve private key from keystore Не удалось получить закрытый ключ из хранилища ключей - + Could not create encryption cipher Не удалось создать шифр шифрования - + Could not encrypt data Не удалось зашифровать данные @@ -4094,7 +4308,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + All settings have been reset to default values Все настройки сброшены до значений по умолчанию @@ -4103,7 +4317,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Закэшированные профили очищены - + Backup file is corrupted Файл резервной копии поврежден @@ -4235,7 +4449,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Мбит/с diff --git a/client/translations/amneziavpn_uk_UA.ts b/client/translations/amneziavpn_uk_UA.ts index 11377c4d..c7195119 100644 --- a/client/translations/amneziavpn_uk_UA.ts +++ b/client/translations/amneziavpn_uk_UA.ts @@ -27,47 +27,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s Звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео. Працює для будь-яких сайтів. Швидкість до %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. VPN для доступу до заблокованих сайтів у регіонах з високим рівнем інтернет-цензури. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amnezia Premium - звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео у високій роздільній здатності. Працює для всіх вебсайтів, навіть у країнах з найвищим рівнем інтернет-цензури. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free — це безкоштовний VPN для обходу блокувань у країнах з високим рівнем інтернет-цензури - + %1 MBit/s %1 MBit/s - + %1 days %1 днів - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> Лише популярні сайти, які заблоковані у вашому регіоні, будуть відкриватись за допомогою VPN підключення (Instagram, Facebook, Twitter та ін.). Звичайні сайти будуть відкриватися без використання VPN, <a href="%1/free" style="color: #FBB26A;">більш детально на нашому сайті.</a> - + Free Безкоштовно - + %1 $/month %1 $/місяць @@ -98,7 +103,7 @@ ConnectButton - + Unable to disconnect during configuration preparation Неможливо відключитися під час підготовки конфігурації @@ -210,9 +215,8 @@ ExportController - Access error! - Помилка доступу! + Помилка доступу! @@ -286,18 +290,18 @@ Can't be disabled for current server Неможливо відкрити файл - - + + Invalid configuration file Недійсний файл конфігурації - + Scanned %1 of %2. Відскановано %1 з %2. - + In the imported configuration, potentially dangerous lines were found: У імпортованій конфігурації знайдено потенційно небезпечні рядки: @@ -400,7 +404,7 @@ Already installed containers were found on the server. All installed containers application name назва застосунку - + Add selected Додати вибране @@ -473,6 +477,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint + + + Dev gateway environment + + PageHome @@ -507,10 +516,63 @@ Already installed containers were found on the server. All installed containers Не можна змінити сервер при активному підключенні + + PageProtocolAwgClientSettings + + + AmneziaWG settings + налаштування AmneziaWG + + + + MTU + MTU + + + + Server settings + + + + + Port + Порт + + + + Save + Зберегти + + + + Save settings? + Зберегти налаштування? + + + + Only the settings for this device will be changed + + + + + Continue + Продовжити + + + + Cancel + Відмінити + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення + + PageProtocolAwgSettings - + AmneziaWG settings налаштування AmneziaWG @@ -521,81 +583,76 @@ Already installed containers were found on the server. All installed containers - MTU - - - - Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + Save Зберегти - + The values of the H1-H4 fields must be unique Значення полів H1-H4 мають бути унікальними - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) Значення поля S1 + розмір повідомлення ініціалізації (148) не має бути рівним значенню S2 + розмір повідомлення відповіді (92) - + Save settings? Зберегти налаштування? - + All users with whom you shared a connection with will no longer be able to connect to it. Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -616,12 +673,12 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - + Continue Продовжити - + Cancel Відмінити @@ -960,30 +1017,102 @@ Already installed containers were found on the server. All installed containers Зберегти і перезавантажити + + PageProtocolWireGuardClientSettings + + + WG settings + + + + + MTU + MTU + + + + Server settings + + + + + Port + Порт + + + + Save + Зберегти + + + + Save settings? + Зберегти налаштування? + + + + Only the settings for this device will be changed + + + + + Continue + Продовжити + + + + Cancel + Відмінити + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення + + PageProtocolWireGuardSettings - + WG settings - + Port Порт - MTU - MTU + MTU - + Save Зберегти - + + Save settings? + Зберегти налаштування? + + + + All users with whom you shared a connection with will no longer be able to connect to it. + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. + + + + Continue + Продовжити + + + + Cancel + Відмінити + + + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -1000,7 +1129,7 @@ Already installed containers were found on the server. All installed containers Disguised as traffic from Замаскувати трафік під - + Save Зберегти @@ -1375,8 +1504,12 @@ Already installed containers were found on the server. All installed containers + support@amnezia.org + + + Mail - Пошта + Пошта @@ -1384,17 +1517,22 @@ Already installed containers were found on the server. All installed containers Для відгуків і повідомлень про помилки - + + Copied + Скопійовано + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт @@ -1403,17 +1541,17 @@ Already installed containers were found on the server. All installed containers https://amnezia.org - + Software version: %1 Версія ПЗ: %1 - + Check for updates Перевірити оновлення - + Privacy Policy Політика конфіденційності @@ -1604,7 +1742,7 @@ Already installed containers were found on the server. All installed containers Запускати застосунок в згорнутому вигляді - + Language Мова @@ -1886,72 +2024,108 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - Логування увімкнене. Зверніть увагу, що логування буде автоматично вимкнене через 14 днів, а всі файли журналів будуть видалені. + Логування увімкнене. Зверніть увагу, що логування буде автоматично вимкнене через 14 днів, а всі файли журналів будуть видалені. - + Logging Логування - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. Увімкнення цієї функції автоматично зберігатиме журнали додатка. За замовчуванням функція логування вимкнена. Увімкніть збереження журналів у випадку збою додатка. - Save logs - Зберегти логи + Зберегти логи - Open folder with logs - Відкрити папку з логами + Відкрити папку з логами - + + Save Зберегти - + + Logs files (*.log) Logs files (*.log) - + + Logs file saved Файл з логами збережено - Save logs to file - Зберегти логи в файл + Зберегти логи в файл - + + Enable logs + + + + Clear logs? Очистити логи? - + Continue Продовжити - + Cancel Відмінити - + Logs have been cleaned up Логи видалено - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs Видалити логи @@ -2151,22 +2325,46 @@ Already installed containers were found on the server. All installed containers Налаштування - Clear %1 profile - Очистити профіль %1 + Очистити профіль %1 - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + Clear %1 profile? Очистити профіль %1? - + + The connection configuration will be deleted for this device only + + + + Unable to clear %1 profile while there is an active connection Неможливо очистити профіль %1 під час активного підключення - + Cannot remove active container Неможливо видалити активний контейнер @@ -2176,17 +2374,17 @@ Already installed containers were found on the server. All installed containers - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + All users with whom you shared a connection will no longer be able to connect to it. Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. @@ -2195,14 +2393,14 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - - + + Continue Продовжити - - + + Cancel Відмінити @@ -2393,7 +2591,7 @@ It's okay as long as it's from someone you trust. Виберіть що у вас є - + File with connection settings Файл з налаштуваннями підключення @@ -2407,77 +2605,87 @@ It's okay as long as it's from someone you trust. Підключення - + + Settings + Налаштування + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code Вставте ключ, додайте файл конфігурації або відскануйте QR-код - + Insert key Вставити ключ - + Insert Вставити - + Continue Продовжити - + Other connection options Інші параметри підключення - + VPN by Amnezia VPN від Amnezia - + Connect to classic paid and free VPN services from Amnezia Підключайтеся до звичайних платних та безкоштовних VPN-сервісів від Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server Налаштуйте Amnezia VPN на власному сервері - + Restore from backup Відновити із бекапа - + Open backup file Відкрити бекап файл - + Backup files (*.backup) Файли резервної копії (*.backup) - + Open config file Відкрити файл з конфігурацією - + QR code QR-код - + I have nothing У мене нічого нема @@ -2691,7 +2899,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Встановити - + The port must be in the range of 1 to 65535 Порт повинен бути в межах від 1 до 65535 @@ -3009,7 +3217,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Користувач більше не зможе підключатись до вашого сервера - + Continue Продовжити @@ -3086,12 +3294,17 @@ and will not be shared or disclosed to the Amnezia or any third parties Поділитись - + + Access error! + Помилка доступу! + + + Connection to Підключення до - + File with connection settings to Файл з налаштуванням доступу до @@ -3108,6 +3321,11 @@ and will not be shared or disclosed to the Amnezia or any third parties Settings restored from backup file Відновлення налаштувань із бекап файлу + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -3146,12 +3364,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Пароль не знайдено - + Could not open keystore Could not open keystore - + Could not remove private key from keystore Could not remove private key from keystore @@ -3327,27 +3545,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Could not open keystore - + Could not create private key generator Could not create private key generator - + Could not generate new private key Could not generate new private key - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create encryption cipher Could not create encryption cipher - + Could not encrypt data Could not encrypt data @@ -4167,7 +4385,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + All settings have been reset to default values Всі налаштування були скинуті до значення "По замовчуванню" @@ -4176,7 +4394,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Кеш профілю очищено - + Backup file is corrupted Backup файл пошкодженно @@ -4308,7 +4526,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Mbps diff --git a/client/translations/amneziavpn_ur_PK.ts b/client/translations/amneziavpn_ur_PK.ts index b18d60e7..cf445bfa 100644 --- a/client/translations/amneziavpn_ur_PK.ts +++ b/client/translations/amneziavpn_ur_PK.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + %1 MBit/s - + %1 days - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free - + %1 $/month @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation تشکیل کی تیاری کے دوران منقطع ہونا ممکن نہیں ہے @@ -186,9 +191,8 @@ ExportController - Access error! - رساءی ناممکن! + رساءی ناممکن! @@ -253,18 +257,18 @@ Can't be disabled for current server فائل کو کھولنے سے قاصر ہے - - + + Invalid configuration file غلط کنفیگریشن فائل - + Scanned %1 of %2. سکین%1 کی%2. - + In the imported configuration, potentially dangerous lines were found: @@ -443,6 +447,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint + + + Dev gateway environment + + PageHome @@ -477,10 +486,63 @@ Already installed containers were found on the server. All installed containers فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں + + PageProtocolAwgClientSettings + + + AmneziaWG settings + امنیزیا وی جی کی ترتیبات + + + + MTU + ام ٹی یو + + + + Server settings + + + + + Port + پورٹ + + + + Save + + + + + Save settings? + ترتیبات محفوظ کریں? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا + + PageProtocolAwgSettings - + AmneziaWG settings امنیزیا وی جی کی ترتیبات @@ -490,92 +552,91 @@ Already installed containers were found on the server. All installed containers پورٹ - MTU - ام ٹی یو + ام ٹی یو - + All users with whom you shared a connection with will no longer be able to connect to it. آپ جن لوگوں کے ساتھ آپ نے اس کنکشن کا اشتراک کیا تھا، وہ اس سے مزید جڑ نہیں سکیں گے۔ - + Save محفوظ کریں - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique H1 تا H4 فیلڈز کی قیمتیں مخصوص ہونی چاہیے - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) S1 + پیغام شروع کار (148) کے فیلڈ کی قیمت S2 + پیغام جواب (92) کے سائز کے برابر نہیں ہونی چاہئے - + Save settings? ترتیبات محفوظ کریں? - + Continue جاری رکھیں - + Cancel منسوخ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -862,30 +923,102 @@ Already installed containers were found on the server. All installed containers جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا + + PageProtocolWireGuardClientSettings + + + WG settings + وائر گارڈ ترتیبات + + + + MTU + ام ٹی یو + + + + Server settings + + + + + Port + پورٹ + + + + Save + + + + + Save settings? + ترتیبات محفوظ کریں? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا + + PageProtocolWireGuardSettings - + WG settings وائر گارڈ ترتیبات - + Port پورٹ - - MTU - ام ٹی یو + + Save settings? + ترتیبات محفوظ کریں? - + + All users with whom you shared a connection with will no longer be able to connect to it. + + + + + Continue + + + + + Cancel + + + + MTU + ام ٹی یو + + + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا - + Save محفوظ کریں @@ -1241,9 +1374,13 @@ Already installed containers were found on the server. All installed containers - Mail - میل + میل + + + + support@amnezia.org + @@ -1251,32 +1388,37 @@ Already installed containers were found on the server. All installed containers جائزہ اور بگ رپورٹس کے لئے - + + Copied + + + + GitHub گِٹ ہَب - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website ویب سائٹ - + Software version: %1 سافٹ ویئر ورژن: %1 - + Check for updates اپ ڈیٹس چیک کریں - + Privacy Policy رازداری کی پالیسی @@ -1733,72 +1875,108 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - لاگنگ فعال ہے۔ یاد رہے کہ لاگوں کو 14 دنوں کے بعد خود بخود غیر فعال کر دیا جائے گا، اور تمام لاگ فائلیں حذف کردی جائیں گی. + لاگنگ فعال ہے۔ یاد رہے کہ لاگوں کو 14 دنوں کے بعد خود بخود غیر فعال کر دیا جائے گا، اور تمام لاگ فائلیں حذف کردی جائیں گی. - + Logging لاگنگ - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. اس فعل کو فعال کرنے سے، ایپلیکیشن کے لاگ خود بخود محفوظ ہوجائیں گے۔ پہلے سے، لاگنگ کی فعالیت غیر فعال ہوتی ہے۔ اگر ایپلیکیشن میں کوئی خرابی ہو، تو لاگ کو بچانا فعال کریں. - Save logs - لاگوں کو محفوظ کریں + لاگوں کو محفوظ کریں - Open folder with logs - فائلوں کے فولڈر کو کھولیں + فائلوں کے فولڈر کو کھولیں - + + Save محفوظ - + + Logs files (*.log) لاگ فائلیں (*.log) - + + Logs file saved لاگ فائل محفوظ ہوگئی - Save logs to file - لاگوں کو فائل میں محفوظ کریں + لاگوں کو فائل میں محفوظ کریں - + + Enable logs + + + + Clear logs? کیا آپ لاگوں کو صاف کرنا چاہتے ہیں؟ - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Logs have been cleaned up تم مسح السجلاتلاگوں کو صاف کر دیا گیا ہے - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs لاگوں کو صاف کریں @@ -1958,22 +2136,21 @@ Already installed containers were found on the server. All installed containers ترتیبات - Clear %1 profile - %1 پروفائل کو صاف کریں + %1 پروفائل کو صاف کریں - + Clear %1 profile? کیا آپ واقعی %1 پروفائل کو صاف کرنا چاہتے ہیں؟ - + Unable to clear %1 profile while there is an active connection فعال کنکشن کے دوران %1 پروفائل کو صاف نہیں کیا جا سکتا - + Cannot remove active container فعال کنٹینر کو ہٹانا ممکن نہیں @@ -1983,29 +2160,54 @@ Already installed containers were found on the server. All installed containers - + Remove ہٹائیں - + All users with whom you shared a connection will no longer be able to connect to it. آپ نے جن کے ساتھ کنکشن شئیر کیا تھا، ان تمام صارفین کو اس سے جڑنے کی اجازت نہیں ہوگی. - + Remove %1 from server? کیا آپ سرور سے %1 کو ہٹانا چاہتے ہیں؟ - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Continue براہ کرم جاری رکھیں - - + + Cancel منسوخ @@ -2189,82 +2391,92 @@ Already installed containers were found on the server. All installed containers کنکشن - + + Settings + ترتیبات + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code - + Insert key - + Insert داخل کریں - + Continue - + Other connection options - + VPN by Amnezia - + Connect to classic paid and free VPN services from Amnezia - + Self-hosted VPN - + Configure Amnezia VPN on your own server - + Restore from backup بیک اپ سے بحال کریں - + Open backup file بیک اپ فائل کو کھولیں - + Backup files (*.backup) بیک اپ فائلیں (*.backup) - + File with connection settings کنکشن کی ترتیبات والی فائل - + Open config file کنفیگ فائل کو کھولیں - + QR code QR کوڈ - + I have nothing میرے پاس کچھ نہیں ہے @@ -2436,7 +2648,7 @@ Already installed containers were found on the server. All installed containers انسٹال - + The port must be in the range of 1 to 65535 @@ -2808,12 +3020,17 @@ Already installed containers were found on the server. All installed containers شیئر - + + Access error! + رساءی ناممکن! + + + Connection to کنکشن کو - + File with connection settings to کنکشن کی ترتیبات کی فائل @@ -2830,6 +3047,11 @@ Already installed containers were found on the server. All installed containers Settings restored from backup file ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -2868,12 +3090,12 @@ Already installed containers were found on the server. All installed containers پاس ورڈ نہیں ملا - + Could not open keystore کی اسٹور کھولا نہیں جا سکا - + Could not remove private key from keystore خصوصی کلید کو کی اسٹور سے ہٹانا نہیں ہو سکا @@ -3049,27 +3271,27 @@ Already installed containers were found on the server. All installed containers کی اسٹور کھولنے میں ناکام - + Could not create private key generator پرائیویٹ کلید جنریٹر تخلیق نہیں کیا - + Could not generate new private key نیا نجی کلید تخلیق نہیں کیا جا سکا - + Could not retrieve private key from keystore کی اسٹور سے نجی کلید حاصل نہیں کیا - + Could not create encryption cipher تشکیل تشکیل نہیں کر سکا - + Could not encrypt data ڈیٹا کو محفوظ کرنے میں ناکام @@ -3709,12 +3931,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted بیک اپ فائل خراب ہو گئی ہے - + All settings have been reset to default values تمام ترتیبات کو ڈیفالٹ اقدار پر دوبارہ ترتیب دیا گیا ہے @@ -3846,7 +4068,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps ایم بی پی ایس diff --git a/client/translations/amneziavpn_zh_CN.ts b/client/translations/amneziavpn_zh_CN.ts index 423c9e00..39b6bee0 100644 --- a/client/translations/amneziavpn_zh_CN.ts +++ b/client/translations/amneziavpn_zh_CN.ts @@ -4,47 +4,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. - + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + %1 MBit/s - + %1 days - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free - + %1 $/month @@ -75,7 +80,7 @@ ConnectButton - + Unable to disconnect during configuration preparation @@ -186,9 +191,8 @@ ExportController - Access error! - 访问错误 + 访问错误 @@ -258,18 +262,18 @@ Can't be disabled for current server - - + + Invalid configuration file - + Scanned %1 of %2. 扫描 %1 of %2. - + In the imported configuration, potentially dangerous lines were found: @@ -469,6 +473,11 @@ Already installed containers were found on the server. All installed containers Gateway endpoint + + + Dev gateway environment + + PageHome @@ -503,10 +512,63 @@ Already installed containers were found on the server. All installed containers 已建立连接时无法更改服务器配置 + + PageProtocolAwgClientSettings + + + AmneziaWG settings + AmneziaWG 配置 + + + + MTU + + + + + Server settings + + + + + Port + 端口 + + + + Save + 保存 + + + + Save settings? + 保存设置? + + + + Only the settings for this device will be changed + + + + + Continue + 继续 + + + + Cancel + 取消 + + + + Unable change settings while there is an active connection + + + PageProtocolAwgSettings - + AmneziaWG settings AmneziaWG 配置 @@ -515,11 +577,6 @@ Already installed containers were found on the server. All installed containers Port 端口 - - - MTU - - Remove AmneziaWG 移除AmneziaWG @@ -529,87 +586,87 @@ Already installed containers were found on the server. All installed containers 从服务上移除AmneziaWG? - + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Save 保存 - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + Save settings? 保存设置? - + Continue 继续 - + Cancel 取消 - + Unable change settings while there is an active connection @@ -933,24 +990,82 @@ Already installed containers were found on the server. All installed containers - PageProtocolWireGuardSettings + PageProtocolWireGuardClientSettings - + WG settings - - Port - 端口 - - - + MTU - + + Server settings + + + + + Port + 端口 + + + + Save + 保存 + + + + Save settings? + 保存设置? + + + + Only the settings for this device will be changed + + + + + Continue + 继续 + + + + Cancel + 取消 + + + + Unable change settings while there is an active connection + + + + + PageProtocolWireGuardSettings + + + WG settings + + + + + Port + 端口 + + + + Save settings? + 保存设置? + + + + All users with whom you shared a connection with will no longer be able to connect to it. + 与您共享连接的所有用户将无法再连接到该连接。 + + + Unable change settings while there is an active connection @@ -959,15 +1074,17 @@ Already installed containers were found on the server. All installed containers 与您共享连接的所有用户将无法再连接到该连接。 + Continue - 继续 + 继续 + Cancel - 取消 + 取消 - + Save 保存 @@ -1329,9 +1446,13 @@ And if you don't like the app, all the more support it - the donation will - Mail - 邮件 + 邮件 + + + + support@amnezia.org + @@ -1339,32 +1460,37 @@ And if you don't like the app, all the more support it - the donation will 用于评论和提交软件的缺陷 - + + Copied + + + + GitHub GitHub - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website 官网 - + Software version: %1 软件版本: %1 - + Check for updates 检查更新 - + Privacy Policy 隐私政策 @@ -1849,72 +1975,104 @@ And if you don't like the app, all the more support it - the donation will PageSettingsLogging - - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - - - - + Logging 日志 - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. 默认情况下,日志功能是禁用的。如果应用程序出现故障,则启用日志保存功能。 - Save logs - 记录日志 + 记录日志 - Open folder with logs - 打开日志文件夹 + 打开日志文件夹 - + + Save 保存 - + + Logs files (*.log) - + + Logs file saved 日志文件已保存 - Save logs to file - 保存日志到文件 + 保存日志到文件 - + + Enable logs + + + + Clear logs? 清理日志? - + Continue 继续 - + Cancel 取消 - + Logs have been cleaned up 日志已清理 - + + Client logs + + + + + AmneziaVPN logs + + + + + + Open logs folder + + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs 清理日志 @@ -2102,12 +2260,7 @@ And if you don't like the app, all the more support it - the donation will 配置 - - Clear %1 profile - - - - + Clear %1 profile? @@ -2117,22 +2270,47 @@ And if you don't like the app, all the more support it - the donation will - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + Unable to clear %1 profile while there is an active connection - + Remove 移除 - + All users with whom you shared a connection will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Cannot remove active container @@ -2145,7 +2323,7 @@ And if you don't like the app, all the more support it - the donation will 从服务器 - + Remove %1 from server? 从服务器移除 %1 ? @@ -2154,14 +2332,14 @@ And if you don't like the app, all the more support it - the donation will 与您共享连接的所有用户将无法再连接到此链接 - - + + Continue 继续 - - + + Cancel 取消 @@ -2376,82 +2554,92 @@ It's okay as long as it's from someone you trust. 连接 - + + Settings + 设置 + + + + Enable logs + + + + Insert the key, add a configuration file or scan the QR-code - + Insert key - + Insert 插入 - + Continue 继续 - + Other connection options - + VPN by Amnezia - + Connect to classic paid and free VPN services from Amnezia - + Self-hosted VPN - + Configure Amnezia VPN on your own server - + Restore from backup 从备份还原 - + Open backup file 打开备份文件 - + Backup files (*.backup) - + File with connection settings 包含连接配置的文件 - + Open config file 打开配置文件 - + QR code 二维码 - + I have nothing 我没有 @@ -2661,7 +2849,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 安装 - + The port must be in the range of 1 to 65535 @@ -3077,12 +3265,17 @@ and will not be shared or disclosed to the Amnezia or any third parties 共享 - + + Access error! + 访问错误 + + + Connection to 连接到 - + File with connection settings to 连接配置文件的内容为 @@ -3099,6 +3292,11 @@ and will not be shared or disclosed to the Amnezia or any third parties Settings restored from backup file 从备份文件还原配置 + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType @@ -3137,12 +3335,12 @@ and will not be shared or disclosed to the Amnezia or any third parties 未发现密码 - + Could not open keystore 无法打开密钥库 - + Could not remove private key from keystore 无法从密钥库中删除私钥 @@ -3318,27 +3516,27 @@ and will not be shared or disclosed to the Amnezia or any third parties 无法打开密钥库 - + Could not create private key generator 无法创建私钥生成器 - + Could not generate new private key 无法生成新的私钥 - + Could not retrieve private key from keystore 无法从密钥库检索私钥 - + Could not create encryption cipher 无法创建加密密码 - + Could not encrypt data 无法加密数据 @@ -4169,12 +4367,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted 备份文件已损坏 - + All settings have been reset to default values 所配置恢复为默认值 @@ -4314,7 +4512,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps From 46cd740a84a5bdac6922d2b3b5795f4f10d21d64 Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 17 Sep 2024 01:14:13 +0400 Subject: [PATCH 21/90] added domain name resolving before connection for wg/awg and xray protocols (#814) added domain name resolving before connection --- .../utils/src/main/kotlin/net/NetworkUtils.kt | 2 +- client/core/controllers/serverController.cpp | 25 ++++++----- .../vpnConfigurationController.cpp | 4 +- client/core/networkUtilities.cpp | 5 ++- client/protocols/wireguardprotocol.cpp | 9 +++- client/protocols/xrayprotocol.cpp | 7 ++- client/protocols/xrayprotocol.h | 1 + client/server_scripts/xray/run_container.sh | 2 +- client/server_scripts/xray/start.sh | 2 +- client/ui/controllers/importController.cpp | 45 ++++++++++--------- .../qml/Pages2/PageSetupWizardCredentials.qml | 3 -- client/vpnconnection.cpp | 2 +- 12 files changed, 61 insertions(+), 46 deletions(-) diff --git a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt index b75748be..5c680d31 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt @@ -35,7 +35,7 @@ fun getLocalNetworks(context: Context, ipv6: Boolean): List { return emptyList() } -fun parseInetAddress(address: String): InetAddress = parseNumericAddressCompat(address) +fun parseInetAddress(address: String): InetAddress = InetAddress.getByName(address) private val parseNumericAddressCompat: (String) -> InetAddress = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 9a745e3d..b6795a01 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -104,7 +104,8 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti if (e) return e; - QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash")); + QString runner = + QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash")); e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName); @@ -424,7 +425,7 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden if (errorCode) return errorCode; - errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),dockerFilePath); + errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(), dockerFilePath); if (errorCode) return errorCode; @@ -435,9 +436,10 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden return ErrorCode::NoError; }; - errorCode = runScript(credentials, - replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)), - cbReadStdOut); + errorCode = + runScript(credentials, + replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)), + cbReadStdOut); if (errorCode) return errorCode; @@ -619,13 +621,15 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential // Socks5 proxy vars vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } }); - auto username = socks5ProxyConfig.value(config_key:: userName).toString(); + auto username = socks5ProxyConfig.value(config_key::userName).toString(); auto password = socks5ProxyConfig.value(config_key::password).toString(); QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : ""; - vars.append({ { "$SOCKS5_USER", socks5user } }); - vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } }); + vars.append({ { "$SOCKS5_USER", socks5user } }); + vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } }); - QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); + QString serverIp = (container != DockerContainer::Awg && container != DockerContainer::WireGuard && container != DockerContainer::Xray) + ? NetworkUtilities::getIPAddress(credentials.hostName) + : credentials.hostName; if (!serverIp.isEmpty()) { vars.append({ { "$SERVER_IP_ADDRESS", serverIp } }); } else { @@ -711,7 +715,8 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential udpProtoScript.append("' | grep -i udp"); tcpProtoScript.append(" | grep LISTEN"); - ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); + ErrorCode errorCode = + runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); if (errorCode != ErrorCode::NoError) { return errorCode; } diff --git a/client/core/controllers/vpnConfigurationController.cpp b/client/core/controllers/vpnConfigurationController.cpp index 291c429c..52f42c42 100644 --- a/client/core/controllers/vpnConfigurationController.cpp +++ b/client/core/controllers/vpnConfigurationController.cpp @@ -100,10 +100,8 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPairprocessConfigWithLocalSettings(dns, isApiConfig, protocolConfigString); QJsonObject vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - - // add mtu for old configs if (container == DockerContainer::Awg || container == DockerContainer::WireGuard) { + // add mtu for old configs if (vpnConfigData[config_key::mtu].toString().isEmpty()) { vpnConfigData[config_key::mtu] = container == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; } diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index 7ffd4c41..a5825f0d 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -109,7 +109,10 @@ QStringList NetworkUtilities::summarizeRoutes(const QStringList &ips, const QStr QString NetworkUtilities::getIPAddress(const QString &host) { - if (ipAddressRegExp().match(host).hasMatch()) { + QHostAddress address(host); + if (QAbstractSocket::IPv4Protocol == address.protocol()) { + return host; + } else if (QAbstractSocket::IPv6Protocol == address.protocol()) { return host; } diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 61b2e261..80579f16 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -4,9 +4,8 @@ #include #include -#include "logger.h" -#include "utilities.h" #include "wireguardprotocol.h" +#include "core/networkUtilities.h" #include "mozilla/localsocketcontroller.h" @@ -37,6 +36,12 @@ void WireguardProtocol::stop() ErrorCode WireguardProtocol::startMzImpl() { + QString protocolName = m_rawConfig.value("protocol").toString(); + QJsonObject vpnConfigData = m_rawConfig.value(protocolName + "_config_data").toObject(); + vpnConfigData[config_key::hostName] = NetworkUtilities::getIPAddress(vpnConfigData.value(config_key::hostName).toString()); + m_rawConfig.insert(protocolName + "_config_data", vpnConfigData); + m_rawConfig[config_key::hostName] = NetworkUtilities::getIPAddress(m_rawConfig[config_key::hostName].toString()); + m_impl->activate(m_rawConfig); return ErrorCode::NoError; } diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 15106c51..a45be887 100644 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -43,7 +43,9 @@ ErrorCode XrayProtocol::start() m_xrayCfgFile.setAutoRemove(false); #endif m_xrayCfgFile.open(); - m_xrayCfgFile.write(QJsonDocument(m_xrayConfig).toJson()); + QString config = QJsonDocument(m_xrayConfig).toJson(); + config.replace(m_remoteHost, m_remoteAddress); + m_xrayCfgFile.write(config.toUtf8()); m_xrayCfgFile.close(); QStringList args = QStringList() << "-c" << m_xrayCfgFile.fileName() << "-format=json"; @@ -238,7 +240,8 @@ void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration) } m_xrayConfig = xrayConfiguration; m_localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort).toInt(); - m_remoteAddress = configuration.value(amnezia::config_key::hostName).toString(); + m_remoteHost = configuration.value(amnezia::config_key::hostName).toString(); + m_remoteAddress = NetworkUtilities::getIPAddress(m_remoteHost); m_routeMode = configuration.value(amnezia::config_key::splitTunnelType).toInt(); m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString(); m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString(); diff --git a/client/protocols/xrayprotocol.h b/client/protocols/xrayprotocol.h index 8df2afb2..c1d62897 100644 --- a/client/protocols/xrayprotocol.h +++ b/client/protocols/xrayprotocol.h @@ -26,6 +26,7 @@ private: static QString tun2SocksExecPath(); private: int m_localPort; + QString m_remoteHost; QString m_remoteAddress; int m_routeMode; QJsonObject m_configData; diff --git a/client/server_scripts/xray/run_container.sh b/client/server_scripts/xray/run_container.sh index bd00b992..40cc6a09 100644 --- a/client/server_scripts/xray/run_container.sh +++ b/client/server_scripts/xray/run_container.sh @@ -13,5 +13,5 @@ sudo docker network connect amnezia-dns-net $CONTAINER_NAME sudo docker exec -i $CONTAINER_NAME bash -c 'mkdir -p /dev/net; if [ ! -c /dev/net/tun ]; then mknod /dev/net/tun c 10 200; fi' # Prevent to route packets outside of the container in case if server behind of the NAT -sudo docker exec -i $CONTAINER_NAME sh -c "ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up" +#sudo docker exec -i $CONTAINER_NAME sh -c "ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up" diff --git a/client/server_scripts/xray/start.sh b/client/server_scripts/xray/start.sh index 2ebce5ed..0148552f 100644 --- a/client/server_scripts/xray/start.sh +++ b/client/server_scripts/xray/start.sh @@ -3,7 +3,7 @@ # This scripts copied from Amnezia client to Docker container to /opt/amnezia and launched every time container starts echo "Container startup" -ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up +#ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index f64ba36e..32170fb6 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -4,12 +4,12 @@ #include #include #include -#include #include +#include -#include "utilities.h" -#include "core/serialization/serialization.h" #include "core/errorstrings.h" +#include "core/serialization/serialization.h" +#include "utilities.h" #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" @@ -96,36 +96,40 @@ bool ImportController::extractConfigFromData(QString data) if (config.startsWith("vless://")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("vmess://") && config.contains("@")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("vmess://")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("trojan://")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("ss://") && !config.contains("plugin=")) { m_configType = ConfigTypes::ShadowSocks; - m_config = extractXrayConfig(Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), prefix); return m_config.empty() ? false : true; } @@ -354,20 +358,19 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) QJsonObject lastConfig; lastConfig[config_key::config] = data; - const static QRegularExpression hostNameAndPortRegExp("Endpoint = (.*):([0-9]*)"); - QRegularExpressionMatch hostNameAndPortMatch = hostNameAndPortRegExp.match(data); + auto url { QUrl::fromUserInput(configMap.value("Endpoint")) }; QString hostName; QString port; - if (hostNameAndPortMatch.hasCaptured(1)) { - hostName = hostNameAndPortMatch.captured(1); + if (!url.host().isEmpty()) { + hostName = url.host(); } else { - qDebug() << "Key parameter 'Endpoint' is missing"; + qDebug() << "Key parameter 'Endpoint' is missing or has an invalid format"; emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); return QJsonObject(); } - if (hostNameAndPortMatch.hasCaptured(2)) { - port = hostNameAndPortMatch.captured(2); + if (url.port() != -1) { + port = QString::number(url.port()); } else { port = protocols::wireguard::defaultPort; } @@ -499,7 +502,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, const QStri if (m_configType == ConfigTypes::ShadowSocks) { config[config_key::defaultContainer] = "amnezia-ssxray"; } else { - config[config_key::defaultContainer] = "amnezia-xray"; + config[config_key::defaultContainer] = "amnezia-xray"; } if (description.isEmpty()) { config[config_key::description] = m_settings->nextAvailableServerName(); diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 7f1c3eed..aced12b1 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -60,9 +60,6 @@ PageType { Layout.fillWidth: true headerText: qsTr("Server IP address [:port]") textFieldPlaceholderText: qsTr("255.255.255.255:22") - textField.validator: RegularExpressionValidator { - regularExpression: InstallController.ipAddressPortRegExp() - } textField.onFocusChanged: { textField.text = textField.text.replace(/^\s+|\s+$/g, '') diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index c4022be6..591e396f 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -233,7 +233,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede } #endif - m_remoteAddress = credentials.hostName; + m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName); emit connectionStateChanged(Vpn::ConnectionState::Connecting); m_vpnConfiguration = vpnConfiguration; From 87cb5f620aa9abbd9a5f7a610c9f0365c9f4eb88 Mon Sep 17 00:00:00 2001 From: pokamest Date: Mon, 16 Sep 2024 22:18:45 +0100 Subject: [PATCH 22/90] Bump version to 4.8.0.4 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 287d92f2..2c2f7bf6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.0.1 +project(${PROJECT} VERSION 4.8.0.4 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 59) +set(APP_ANDROID_VERSION_CODE 60) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 253ae757954398c4d6322c270b36b2d4d0c40cb0 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 17 Sep 2024 08:28:44 +0200 Subject: [PATCH 23/90] Added list of AllowedIPs for WireGuard/AWG connections on Share -> Users ->ExpandedContent page (#1055) Added list of AllowedIPs for WireGuard/AWG connections on Share -> Users ->ExpandedContent page --- client/ui/models/clientManagementModel.cpp | 14 +++++++++++--- client/ui/models/clientManagementModel.h | 4 +++- client/ui/qml/Pages2/PageShare.qml | 8 ++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index f2117f75..7d3be2cb 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -20,6 +20,7 @@ namespace constexpr char latestHandshake[] = "latestHandshake"; constexpr char dataReceived[] = "dataReceived"; constexpr char dataSent[] = "dataSent"; + constexpr char allowedIps[] = "allowedIps"; } } @@ -49,6 +50,7 @@ QVariant ClientManagementModel::data(const QModelIndex &index, int role) const case LatestHandshakeRole: return userData.value(configKey::latestHandshake).toString(); case DataReceivedRole: return userData.value(configKey::dataReceived).toString(); case DataSentRole: return userData.value(configKey::dataSent).toString(); + case AllowedIpsRole: return userData.value(configKey::allowedIps).toString(); } return QVariant(); @@ -141,6 +143,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co userData[configKey::dataSent] = client.dataSent; } + if (!client.allowedIps.isEmpty()) { + userData[configKey::allowedIps] = client.allowedIps; + } + obj[configKey::userData] = userData; m_clientsTable.replace(i, obj); break; @@ -266,8 +272,9 @@ ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const S const auto peerList = parts.filter("peer:"); const auto latestHandshakeList = parts.filter("latest handshake:"); const auto transferredDataList = parts.filter("transfer:"); + const auto allowedIpsList = parts.filter("allowed ips:"); - if (latestHandshakeList.isEmpty() || transferredDataList.isEmpty() || peerList.isEmpty()) { + if (allowedIpsList.isEmpty() || latestHandshakeList.isEmpty() || transferredDataList.isEmpty() || peerList.isEmpty()) { return error; } @@ -281,19 +288,20 @@ ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const S } }; - for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size(); ++i) { + for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size() && i < allowedIpsList.size(); ++i) { const auto transferredData = getStrValue(transferredDataList[i]).split(","); auto latestHandshake = getStrValue(latestHandshakeList[i]); auto serverBytesReceived = transferredData.front().trimmed(); auto serverBytesSent = transferredData.back().trimmed(); + auto allowedIps = getStrValue(allowedIpsList[i]); changeHandshakeFormat(latestHandshake); serverBytesReceived.chop(QStringLiteral(" received").length()); serverBytesSent.chop(QStringLiteral(" sent").length()); - data.push_back({ getStrValue(peerList[i]), latestHandshake, serverBytesSent, serverBytesReceived }); + data.push_back({ getStrValue(peerList[i]), latestHandshake, serverBytesSent, serverBytesReceived, allowedIps }); } return error; diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index d64280a3..60132abe 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -17,7 +17,8 @@ public: CreationDateRole, LatestHandshakeRole, DataReceivedRole, - DataSentRole + DataSentRole, + AllowedIpsRole }; struct WgShowData @@ -26,6 +27,7 @@ public: QString latestHandshake; QString dataReceived; QString dataSent; + QString allowedIps; }; ClientManagementModel(std::shared_ptr settings, QObject *parent = nullptr); diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 33577d74..6640df36 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -840,6 +840,14 @@ PageType { text: qsTr("Data sent: %1").arg(dataSent) } + + ParagraphTextType { + color: textColumn.textColor + visible: allowedIps + Layout.fillWidth: true + + text: qsTr("Allowed IPs: %1").arg(allowedIps) + } } Item { From aa92ccd06d2aa279ca364220625b46c4e6c4a78e Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 17 Sep 2024 08:29:01 +0200 Subject: [PATCH 24/90] Small improve on next IP generation / WireGuard, AWG (#1054) Small improve on next IP generation --- client/configurators/wireguard_configurator.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index 27db92ff..0d083742 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -95,6 +95,18 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon stdOut.replace("/32", ""); QStringList ips = stdOut.split("\n", Qt::SkipEmptyParts); + // remove extra IPs from each line for case when user manually edited the wg0.conf + // and added there more IPs for route his itnernal networks, like: + // ... + // AllowedIPs = 10.8.1.6/32, 192.168.1.0/24, 192.168.2.0/24, ... + // ... + // without this code - next IP would be 1 if last item in 'ips' has format above + QStringList vpnIps; + for (const auto &ip : ips) { + vpnIps.append(ip.split(",", Qt::SkipEmptyParts).first().trimmed()); + } + ips = vpnIps; + // Calc next IP address if (ips.isEmpty()) { nextIpNumber = "2"; From 8948601caa4fed80573ff4946b2632ee82cf713a Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 17 Sep 2024 15:11:14 +0400 Subject: [PATCH 25/90] bugfix: fixed awg/wg persistentKeepAlive variable type --- client/platforms/ios/WGConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/platforms/ios/WGConfig.swift b/client/platforms/ios/WGConfig.swift index e3b67efe..0647abea 100644 --- a/client/platforms/ios/WGConfig.swift +++ b/client/platforms/ios/WGConfig.swift @@ -15,7 +15,7 @@ struct WGConfig: Decodable { let serverPublicKey: String let presharedKey: String? var allowedIPs: [String] - var persistentKeepAlive: String + var persistentKeepAlive: Int let splitTunnelType: Int let splitTunnelSites: [String] From 6f94f4646adf6c12d8b9292104ac2f7bab1e66f3 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Thu, 19 Sep 2024 11:18:40 +0100 Subject: [PATCH 26/90] Fix Xray connection timeout for Windows --- client/protocols/xrayprotocol.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 client/protocols/xrayprotocol.cpp diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp old mode 100644 new mode 100755 index a45be887..41e955b0 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -146,7 +146,7 @@ ErrorCode XrayProtocol::startTun2Sock() IpcClient::Interface()->updateResolvers("utun22", dnsAddr); #endif #ifdef Q_OS_WINDOWS - QThread::msleep(15000); + QThread::msleep(7000); #endif #ifdef Q_OS_LINUX QThread::msleep(1000); From 138e6f70a47d09caabbb1d65eeb0c56f13471cef Mon Sep 17 00:00:00 2001 From: albexk Date: Thu, 19 Sep 2024 13:31:59 +0300 Subject: [PATCH 27/90] Fix domain name resolution for XRay --- CMakeLists.txt | 4 ++-- .../android/utils/src/main/kotlin/net/NetworkUtils.kt | 2 +- client/android/xray/src/main/kotlin/Xray.kt | 11 ++++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c2f7bf6..b84c25f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.0.4 +project(${PROJECT} VERSION 4.8.0.5 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 60) +set(APP_ANDROID_VERSION_CODE 61) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt index 5c680d31..784aa352 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt @@ -60,7 +60,7 @@ private val parseNumericAddressCompat: (String) -> InetAddress = internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6 .replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2") -internal val InetAddress.ip: String +val InetAddress.ip: String get() = if (this is Inet4Address) { hostAddress!! } else { diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index ec22a4bd..6e37c9c2 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -17,6 +17,7 @@ import org.amnezia.vpn.protocol.xray.libXray.Logger import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork +import org.amnezia.vpn.util.net.ip import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject @@ -61,7 +62,15 @@ class Xray : Protocol() { .put("loglevel", "warning") .put("access", "none") // disable access log - start(xrayConfig, xrayJsonConfig.toString(), vpnBuilder, protect) + var xrayJsonConfigString = xrayJsonConfig.toString() + config.getString("hostName").let { hostName -> + val ipAddress = parseInetAddress(hostName).ip + if (hostName != ipAddress) { + xrayJsonConfigString = xrayJsonConfigString.replace(hostName, ipAddress) + } + } + + start(xrayConfig, xrayJsonConfigString, vpnBuilder, protect) state.value = CONNECTED isRunning = true } From b881d92a800f94aeedbf24ed1d8ff7caf522e578 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 19 Sep 2024 15:52:58 +0400 Subject: [PATCH 28/90] bugfix: returned awg/wg persistentKeepAlive variable type to string --- client/configurators/wireguard_configurator.cpp | 2 +- client/platforms/ios/WGConfig.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index 0d083742..3f96e74c 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -199,7 +199,7 @@ QString WireguardConfigurator::createConfig(const ServerCredentials &credentials jConfig[config_key::server_pub_key] = connData.serverPubKey; jConfig[config_key::mtu] = wireguarConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); - jConfig[config_key::persistent_keep_alive] = 25; + jConfig[config_key::persistent_keep_alive] = "25"; QJsonArray allowedIps { "0.0.0.0/0", "::/0" }; jConfig[config_key::allowed_ips] = allowedIps; diff --git a/client/platforms/ios/WGConfig.swift b/client/platforms/ios/WGConfig.swift index 0647abea..e3b67efe 100644 --- a/client/platforms/ios/WGConfig.swift +++ b/client/platforms/ios/WGConfig.swift @@ -15,7 +15,7 @@ struct WGConfig: Decodable { let serverPublicKey: String let presharedKey: String? var allowedIPs: [String] - var persistentKeepAlive: Int + var persistentKeepAlive: String let splitTunnelType: Int let splitTunnelSites: [String] From 864879058322bfbbdf89ef8bd60a02a4e227d9d0 Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Thu, 19 Sep 2024 18:47:20 +0300 Subject: [PATCH 29/90] Fix iOS build on GHA --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e8ad8fdf..d9138516 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -181,7 +181,7 @@ jobs: - name: 'Install go' uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.22.1' cache: false - name: 'Setup gomobile' From 8735eee662d663f1d11a776ef5786d4f2ec465c6 Mon Sep 17 00:00:00 2001 From: albexk Date: Thu, 19 Sep 2024 23:38:58 +0300 Subject: [PATCH 30/90] Exclude protocol libraries from loading at application startup --- client/android/cloak/src/main/kotlin/Cloak.kt | 6 ++ .../amnezia/vpn/protocol/openvpn/OpenVpn.kt | 6 +- .../protocolApi/src/main/kotlin/Exceptions.kt | 1 - .../protocolApi/src/main/kotlin/Protocol.kt | 54 --------------- client/android/res/values/libs.xml | 1 - .../src/org/amnezia/vpn/AmneziaActivity.kt | 13 ++++ .../src/org/amnezia/vpn/AmneziaVpnService.kt | 2 +- .../utils/src/main/kotlin/LibraryLoader.kt | 66 +++++++++++++++++++ .../vpn/protocol/wireguard/Wireguard.kt | 2 +- 9 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 client/android/utils/src/main/kotlin/LibraryLoader.kt diff --git a/client/android/cloak/src/main/kotlin/Cloak.kt b/client/android/cloak/src/main/kotlin/Cloak.kt index 18a5e6c7..d408fb19 100644 --- a/client/android/cloak/src/main/kotlin/Cloak.kt +++ b/client/android/cloak/src/main/kotlin/Cloak.kt @@ -3,10 +3,16 @@ package org.amnezia.vpn.protocol.cloak import android.util.Base64 import net.openvpn.ovpn3.ClientAPI_Config import org.amnezia.vpn.protocol.openvpn.OpenVpn +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.json.JSONObject class Cloak : OpenVpn() { + override fun internalInit() { + super.internalInit() + if (!isInitialized) loadSharedLibrary(context, "ck-ovpn-plugin") + } + override fun parseConfig(config: JSONObject): ClientAPI_Config { val openVpnConfig = ClientAPI_Config() diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index fa0e19f1..22fe35cd 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -11,6 +11,7 @@ import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.VpnStartException +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.getLocalNetworks import org.amnezia.vpn.util.net.parseInetAddress @@ -34,7 +35,10 @@ open class OpenVpn : Protocol() { } override fun internalInit() { - if (!isInitialized) loadSharedLibrary(context, "ovpn3") + if (!isInitialized) { + loadSharedLibrary(context, "ovpn3") + loadSharedLibrary(context, "ovpnutil") + } if (this::scope.isInitialized) { scope.cancel() } diff --git a/client/android/protocolApi/src/main/kotlin/Exceptions.kt b/client/android/protocolApi/src/main/kotlin/Exceptions.kt index 739a327c..b80648b0 100644 --- a/client/android/protocolApi/src/main/kotlin/Exceptions.kt +++ b/client/android/protocolApi/src/main/kotlin/Exceptions.kt @@ -2,7 +2,6 @@ package org.amnezia.vpn.protocol sealed class ProtocolException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) -class LoadLibraryException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) class BadConfigException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) class VpnStartException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index 24cbc595..b5c382be 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -158,60 +158,6 @@ abstract class Protocol { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) vpnBuilder.setMetered(false) } - - companion object { - private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean { - Log.d(TAG, "Extracting library: $libraryName") - val apks = hashSetOf() - context.applicationInfo.run { - sourceDir?.let { apks += it } - splitSourceDirs?.let { apks += it } - } - for (abi in Build.SUPPORTED_ABIS) { - for (apk in apks) { - ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile -> - val mappedName = System.mapLibraryName(libraryName) - val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator) - val zipEntry = zipFile.getEntry(libraryZipPath) - zipEntry?.let { - Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}") - FileOutputStream(destination).use { outStream -> - zipFile.getInputStream(zipEntry).use { inStream -> - inStream.copyTo(outStream, 32 * 1024) - outStream.fd.sync() - } - } - } - return true - } - } - } - return false - } - - @SuppressLint("UnsafeDynamicallyLoadedCode") - fun loadSharedLibrary(context: Context, libraryName: String) { - Log.d(TAG, "Loading library: $libraryName") - try { - System.loadLibrary(libraryName) - return - } catch (_: UnsatisfiedLinkError) { - Log.d(TAG, "Failed to load library, try to extract it from apk") - } - var tempFile: File? = null - try { - tempFile = File.createTempFile("lib", ".so", context.codeCacheDir) - if (extractLibrary(context, libraryName, tempFile)) { - System.load(tempFile.absolutePath) - return - } - } catch (e: Exception) { - throw LoadLibraryException("Failed to load library apk: $libraryName", e) - } finally { - tempFile?.delete() - } - } - } } private fun VpnService.Builder.addAddress(addr: InetNetwork) = addAddress(addr.address, addr.mask) diff --git a/client/android/res/values/libs.xml b/client/android/res/values/libs.xml index fe63866f..3ccf1d80 100644 --- a/client/android/res/values/libs.xml +++ b/client/android/res/values/libs.xml @@ -3,7 +3,6 @@ - diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 9d1c31cb..d5026425 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.withContext import org.amnezia.vpn.protocol.getStatistics import org.amnezia.vpn.protocol.getStatus import org.amnezia.vpn.qt.QtAndroidController +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Prefs import org.json.JSONException @@ -158,6 +159,7 @@ class AmneziaActivity : QtActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "Create Amnezia activity: $intent") + loadLibs() window.apply { addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) statusBarColor = getColor(R.color.black) @@ -179,6 +181,17 @@ class AmneziaActivity : QtActivity() { runBlocking { vpnProto = proto.await() } } + private fun loadLibs() { + listOf( + "rsapss", + "crypto_3", + "ssl_3", + "ssh" + ).forEach { + loadSharedLibrary(this.applicationContext, it) + } + } + private fun registerBroadcastReceivers() { notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { registerBroadcastReceiver( diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 54330861..6a7da7c7 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -40,7 +40,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import org.amnezia.vpn.protocol.BadConfigException -import org.amnezia.vpn.protocol.LoadLibraryException import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.CONNECTING import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED @@ -50,6 +49,7 @@ import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN import org.amnezia.vpn.protocol.VpnException import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.putStatus +import org.amnezia.vpn.util.LoadLibraryException import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.net.NetworkState diff --git a/client/android/utils/src/main/kotlin/LibraryLoader.kt b/client/android/utils/src/main/kotlin/LibraryLoader.kt new file mode 100644 index 00000000..f1c6465e --- /dev/null +++ b/client/android/utils/src/main/kotlin/LibraryLoader.kt @@ -0,0 +1,66 @@ +package org.amnezia.vpn.util + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipFile + +private const val TAG = "LibraryLoader" + +object LibraryLoader { + private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean { + Log.d(TAG, "Extracting library: $libraryName") + val apks = hashSetOf() + context.applicationInfo.run { + sourceDir?.let { apks += it } + splitSourceDirs?.let { apks += it } + } + for (abi in Build.SUPPORTED_ABIS) { + for (apk in apks) { + ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile -> + val mappedName = System.mapLibraryName(libraryName) + val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator) + val zipEntry = zipFile.getEntry(libraryZipPath) + zipEntry?.let { + Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}") + FileOutputStream(destination).use { outStream -> + zipFile.getInputStream(zipEntry).use { inStream -> + inStream.copyTo(outStream, 32 * 1024) + outStream.fd.sync() + } + } + } + return true + } + } + } + return false + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + fun loadSharedLibrary(context: Context, libraryName: String) { + Log.d(TAG, "Loading library: $libraryName") + try { + System.loadLibrary(libraryName) + return + } catch (_: UnsatisfiedLinkError) { + Log.d(TAG, "Failed to load library, try to extract it from apk") + } + var tempFile: File? = null + try { + tempFile = File.createTempFile("lib", ".so", context.codeCacheDir) + if (extractLibrary(context, libraryName, tempFile)) { + System.load(tempFile.absolutePath) + return + } + } catch (e: Exception) { + throw LoadLibraryException("Failed to load library apk: $libraryName", e) + } finally { + tempFile?.delete() + } + } +} + +class LoadLibraryException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index c32ab8c2..31e7f9be 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -3,7 +3,6 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder import java.io.IOException import java.util.Locale -import java.util.TreeMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -13,6 +12,7 @@ import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.VpnStartException +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.asSequence import org.amnezia.vpn.util.net.InetEndpoint From a22a9448caa81d44cd480bbc73b28d2c65afdc7e Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Fri, 20 Sep 2024 04:12:22 -0700 Subject: [PATCH 31/90] Some XRay improvements (#1075) --- client/CMakeLists.txt | 1 + client/core/ipcclient.cpp | 16 ++- client/core/ipcclient.h | 5 + client/protocols/openvpnprotocol.cpp | 8 +- client/protocols/xrayprotocol.cpp | 185 +++++++++++---------------- client/protocols/xrayprotocol.h | 3 +- ipc/ipc_interface.rep | 1 - ipc/ipc_process_interface.rep | 1 - ipc/ipc_process_tun2socks.rep | 11 ++ ipc/ipcserver.cpp | 8 +- ipc/ipcserver.h | 4 + ipc/ipctun2socksprocess.cpp | 74 +++++++++++ ipc/ipctun2socksprocess.h | 52 ++++++++ service/server/CMakeLists.txt | 3 + service/server/localserver.cpp | 1 + service/server/localserver.h | 1 + 16 files changed, 252 insertions(+), 122 deletions(-) create mode 100644 ipc/ipc_process_tun2socks.rep create mode 100644 ipc/ipctun2socksprocess.cpp create mode 100644 ipc/ipctun2socksprocess.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 4d0c1c2a..2de5db48 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -61,6 +61,7 @@ qt_add_executable(${PROJECT} MANUAL_FINALIZATION) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep) + qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep) endif() qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc) diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index 3e364452..b44da1bf 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -29,6 +29,12 @@ QSharedPointer IpcClient::Interface() return Instance()->m_ipcClient; } +QSharedPointer IpcClient::InterfaceTun2Socks() +{ + if (!Instance()) return nullptr; + return Instance()->m_Tun2SocksClient; +} + bool IpcClient::init(IpcClient *instance) { m_instance = instance; @@ -44,6 +50,12 @@ bool IpcClient::init(IpcClient *instance) qWarning() << "IpcClient replica is not connected!"; } + Instance()->m_Tun2SocksClient.reset(Instance()->m_ClientNode.acquire()); + Instance()->m_Tun2SocksClient->waitForSource(1000); + + if (!Instance()->m_Tun2SocksClient->isReplicaValid()) { + qWarning() << "IpcClient::m_Tun2SocksClient replica is not connected!"; + } }); connect(Instance()->m_localSocket, &QLocalSocket::disconnected, [instance](){ @@ -51,16 +63,16 @@ bool IpcClient::init(IpcClient *instance) }); Instance()->m_localSocket->connectToServer(amnezia::getIpcServiceUrl()); - Instance()->m_localSocket->waitForConnected(); if (!Instance()->m_ipcClient) { qDebug() << "IpcClient::init failed"; return false; } + qDebug() << "IpcClient::init succeed"; - return Instance()->m_ipcClient->isReplicaValid(); + return (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid()); } QSharedPointer IpcClient::CreatePrivilegedProcess() diff --git a/client/core/ipcclient.h b/client/core/ipcclient.h index ab5d750a..ad2e6b6e 100644 --- a/client/core/ipcclient.h +++ b/client/core/ipcclient.h @@ -6,6 +6,7 @@ #include "ipc.h" #include "rep_ipc_interface_replica.h" +#include "rep_ipc_process_tun2socks_replica.h" #include "privileged_process.h" @@ -18,6 +19,7 @@ public: static IpcClient *Instance(); static bool init(IpcClient *instance); static QSharedPointer Interface(); + static QSharedPointer InterfaceTun2Socks(); static QSharedPointer CreatePrivilegedProcess(); bool isSocketConnected() const; @@ -28,8 +30,11 @@ private: ~IpcClient() override; QRemoteObjectNode m_ClientNode; + QRemoteObjectNode m_Tun2SocksNode; QSharedPointer m_ipcClient; QPointer m_localSocket; + QPointer m_tun2socksSocket; + QSharedPointer m_Tun2SocksClient; struct ProcessDescriptor { ProcessDescriptor () { diff --git a/client/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index 04a18327..4c2feb52 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -6,6 +6,7 @@ #include #include +#include "core/networkUtilities.h" #include "logger.h" #include "openvpnprotocol.h" #include "utilities.h" @@ -127,7 +128,6 @@ void OpenVpnProtocol::sendManagementCommand(const QString &command) uint OpenVpnProtocol::selectMgmtPort() { - for (int i = 0; i < 100; ++i) { quint32 port = QRandomGenerator::global()->generate(); port = (double)(65000 - 15001) * port / UINT32_MAX + 15001; @@ -137,7 +137,6 @@ uint OpenVpnProtocol::selectMgmtPort() if (ok) return port; } - return m_managementPort; } @@ -343,7 +342,8 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) } m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index()); m_configData.insert("vpnGateway", m_vpnGateway); - m_configData.insert("vpnServer", m_configData.value(amnezia::config_key::hostName).toString()); + m_configData.insert("vpnServer", + NetworkUtilities::getIPAddress(m_configData.value(amnezia::config_key::hostName).toString())); IpcClient::Interface()->enablePeerTraffic(m_configData); } } @@ -352,6 +352,8 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) // killSwitch toggle if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { + m_configData.insert("vpnServer", + NetworkUtilities::getIPAddress(m_configData.value(amnezia::config_key::hostName).toString())); IpcClient::Interface()->enableKillSwitch(m_configData, 0); } #endif diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 41e955b0..2dfbcc21 100755 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -17,6 +17,7 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent): m_routeGateway = NetworkUtilities::getGatewayAndIface(); m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr; m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr; + m_t2sProcess = IpcClient::InterfaceTun2Socks(); } XrayProtocol::~XrayProtocol() @@ -65,7 +66,7 @@ ErrorCode XrayProtocol::start() }); connect(&m_xrayProcess, QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { - qDebug().noquote() << "XrayProtocol finished, exitCode, exiStatus" << exitCode << exitStatus; + qDebug().noquote() << "XrayProtocol finished, exitCode, exitStatus" << exitCode << exitStatus; setConnectionState(Vpn::ConnectionState::Disconnected); if (exitStatus != QProcess::NormalExit) { emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); @@ -91,116 +92,80 @@ ErrorCode XrayProtocol::start() ErrorCode XrayProtocol::startTun2Sock() { - if (!QFileInfo::exists(Utils::tun2socksPath())) { - setLastError(ErrorCode::Tun2SockExecutableMissing); - return lastError(); - } - - m_t2sProcess = IpcClient::CreatePrivilegedProcess(); - - if (!m_t2sProcess) { - setLastError(ErrorCode::AmneziaServiceConnectionFailed); - return ErrorCode::AmneziaServiceConnectionFailed; - } - - m_t2sProcess->waitForSource(1000); - if (!m_t2sProcess->isInitialized()) { - qWarning() << "IpcProcess replica is not connected!"; - setLastError(ErrorCode::AmneziaServiceConnectionFailed); - return ErrorCode::AmneziaServiceConnectionFailed; - } - - QString XrayConStr = "socks5://127.0.0.1:" + QString::number(m_localPort); - - m_t2sProcess->setProgram(PermittedProcess::Tun2Socks); -#ifdef Q_OS_WIN - m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress))); - QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up", - QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255").arg(amnezia::protocols::xray::defaultLocalAddr)}); -#endif -#ifdef Q_OS_LINUX - QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr}); -#endif -#ifdef Q_OS_MAC - QStringList arguments({"-device", "utun22", "-proxy", XrayConStr}); -#endif - m_t2sProcess->setArguments(arguments); - - qDebug() << arguments.join(" "); - connect(m_t2sProcess.data(), &PrivilegedProcess::errorOccurred, - [&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; }); - - connect(m_t2sProcess.data(), &PrivilegedProcess::stateChanged, - [&](QProcess::ProcessState newState) { - qDebug() << "PrivilegedProcess stateChanged" << newState; - if (newState == QProcess::Running) - { - setConnectionState(Vpn::ConnectionState::Connecting); - QList dnsAddr; - dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns1).toString())); - dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns2).toString())); - -#ifdef Q_OS_MACOS - QThread::msleep(5000); - IpcClient::Interface()->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr); - IpcClient::Interface()->updateResolvers("utun22", dnsAddr); -#endif -#ifdef Q_OS_WINDOWS - QThread::msleep(7000); -#endif -#ifdef Q_OS_LINUX - QThread::msleep(1000); - IpcClient::Interface()->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); - IpcClient::Interface()->updateResolvers("tun2", dnsAddr); -#endif -#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) - // killSwitch toggle - if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { - IpcClient::Interface()->enableKillSwitch(m_configData, 0); - } -#endif - if (m_routeMode == 0) { - IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "0.0.0.0/1"); - IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "128.0.0.0/1"); - IpcClient::Interface()->routeAddList(m_routeGateway, QStringList() << m_remoteAddress); - } - IpcClient::Interface()->StopRoutingIpv6(); -#ifdef Q_OS_WIN - IpcClient::Interface()->updateResolvers("tun2", dnsAddr); - QList netInterfaces = QNetworkInterface::allInterfaces(); - for (int i = 0; i < netInterfaces.size(); i++) { - for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++) - { - // killSwitch toggle - if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { - if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { - IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index()); - } - m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index()); - m_configData.insert("vpnGateway", m_vpnGateway); - m_configData.insert("vpnServer", m_remoteAddress); - IpcClient::Interface()->enablePeerTraffic(m_configData); - } - } - } -#endif - setConnectionState(Vpn::ConnectionState::Connected); - } - }); - - -#if !defined(Q_OS_MACOS) - connect(m_t2sProcess.data(), &PrivilegedProcess::finished, this, - [&]() { - setConnectionState(Vpn::ConnectionState::Disconnected); - IpcClient::Interface()->deleteTun("tun2"); - IpcClient::Interface()->StartRoutingIpv6(); - IpcClient::Interface()->clearSavedRoutes(); - }); -#endif - m_t2sProcess->start(); +#ifdef Q_OS_WIN + m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress))); +#endif + + connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this, + [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); + + connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, + [&](int vpnState) { + qDebug() << "PrivilegedProcess setConnectionState " << vpnState; + if (vpnState == Vpn::ConnectionState::Connected) + { + setConnectionState(Vpn::ConnectionState::Connecting); + QList dnsAddr; + dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns1).toString())); + dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns2).toString())); +#ifdef Q_OS_WIN + QThread::msleep(8000); +#endif +#ifdef Q_OS_MACOS + QThread::msleep(5000); + IpcClient::Interface()->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr); + IpcClient::Interface()->updateResolvers("utun22", dnsAddr); +#endif +#ifdef Q_OS_LINUX + QThread::msleep(1000); + IpcClient::Interface()->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); + IpcClient::Interface()->updateResolvers("tun2", dnsAddr); +#endif +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + // killSwitch toggle + if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { + m_configData.insert("vpnServer", m_remoteAddress); + IpcClient::Interface()->enableKillSwitch(m_configData, 0); + } +#endif + if (m_routeMode == 0) { + IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "0.0.0.0/1"); + IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "128.0.0.0/1"); + IpcClient::Interface()->routeAddList(m_routeGateway, QStringList() << m_remoteAddress); + } + IpcClient::Interface()->StopRoutingIpv6(); +#ifdef Q_OS_WIN + IpcClient::Interface()->updateResolvers("tun2", dnsAddr); + QList netInterfaces = QNetworkInterface::allInterfaces(); + for (int i = 0; i < netInterfaces.size(); i++) { + for (int j = 0; j < netInterfaces.at(i).addressEntries().size(); j++) + { + // killSwitch toggle + if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { + if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { + IpcClient::Interface()->enableKillSwitch(QJsonObject(), netInterfaces.at(i).index()); + } + m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index()); + m_configData.insert("vpnGateway", m_vpnGateway); + m_configData.insert("vpnServer", m_remoteAddress); + IpcClient::Interface()->enablePeerTraffic(m_configData); + } + } + } +#endif + setConnectionState(Vpn::ConnectionState::Connected); + } +#if !defined(Q_OS_MACOS) + if (vpnState == Vpn::ConnectionState::Disconnected) { + setConnectionState(Vpn::ConnectionState::Disconnected); + IpcClient::Interface()->deleteTun("tun2"); + IpcClient::Interface()->StartRoutingIpv6(); + IpcClient::Interface()->clearSavedRoutes(); + } +#endif + }); return ErrorCode::NoError; } @@ -214,7 +179,7 @@ void XrayProtocol::stop() qDebug() << "XrayProtocol::stop()"; m_xrayProcess.terminate(); if (m_t2sProcess) { - m_t2sProcess->close(); + m_t2sProcess->stop(); } #ifdef Q_OS_WIN diff --git a/client/protocols/xrayprotocol.h b/client/protocols/xrayprotocol.h index c1d62897..ee632333 100644 --- a/client/protocols/xrayprotocol.h +++ b/client/protocols/xrayprotocol.h @@ -34,9 +34,10 @@ private: QString m_secondaryDNS; #ifndef Q_OS_IOS QProcess m_xrayProcess; - QSharedPointer m_t2sProcess; + QSharedPointer m_t2sProcess; #endif QTemporaryFile m_xrayCfgFile; + }; #endif // XRAYPROTOCOL_H diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 5baf9c33..c0f031fe 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -7,7 +7,6 @@ class IpcInterface { SLOT( int createPrivilegedProcess() ); // return local pid - //SIGNAL(sendMessage(const QByteArray &message)); // Route functions SLOT( int routeAddList(const QString &gw, const QStringList &ips) ); diff --git a/ipc/ipc_process_interface.rep b/ipc/ipc_process_interface.rep index ba42332c..6b3bb654 100644 --- a/ipc/ipc_process_interface.rep +++ b/ipc/ipc_process_interface.rep @@ -3,7 +3,6 @@ class IpcProcessInterface { - //SLOT( start(const QString &program, const QStringList &args) ); SLOT( start() ); SLOT( close() ); diff --git a/ipc/ipc_process_tun2socks.rep b/ipc/ipc_process_tun2socks.rep new file mode 100644 index 00000000..e355035e --- /dev/null +++ b/ipc/ipc_process_tun2socks.rep @@ -0,0 +1,11 @@ +#include +#include + +class IpcProcessTun2Socks +{ + SLOT( start() ); + SLOT( stop() ); + + SIGNAL( setConnectionState(int state) ); + SIGNAL( stateChanged(QProcess::ProcessState newState) ); +}; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index cee2c398..bb8a4182 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -8,6 +8,7 @@ #include "logger.h" #include "router.h" +#include "../core/networkUtilities.h" #include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN #include "../client/platforms/windows/daemon/windowsdaemon.h" @@ -209,7 +210,7 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd if (splitTunnelType == 0) { blockAll = true; allowNets = true; - allownets.append(configStr.value(amnezia::config_key::hostName).toString()); + allownets.append(configStr.value("vpnServer").toString()); } else if (splitTunnelType == 1) { blockNets = true; for (auto v : splitTunnelSites) { @@ -218,7 +219,7 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd } else if (splitTunnelType == 2) { blockAll = true; allowNets = true; - allownets.append(configStr.value(amnezia::config_key::hostName).toString()); + allownets.append(configStr.value("vpnServer").toString()); for (auto v : splitTunnelSites) { allownets.append(v.toString()); } @@ -329,7 +330,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) } } - config.m_excludedAddresses.append(configStr.value(amnezia::config_key::hostName).toString()); + config.m_excludedAddresses.append(configStr.value("vpnServer").toString()); if (splitTunnelType == 2) { for (auto v : splitTunnelSites) { QString ipRange = v.toString(); @@ -351,7 +352,6 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) WindowsDaemon::instance()->prepareActivation(config, inetAdapterIndex); WindowsDaemon::instance()->activateSplitTunnel(config, vpnAdapterIndex); - return true; #endif return true; } diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 74f88d0f..9810046b 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -9,8 +9,10 @@ #include "ipc.h" #include "ipcserverprocess.h" +#include "ipctun2socksprocess.h" #include "rep_ipc_interface_source.h" +#include "rep_ipc_process_tun2socks_source.h" class IpcServer : public IpcInterfaceSource { @@ -44,10 +46,12 @@ private: ProcessDescriptor (QObject *parent = nullptr) { serverNode = QSharedPointer(new QRemoteObjectHost(parent)); ipcProcess = QSharedPointer(new IpcServerProcess(parent)); + tun2socksProcess = QSharedPointer(new IpcProcessTun2Socks(parent)); localServer = QSharedPointer(new QLocalServer(parent)); } QSharedPointer ipcProcess; + QSharedPointer tun2socksProcess; QSharedPointer serverNode; QSharedPointer localServer; }; diff --git a/ipc/ipctun2socksprocess.cpp b/ipc/ipctun2socksprocess.cpp new file mode 100644 index 00000000..ffcb1bcd --- /dev/null +++ b/ipc/ipctun2socksprocess.cpp @@ -0,0 +1,74 @@ +#include "ipctun2socksprocess.h" +#include "ipc.h" +#include +#include + +#include "../protocols/protocols_defs.h" + +#ifndef Q_OS_IOS + +IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) : + IpcProcessTun2SocksSource(parent), + m_t2sProcess(QSharedPointer(new QProcess())) +{ + connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged); + qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()"; + +} + +IpcProcessTun2Socks::~IpcProcessTun2Socks() +{ + qDebug() << "IpcProcessTun2Socks::~IpcProcessTun2Socks()"; +} + +void IpcProcessTun2Socks::start() +{ + qDebug() << "IpcProcessTun2Socks::start()"; + m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast(amnezia::PermittedProcess::Tun2Socks))); + QString XrayConStr = "socks5://127.0.0.1:10808"; + +#ifdef Q_OS_WIN + QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up", + QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255") + .arg(amnezia::protocols::xray::defaultLocalAddr)}); +#endif +#ifdef Q_OS_LINUX + QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr}); +#endif +#ifdef Q_OS_MAC + QStringList arguments({"-device", "utun22", "-proxy", XrayConStr}); +#endif + + m_t2sProcess->setArguments(arguments); + + Utils::killProcessByName(m_t2sProcess->program()); + m_t2sProcess->start(); + + connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() { + QString line = m_t2sProcess.data()->readAllStandardOutput(); + if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) { + emit setConnectionState(Vpn::ConnectionState::Connected); + } + }); + + connect(m_t2sProcess.data(), QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus; + emit setConnectionState(Vpn::ConnectionState::Disconnected); + if (exitStatus != QProcess::NormalExit){ + stop(); + } + if (exitCode !=0 ){ + stop(); + } + }); + + m_t2sProcess->start(); + m_t2sProcess->waitForStarted(); +} + +void IpcProcessTun2Socks::stop() +{ + qDebug() << "IpcProcessTun2Socks::stop()"; + m_t2sProcess->close(); +} +#endif diff --git a/ipc/ipctun2socksprocess.h b/ipc/ipctun2socksprocess.h new file mode 100644 index 00000000..8ce9be1a --- /dev/null +++ b/ipc/ipctun2socksprocess.h @@ -0,0 +1,52 @@ +#ifndef IPCTUN2SOCKSPROCESS_H +#define IPCTUN2SOCKSPROCESS_H + +#include + +#ifndef Q_OS_IOS +#include "rep_ipc_process_tun2socks_source.h" + +namespace Vpn +{ +Q_NAMESPACE + enum ConnectionState { + Unknown, + Disconnected, + Preparing, + Connecting, + Connected, + Disconnecting, + Reconnecting, + Error + }; +Q_ENUM_NS(ConnectionState) +} + + +class IpcProcessTun2Socks : public IpcProcessTun2SocksSource +{ + Q_OBJECT +public: + explicit IpcProcessTun2Socks(QObject *parent = nullptr); + virtual ~IpcProcessTun2Socks(); + + void start() override; + void stop() override; + +signals: + +private: + QSharedPointer m_t2sProcess; +}; + +#else +class IpcProcessTun2Socks : public QObject +{ + Q_OBJECT + +public: + explicit IpcProcessTun2Socks(QObject *parent = nullptr); +}; +#endif + +#endif // IPCTUN2SOCKSPROCESS_H diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index c41e57f5..0f101087 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -18,6 +18,7 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h + ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.h ${CMAKE_CURRENT_LIST_DIR}/localserver.h ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h ${CMAKE_CURRENT_LIST_DIR}/router.h @@ -30,6 +31,7 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.cpp ${CMAKE_CURRENT_LIST_DIR}/localserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp @@ -279,6 +281,7 @@ endif() qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep) qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep) +qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_tun2socks.rep) # copy deploy artifacts required to run the application to the debug build folder if(WIN32) diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 3e1b0954..8a5079cb 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -37,6 +37,7 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), if (!m_isRemotingEnabled) { m_isRemotingEnabled = true; m_serverNode.enableRemoting(&m_ipcServer); + m_serverNode.enableRemoting(&m_tun2socks); } }); diff --git a/service/server/localserver.h b/service/server/localserver.h index 4a6648a5..3c565d3b 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -38,6 +38,7 @@ public: ~LocalServer(); QSharedPointer m_server; IpcServer m_ipcServer; + IpcProcessTun2Socks m_tun2socks; QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; #ifdef Q_OS_LINUX From 504862c2b8a31183af5b170e1d966117450482a3 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 20 Sep 2024 15:36:20 +0400 Subject: [PATCH 32/90] bugfix: fixed drawer size to pageHome on first startup --- client/ui/qml/main2.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index a5a47e2c..fb99559f 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -80,7 +80,8 @@ Window { } PageStart { - anchors.fill: parent + width: root.width + height: root.height } Item { From c681611102c0a371c694be79fe25e18694f006a7 Mon Sep 17 00:00:00 2001 From: pokamest Date: Fri, 20 Sep 2024 13:08:28 +0100 Subject: [PATCH 33/90] Bump version to 4.8.1.0 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b84c25f6..79b0c18c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.0.5 +project(${PROJECT} VERSION 4.8.1.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 61) +set(APP_ANDROID_VERSION_CODE 62) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 268adfb0a17465f4b02872d592432e9e82076b75 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sun, 22 Sep 2024 23:05:07 +0500 Subject: [PATCH 34/90] bugfix: fixed parameter handling for native wg obfuscation --- client/platforms/ios/ios_controller.mm | 14 +++++++++++ client/protocols/protocols_defs.h | 1 + client/ui/controllers/importController.cpp | 28 ++++++++++++---------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 6abae584..85fb50b7 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -499,6 +499,20 @@ bool IosController::setupWireGuard() wgConfig.insert(config_key::persistent_keep_alive, "25"); } + if (config.contains(config_key::isObfuscationEnabled) && config.value(config_key::isObfuscationEnabled).toBool()) { + wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]); + wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]); + wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]); + wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]); + + wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]); + wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]); + + wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]); + wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]); + wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]); + } + QJsonDocument wgConfigDoc(wgConfig); QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact)); diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index 56be0d7d..865edae4 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -65,6 +65,7 @@ namespace amnezia constexpr char last_config[] = "last_config"; constexpr char isThirdPartyConfig[] = "isThirdPartyConfig"; + constexpr char isObfuscationEnabled[] = "isObfuscationEnabled"; constexpr char junkPacketCount[] = "Jc"; constexpr char junkPacketMinSize[] = "Jmin"; diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 32170fb6..261551ea 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -242,24 +242,26 @@ void ImportController::processNativeWireGuardConfig() auto containers = m_config.value(config_key::containers).toArray(); if (!containers.isEmpty()) { auto container = containers.at(0).toObject(); - auto containerConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject(); - auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object(); + auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject(); + auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5)); QString junkPacketMinSize = QString::number(10); QString junkPacketMaxSize = QString::number(50); - protocolConfig[config_key::junkPacketCount] = junkPacketCount; - protocolConfig[config_key::junkPacketMinSize] = junkPacketMinSize; - protocolConfig[config_key::junkPacketMaxSize] = junkPacketMaxSize; - protocolConfig[config_key::initPacketJunkSize] = "0"; - protocolConfig[config_key::responsePacketJunkSize] = "0"; - protocolConfig[config_key::initPacketMagicHeader] = "1"; - protocolConfig[config_key::responsePacketMagicHeader] = "2"; - protocolConfig[config_key::underloadPacketMagicHeader] = "3"; - protocolConfig[config_key::transportPacketMagicHeader] = "4"; + clientProtocolConfig[config_key::junkPacketCount] = junkPacketCount; + clientProtocolConfig[config_key::junkPacketMinSize] = junkPacketMinSize; + clientProtocolConfig[config_key::junkPacketMaxSize] = junkPacketMaxSize; + clientProtocolConfig[config_key::initPacketJunkSize] = "0"; + clientProtocolConfig[config_key::responsePacketJunkSize] = "0"; + clientProtocolConfig[config_key::initPacketMagicHeader] = "1"; + clientProtocolConfig[config_key::responsePacketMagicHeader] = "2"; + clientProtocolConfig[config_key::underloadPacketMagicHeader] = "3"; + clientProtocolConfig[config_key::transportPacketMagicHeader] = "4"; - containerConfig[config_key::last_config] = QString(QJsonDocument(protocolConfig).toJson()); - container["wireguard"] = containerConfig; + clientProtocolConfig[config_key::isObfuscationEnabled] = true; + + serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson()); + container["wireguard"] = serverProtocolConfig; containers.replace(0, container); m_config[config_key::containers] = containers; } From 1f08d78b43f6e3321e6ccb0c9bd97863ac0f04e4 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Sun, 22 Sep 2024 22:52:59 +0100 Subject: [PATCH 35/90] wip --- client/ui/controllers/installController.cpp | 27 +--- client/utilities.cpp | 139 ++++++++++++++++---- client/utilities.h | 6 +- 3 files changed, 120 insertions(+), 52 deletions(-) mode change 100644 => 100755 client/ui/controllers/installController.cpp mode change 100644 => 100755 client/utilities.cpp mode change 100644 => 100755 client/utilities.h diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp old mode 100644 new mode 100755 index c6f17057..5a64d092 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -33,31 +33,6 @@ namespace constexpr char apiConfig[] = "api_config"; } - -#ifdef Q_OS_WINDOWS - QString getNextDriverLetter() - { - QProcess drivesProc; - drivesProc.start("wmic logicaldisk get caption"); - drivesProc.waitForFinished(); - QString drives = drivesProc.readAll(); - qDebug() << drives; - - QString letters = "CFGHIJKLMNOPQRSTUVWXYZ"; - QString letter; - for (int i = letters.size() - 1; i > 0; i--) { - letter = letters.at(i); - if (!drives.contains(letter + ":")) - break; - } - if (letter == "C:") { - // set err info - qDebug() << "Can't find free drive letter"; - return ""; - } - return letter; - } -#endif } InstallController::InstallController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, @@ -667,7 +642,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw QString hostname = serverCredentials.hostName; #ifdef Q_OS_WINDOWS - mountPath = getNextDriverLetter() + ":"; + mountPath = Utils::getNextDriverLetter() + ":"; // QString cmd = QString("net use \\\\sshfs\\%1@x.x.x.x!%2 /USER:%1 %3") // .arg(labelTftpUserNameText()) // .arg(labelTftpPortText()) diff --git a/client/utilities.cpp b/client/utilities.cpp old mode 100644 new mode 100755 index 4047365f..bcae2ed5 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -10,7 +10,62 @@ #include #include "utilities.h" -#include "version.h" + +#ifdef Q_OS_WINDOWS +QString printErrorMessage(DWORD errorCode) { + LPVOID lpMsgBuf; + + DWORD dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + + DWORD dwLanguageId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + + FormatMessageW( + dwFlags, + NULL, + errorCode, + dwLanguageId, + (LPWSTR)&lpMsgBuf, + 0, + NULL + ); + + QString errorMsg = QString::fromWCharArray((LPCWSTR)lpMsgBuf); + LocalFree(lpMsgBuf); + return errorMsg.trimmed(); +} + +QString Utils::getNextDriverLetter() +{ + DWORD drivesBitmask = GetLogicalDrives(); + if (drivesBitmask == 0) { + DWORD error = GetLastError(); + qDebug() << "GetLogicalDrives failed. Error code:" << error; + return ""; + } + + QString letters = "FGHIJKLMNOPQRSTUVWXYZ"; + QString availableLetter; + + for (int i = letters.size() - 1; i >= 0; --i) { + QChar letterChar = letters.at(i); + int driveIndex = letterChar.toLatin1() - 'A'; + + if ((drivesBitmask & (1 << driveIndex)) == 0) { + availableLetter = letterChar; + break; + } + } + + if (availableLetter.isEmpty()) { + qDebug() << "Can't find free drive letter"; + return ""; + } + + return availableLetter; +} +#endif QString Utils::getRandomString(int len) { @@ -109,30 +164,34 @@ QString Utils::usrExecutable(const QString &baseName) bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) { #ifdef Q_OS_WIN - QProcess process; - process.setReadChannel(QProcess::StandardOutput); - process.setProcessChannelMode(QProcess::MergedChannels); - process.start("wmic.exe", - QStringList() << "/OUTPUT:STDOUT" - << "PROCESS" - << "get" - << "Caption"); - process.waitForStarted(); - process.waitForFinished(); - QString processData(process.readAll()); - QStringList processList = processData.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); - foreach (const QString &rawLine, processList) { - const QString line = rawLine.simplified(); - if (line.isEmpty()) { - continue; - } + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) { + qWarning() << "Utils::processIsRunning error CreateToolhelp32Snapshot"; + return false; + } - if (line == fileName) { + PROCESSENTRY32W pe32; + pe32.dwSize = sizeof(PROCESSENTRY32W); + + if (!Process32FirstW(hSnapshot, &pe32)) { + CloseHandle(hSnapshot); + qWarning() << "Utils::processIsRunning error Process32FirstW"; + return false; + } + + do { + QString exeFile = QString::fromWCharArray(pe32.szExeFile); + + if (exeFile.compare(fileName, Qt::CaseInsensitive) == 0) { + CloseHandle(hSnapshot); return true; } - } + } while (Process32NextW(hSnapshot, &pe32)); + + CloseHandle(hSnapshot); return false; -#elif defined(Q_OS_IOS) + +#elif defined(Q_OS_IOS) || defined(Q_OS_ANDROID) return false; #else QProcess process; @@ -150,12 +209,44 @@ bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) #endif } -void Utils::killProcessByName(const QString &name) +bool Utils::killProcessByName(const QString &name) { qDebug().noquote() << "Kill process" << name; #ifdef Q_OS_WIN - QProcess::execute("taskkill", QStringList() << "/IM" << name << "/F"); -#elif defined Q_OS_IOS + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) + return false; + + PROCESSENTRY32W pe32; + pe32.dwSize = sizeof(PROCESSENTRY32W); + + bool success = false; + + if (Process32FirstW(hSnapshot, &pe32)) { + do { + QString exeFile = QString::fromWCharArray(pe32.szExeFile); + + if (exeFile.compare(name, Qt::CaseInsensitive) == 0) { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID); + if (hProcess != NULL) { + if (TerminateProcess(hProcess, 0)) { + success = true; + } else { + DWORD error = GetLastError(); + qCritical() << "Can't terminate process" << exeFile << "(PID:" << pe32.th32ProcessID << "). Error:" << printErrorMessage(error); + } + CloseHandle(hProcess); + } else { + DWORD error = GetLastError(); + qCritical() << "Can't open process for termination" << exeFile << "(PID:" << pe32.th32ProcessID << "). Error:" << printErrorMessage(error); + } + } + } while (Process32NextW(hSnapshot, &pe32)); + } + + CloseHandle(hSnapshot); + return success; +#elif defined Q_OS_IOS || defined(Q_OS_ANDROID) return; #else QProcess::execute(QString("pkill %1").arg(name)); diff --git a/client/utilities.h b/client/utilities.h old mode 100644 new mode 100755 index 9bf8c82a..b3e3b50b --- a/client/utilities.h +++ b/client/utilities.h @@ -7,7 +7,8 @@ #include #ifdef Q_OS_WIN - #include "Windows.h" +#include +#include #endif class Utils : public QObject @@ -27,7 +28,7 @@ public: static bool initializePath(const QString &path); static bool processIsRunning(const QString &fileName, const bool fullFlag = false); - static void killProcessByName(const QString &name); + static bool killProcessByName(const QString &name); static QString openVpnExecPath(); static QString wireguardExecPath(); @@ -36,6 +37,7 @@ public: #ifdef Q_OS_WIN static bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent); + static QString getNextDriverLetter(); #endif }; From 3aa8a46f6e36ea62a0d3f010ed7a05d72aa55188 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Mon, 23 Sep 2024 01:19:46 +0300 Subject: [PATCH 36/90] wip --- client/ui/controllers/connectionController.cpp | 14 +++++++------- client/utilities.cpp | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index c7f95000..db8beed1 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -34,13 +34,13 @@ ConnectionController::ConnectionController(const QSharedPointer &s void ConnectionController::openConnection() { -// #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) -// if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) -// { -// emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); -// return; -// } -// #endif +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) + { + emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); + return; + } +#endif int serverIndex = m_serversModel->getDefaultServerIndex(); QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex); diff --git a/client/utilities.cpp b/client/utilities.cpp index bcae2ed5..ed91f5fc 100755 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -247,7 +247,7 @@ bool Utils::killProcessByName(const QString &name) CloseHandle(hSnapshot); return success; #elif defined Q_OS_IOS || defined(Q_OS_ANDROID) - return; + return false; #else QProcess::execute(QString("pkill %1").arg(name)); #endif From 1542adba82a0f7f4f48e381406799c86c90d7bec Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Mon, 23 Sep 2024 00:44:25 +0100 Subject: [PATCH 37/90] Switched to secure PRNG & some pw len increased --- client/ui/controllers/installController.cpp | 4 ++-- client/utilities.cpp | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index c6f17057..0126a5b0 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -135,10 +135,10 @@ void InstallController::install(DockerContainer container, int port, TransportPr containerConfig[config_key::transportPacketMagicHeader] = transportPacketMagicHeader; } else if (container == DockerContainer::Sftp) { containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName); - containerConfig.insert(config_key::password, Utils::getRandomString(10)); + containerConfig.insert(config_key::password, Utils::getRandomString(16)); } else if (container == DockerContainer::Socks5Proxy) { containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName); - containerConfig.insert(config_key::password, Utils::getRandomString(10)); + containerConfig.insert(config_key::password, Utils::getRandomString(16)); } config.insert(config_key::container, ContainerProps::containerToString(container)); diff --git a/client/utilities.cpp b/client/utilities.cpp index 4047365f..cb50235a 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -14,14 +14,13 @@ QString Utils::getRandomString(int len) { - const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); - + const QString possibleCharacters = QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); QString randomString; + for (int i = 0; i < len; ++i) { - quint32 index = QRandomGenerator::global()->generate() % possibleCharacters.length(); - QChar nextChar = possibleCharacters.at(index); - randomString.append(nextChar); + randomString.append(possibleCharacters.at(QRandomGenerator::system()->bounded(possibleCharacters.length()))); } + return randomString; } From 425acc5f8b220cf2912f25f4b0b73075dcb93bb1 Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 23 Sep 2024 17:53:56 +0300 Subject: [PATCH 38/90] Add support for obfuscated WG on Android --- client/android/awg/src/main/kotlin/Awg.kt | 17 +-- .../android/awg/src/main/kotlin/AwgConfig.kt | 108 ------------------ .../vpn/protocol/wireguard/Wireguard.kt | 17 +++ .../vpn/protocol/wireguard/WireguardConfig.kt | 73 +++++++++++- 4 files changed, 93 insertions(+), 122 deletions(-) delete mode 100644 client/android/awg/src/main/kotlin/AwgConfig.kt diff --git a/client/android/awg/src/main/kotlin/Awg.kt b/client/android/awg/src/main/kotlin/Awg.kt index fbd1cce0..c147ae03 100644 --- a/client/android/awg/src/main/kotlin/Awg.kt +++ b/client/android/awg/src/main/kotlin/Awg.kt @@ -1,28 +1,21 @@ package org.amnezia.vpn.protocol.awg import org.amnezia.vpn.protocol.wireguard.Wireguard -import org.amnezia.vpn.util.optStringOrNull +import org.amnezia.vpn.protocol.wireguard.WireguardConfig import org.json.JSONObject class Awg : Wireguard() { override val ifName: String = "awg0" - override fun parseConfig(config: JSONObject): AwgConfig { + override fun parseConfig(config: JSONObject): WireguardConfig { val configData = config.getJSONObject("awg_config_data") - return AwgConfig.build { + return WireguardConfig.build { + setUseProtocolExtension(true) + configExtensionParameters(configData) configWireguard(config, configData) configSplitTunneling(config) configAppSplitTunneling(config) - configData.optStringOrNull("Jc")?.let { setJc(it.toInt()) } - configData.optStringOrNull("Jmin")?.let { setJmin(it.toInt()) } - configData.optStringOrNull("Jmax")?.let { setJmax(it.toInt()) } - configData.optStringOrNull("S1")?.let { setS1(it.toInt()) } - configData.optStringOrNull("S2")?.let { setS2(it.toInt()) } - configData.optStringOrNull("H1")?.let { setH1(it.toLong()) } - configData.optStringOrNull("H2")?.let { setH2(it.toLong()) } - configData.optStringOrNull("H3")?.let { setH3(it.toLong()) } - configData.optStringOrNull("H4")?.let { setH4(it.toLong()) } } } } diff --git a/client/android/awg/src/main/kotlin/AwgConfig.kt b/client/android/awg/src/main/kotlin/AwgConfig.kt deleted file mode 100644 index 014c6e0a..00000000 --- a/client/android/awg/src/main/kotlin/AwgConfig.kt +++ /dev/null @@ -1,108 +0,0 @@ -package org.amnezia.vpn.protocol.awg - -import org.amnezia.vpn.protocol.BadConfigException -import org.amnezia.vpn.protocol.wireguard.WireguardConfig - -class AwgConfig private constructor( - wireguardConfigBuilder: WireguardConfig.Builder, - val jc: Int, - val jmin: Int, - val jmax: Int, - val s1: Int, - val s2: Int, - val h1: Long, - val h2: Long, - val h3: Long, - val h4: Long -) : WireguardConfig(wireguardConfigBuilder) { - - private constructor(builder: Builder) : this( - builder, - builder.jc, - builder.jmin, - builder.jmax, - builder.s1, - builder.s2, - builder.h1, - builder.h2, - builder.h3, - builder.h4 - ) - - override fun appendDeviceLine(sb: StringBuilder) = with(sb) { - super.appendDeviceLine(this) - appendLine("jc=$jc") - appendLine("jmin=$jmin") - appendLine("jmax=$jmax") - appendLine("s1=$s1") - appendLine("s2=$s2") - appendLine("h1=$h1") - appendLine("h2=$h2") - appendLine("h3=$h3") - appendLine("h4=$h4") - } - - class Builder : WireguardConfig.Builder() { - - private var _jc: Int? = null - internal var jc: Int - get() = _jc ?: throw BadConfigException("AWG: parameter jc is undefined") - private set(value) { _jc = value } - - private var _jmin: Int? = null - internal var jmin: Int - get() = _jmin ?: throw BadConfigException("AWG: parameter jmin is undefined") - private set(value) { _jmin = value } - - private var _jmax: Int? = null - internal var jmax: Int - get() = _jmax ?: throw BadConfigException("AWG: parameter jmax is undefined") - private set(value) { _jmax = value } - - private var _s1: Int? = null - internal var s1: Int - get() = _s1 ?: throw BadConfigException("AWG: parameter s1 is undefined") - private set(value) { _s1 = value } - - private var _s2: Int? = null - internal var s2: Int - get() = _s2 ?: throw BadConfigException("AWG: parameter s2 is undefined") - private set(value) { _s2 = value } - - private var _h1: Long? = null - internal var h1: Long - get() = _h1 ?: throw BadConfigException("AWG: parameter h1 is undefined") - private set(value) { _h1 = value } - - private var _h2: Long? = null - internal var h2: Long - get() = _h2 ?: throw BadConfigException("AWG: parameter h2 is undefined") - private set(value) { _h2 = value } - - private var _h3: Long? = null - internal var h3: Long - get() = _h3 ?: throw BadConfigException("AWG: parameter h3 is undefined") - private set(value) { _h3 = value } - - private var _h4: Long? = null - internal var h4: Long - get() = _h4 ?: throw BadConfigException("AWG: parameter h4 is undefined") - private set(value) { _h4 = value } - - fun setJc(jc: Int) = apply { this.jc = jc } - fun setJmin(jmin: Int) = apply { this.jmin = jmin } - fun setJmax(jmax: Int) = apply { this.jmax = jmax } - fun setS1(s1: Int) = apply { this.s1 = s1 } - fun setS2(s2: Int) = apply { this.s2 = s2 } - fun setH1(h1: Long) = apply { this.h1 = h1 } - fun setH2(h2: Long) = apply { this.h2 = h2 } - fun setH3(h3: Long) = apply { this.h3 = h3 } - fun setH4(h4: Long) = apply { this.h4 = h4 } - - override fun build(): AwgConfig = configBuild().run { AwgConfig(this@Builder) } - } - - companion object { - inline fun build(block: Builder.() -> Unit): AwgConfig = Builder().apply(block).build() - } -} diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 31e7f9be..ac11374b 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -129,12 +129,29 @@ open class Wireguard : Protocol() { val port = configData.getInt("port") setEndpoint(InetEndpoint(host, port)) + if (configData.optBoolean("isObfuscationEnabled")) { + setUseProtocolExtension(true) + configExtensionParameters(configData) + } + configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) } 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()) } } + protected fun WireguardConfig.Builder.configExtensionParameters(configData: JSONObject) { + configData.optStringOrNull("Jc")?.let { setJc(it.toInt()) } + configData.optStringOrNull("Jmin")?.let { setJmin(it.toInt()) } + configData.optStringOrNull("Jmax")?.let { setJmax(it.toInt()) } + configData.optStringOrNull("S1")?.let { setS1(it.toInt()) } + configData.optStringOrNull("S2")?.let { setS2(it.toInt()) } + configData.optStringOrNull("H1")?.let { setH1(it.toLong()) } + configData.optStringOrNull("H2")?.let { setH2(it.toLong()) } + configData.optStringOrNull("H3")?.let { setH3(it.toLong()) } + configData.optStringOrNull("H4")?.let { setH4(it.toLong()) } + } + private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) { if (tunnelHandle != -1) { Log.w(TAG, "Tunnel already up") diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt index 09269f54..7ae3d43b 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt @@ -1,6 +1,7 @@ package org.amnezia.vpn.protocol.wireguard import android.util.Base64 +import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.ProtocolConfig import org.amnezia.vpn.util.net.InetEndpoint @@ -12,7 +13,17 @@ open class WireguardConfig protected constructor( val persistentKeepalive: Int, val publicKeyHex: String, val preSharedKeyHex: String?, - val privateKeyHex: String + val privateKeyHex: String, + val useProtocolExtension: Boolean, + val jc: Int?, + val jmin: Int?, + val jmax: Int?, + val s1: Int?, + val s2: Int?, + val h1: Long?, + val h2: Long?, + val h3: Long?, + val h4: Long? ) : ProtocolConfig(protocolConfigBuilder) { protected constructor(builder: Builder) : this( @@ -21,7 +32,17 @@ open class WireguardConfig protected constructor( builder.persistentKeepalive, builder.publicKeyHex, builder.preSharedKeyHex, - builder.privateKeyHex + builder.privateKeyHex, + builder.useProtocolExtension, + builder.jc, + builder.jmin, + builder.jmax, + builder.s1, + builder.s2, + builder.h1, + builder.h2, + builder.h3, + builder.h4 ) fun toWgUserspaceString(): String = with(StringBuilder()) { @@ -33,6 +54,30 @@ open class WireguardConfig protected constructor( open fun appendDeviceLine(sb: StringBuilder) = with(sb) { appendLine("private_key=$privateKeyHex") + if (useProtocolExtension) { + validateProtocolExtensionParameters() + appendLine("jc=$jc") + appendLine("jmin=$jmin") + appendLine("jmax=$jmax") + appendLine("s1=$s1") + appendLine("s2=$s2") + appendLine("h1=$h1") + appendLine("h2=$h2") + appendLine("h3=$h3") + appendLine("h4=$h4") + } + } + + private fun validateProtocolExtensionParameters() { + if (jc == null) throw BadConfigException("Parameter jc is undefined") + if (jmin == null) throw BadConfigException("Parameter jmin is undefined") + if (jmax == null) throw BadConfigException("Parameter jmax is undefined") + if (s1 == null) throw BadConfigException("Parameter s1 is undefined") + if (s2 == null) throw BadConfigException("Parameter s2 is undefined") + if (h1 == null) throw BadConfigException("Parameter h1 is undefined") + if (h2 == null) throw BadConfigException("Parameter h2 is undefined") + if (h3 == null) throw BadConfigException("Parameter h3 is undefined") + if (h4 == null) throw BadConfigException("Parameter h4 is undefined") } open fun appendPeerLine(sb: StringBuilder) = with(sb) { @@ -65,6 +110,18 @@ open class WireguardConfig protected constructor( override var mtu: Int = WIREGUARD_DEFAULT_MTU + internal var useProtocolExtension: Boolean = false + + internal var jc: Int? = null + internal var jmin: Int? = null + internal var jmax: Int? = null + internal var s1: Int? = null + internal var s2: Int? = null + internal var h1: Long? = null + internal var h2: Long? = null + internal var h3: Long? = null + internal var h4: Long? = null + fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint } fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive } @@ -75,6 +132,18 @@ open class WireguardConfig protected constructor( fun setPrivateKeyHex(privateKeyHex: String) = apply { this.privateKeyHex = privateKeyHex } + fun setUseProtocolExtension(useProtocolExtension: Boolean) = apply { this.useProtocolExtension = useProtocolExtension } + + fun setJc(jc: Int) = apply { this.jc = jc } + fun setJmin(jmin: Int) = apply { this.jmin = jmin } + fun setJmax(jmax: Int) = apply { this.jmax = jmax } + fun setS1(s1: Int) = apply { this.s1 = s1 } + fun setS2(s2: Int) = apply { this.s2 = s2 } + fun setH1(h1: Long) = apply { this.h1 = h1 } + fun setH2(h2: Long) = apply { this.h2 = h2 } + fun setH3(h3: Long) = apply { this.h3 = h3 } + fun setH4(h4: Long) = apply { this.h4 = h4 } + override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) } } From 02a98b9d68d25885d8f938e34c821e0e4133eb93 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 25 Sep 2024 11:22:16 +0500 Subject: [PATCH 39/90] replaced QSingleApplication with QLocalServer --- .../SingleApplication/singleapplication.cmake | 25 - .../SingleApplication/singleapplication.cpp | 274 ---------- .../3rd/SingleApplication/singleapplication.h | 154 ------ .../SingleApplication/singleapplication.pri | 15 - .../SingleApplication/singleapplication_p.cpp | 486 ------------------ .../SingleApplication/singleapplication_p.h | 104 ---- client/amnezia_application.cpp | 36 +- client/amnezia_application.h | 14 +- client/cmake/3rdparty.cmake | 4 - client/main.cpp | 23 +- 10 files changed, 41 insertions(+), 1094 deletions(-) delete mode 100644 client/3rd/SingleApplication/singleapplication.cmake delete mode 100644 client/3rd/SingleApplication/singleapplication.cpp delete mode 100644 client/3rd/SingleApplication/singleapplication.h delete mode 100644 client/3rd/SingleApplication/singleapplication.pri delete mode 100644 client/3rd/SingleApplication/singleapplication_p.cpp delete mode 100644 client/3rd/SingleApplication/singleapplication_p.h diff --git a/client/3rd/SingleApplication/singleapplication.cmake b/client/3rd/SingleApplication/singleapplication.cmake deleted file mode 100644 index 78abfa8a..00000000 --- a/client/3rd/SingleApplication/singleapplication.cmake +++ /dev/null @@ -1,25 +0,0 @@ -include_directories(${CMAKE_CURRENT_LIST_DIR}) - -find_package(Qt6 REQUIRED COMPONENTS - Core Network -) -set(LIBS ${LIBS} Qt6::Core Qt6::Network) - - -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/singleapplication.h - ${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.h -) - -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/singleapplication.cpp - ${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.cpp -) - -if(WIN32) - if(MSVC) - set(LIBS ${LIBS} Advapi32.lib) - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - set(LIBS ${LIBS} advapi32) - endif() -endif() diff --git a/client/3rd/SingleApplication/singleapplication.cpp b/client/3rd/SingleApplication/singleapplication.cpp deleted file mode 100644 index 7e153a00..00000000 --- a/client/3rd/SingleApplication/singleapplication.cpp +++ /dev/null @@ -1,274 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2020 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include -#include -#include - -#include "singleapplication.h" -#include "singleapplication_p.h" - -/** - * @brief Constructor. Checks and fires up LocalServer or closes the program - * if another instance already exists - * @param argc - * @param argv - * @param allowSecondary Whether to enable secondary instance support - * @param options Optional flags to toggle specific behaviour - * @param timeout Maximum time blocking functions are allowed during app load - */ -SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData ) - : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) -{ - Q_D( SingleApplication ); - -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - // On Android and iOS since the library is not supported fallback to - // standard QApplication behaviour by simply returning at this point. - qWarning() << "SingleApplication is not supported on Android and iOS systems."; - return; -#endif - - // Store the current mode of the program - d->options = options; - - // Add any unique user data - if ( ! userData.isEmpty() ) - d->addAppData( userData ); - - // Generating an application ID used for identifying the shared memory - // block and QLocalServer - d->genBlockServerName(); - - // To mitigate QSharedMemory issues with large amount of processes - // attempting to attach at the same time - SingleApplicationPrivate::randomSleep(); - -#ifdef Q_OS_UNIX - // By explicitly attaching it and then deleting it we make sure that the - // memory is deleted even after the process has crashed on Unix. - d->memory = new QSharedMemory( d->blockServerName ); - d->memory->attach(); - delete d->memory; -#endif - // Guarantee thread safe behaviour with a shared memory block. - d->memory = new QSharedMemory( d->blockServerName ); - - // Create a shared memory block - if( d->memory->create( sizeof( InstancesInfo ) )){ - // Initialize the shared memory block - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory block after create."; - abortSafely(); - } - d->initializeMemoryBlock(); - } else { - if( d->memory->error() == QSharedMemory::AlreadyExists ){ - // Attempt to attach to the memory segment - if( ! d->memory->attach() ){ - qCritical() << "SingleApplication: Unable to attach to shared memory block."; - abortSafely(); - } - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory block after attach."; - abortSafely(); - } - } else { - qCritical() << "SingleApplication: Unable to create block."; - abortSafely(); - } - } - - auto *inst = static_cast( d->memory->data() ); - QElapsedTimer time; - time.start(); - - // Make sure the shared memory block is initialised and in consistent state - while( true ){ - // If the shared memory block's checksum is valid continue - if( d->blockChecksum() == inst->checksum ) break; - - // If more than 5s have elapsed, assume the primary instance crashed and - // assume it's position - if( time.elapsed() > 5000 ){ - qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure."; - d->initializeMemoryBlock(); - } - - // Otherwise wait for a random period and try again. The random sleep here - // limits the probability of a collision between two racing apps and - // allows the app to initialise faster - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory for random wait."; - qDebug() << d->memory->errorString(); - } - SingleApplicationPrivate::randomSleep(); - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory after random wait."; - abortSafely(); - } - } - - if( inst->primary == false ){ - d->startPrimary(); - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory after primary start."; - qDebug() << d->memory->errorString(); - } - return; - } - - // Check if another instance can be started - if( allowSecondary ){ - d->startSecondary(); - if( d->options & Mode::SecondaryNotification ){ - d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); - } - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory after secondary start."; - qDebug() << d->memory->errorString(); - } - return; - } - - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; - qDebug() << d->memory->errorString(); - } - - d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); - - delete d; - - ::exit( EXIT_SUCCESS ); -} - -SingleApplication::~SingleApplication() -{ - Q_D( SingleApplication ); - delete d; -} - -/** - * Checks if the current application instance is primary. - * @return Returns true if the instance is primary, false otherwise. - */ -bool SingleApplication::isPrimary() const -{ - Q_D( const SingleApplication ); - return d->server != nullptr; -} - -/** - * Checks if the current application instance is secondary. - * @return Returns true if the instance is secondary, false otherwise. - */ -bool SingleApplication::isSecondary() const -{ - Q_D( const SingleApplication ); - return d->server == nullptr; -} - -/** - * Allows you to identify an instance by returning unique consecutive instance - * ids. It is reset when the first (primary) instance of your app starts and - * only incremented afterwards. - * @return Returns a unique instance id. - */ -quint32 SingleApplication::instanceId() const -{ - Q_D( const SingleApplication ); - return d->instanceNumber; -} - -/** - * Returns the OS PID (Process Identifier) of the process running the primary - * instance. Especially useful when SingleApplication is coupled with OS. - * specific APIs. - * @return Returns the primary instance PID. - */ -qint64 SingleApplication::primaryPid() const -{ - Q_D( const SingleApplication ); - return d->primaryPid(); -} - -/** - * Returns the username the primary instance is running as. - * @return Returns the username the primary instance is running as. - */ -QString SingleApplication::primaryUser() const -{ - Q_D( const SingleApplication ); - return d->primaryUser(); -} - -/** - * Returns the username the current instance is running as. - * @return Returns the username the current instance is running as. - */ -QString SingleApplication::currentUser() const -{ - return SingleApplicationPrivate::getUsername(); -} - -/** - * Sends message to the Primary Instance. - * @param message The message to send. - * @param timeout the maximum timeout in milliseconds for blocking functions. - * @return true if the message was sent successfuly, false otherwise. - */ -bool SingleApplication::sendMessage( const QByteArray &message, int timeout ) -{ - Q_D( SingleApplication ); - - // Nobody to connect to - if( isPrimary() ) return false; - - // Make sure the socket is connected - if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) ) - return false; - - d->socket->write( message ); - bool dataWritten = d->socket->waitForBytesWritten( timeout ); - d->socket->flush(); - return dataWritten; -} - -/** - * Cleans up the shared memory block and exits with a failure. - * This function halts program execution. - */ -void SingleApplication::abortSafely() -{ - Q_D( SingleApplication ); - - qCritical() << "SingleApplication: " << d->memory->error() << d->memory->errorString(); - delete d; - ::exit( EXIT_FAILURE ); -} - -QStringList SingleApplication::userData() const -{ - Q_D( const SingleApplication ); - return d->appData(); -} diff --git a/client/3rd/SingleApplication/singleapplication.h b/client/3rd/SingleApplication/singleapplication.h deleted file mode 100644 index 400c88ac..00000000 --- a/client/3rd/SingleApplication/singleapplication.h +++ /dev/null @@ -1,154 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2018 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#ifndef SINGLE_APPLICATION_H -#define SINGLE_APPLICATION_H - -#include -#include - -#ifndef QAPPLICATION_CLASS - #define QAPPLICATION_CLASS QApplication -#endif - -#include QT_STRINGIFY(QAPPLICATION_CLASS) - -class SingleApplicationPrivate; - -/** - * @brief The SingleApplication class handles multiple instances of the same - * Application - * @see QCoreApplication - */ -class SingleApplication : public QAPPLICATION_CLASS -{ - Q_OBJECT - - using app_t = QAPPLICATION_CLASS; - -public: - /** - * @brief Mode of operation of SingleApplication. - * Whether the block should be user-wide or system-wide and whether the - * primary instance should be notified when a secondary instance had been - * started. - * @note Operating system can restrict the shared memory blocks to the same - * user, in which case the User/System modes will have no effect and the - * block will be user wide. - * @enum - */ - enum Mode { - User = 1 << 0, - System = 1 << 1, - SecondaryNotification = 1 << 2, - ExcludeAppVersion = 1 << 3, - ExcludeAppPath = 1 << 4 - }; - Q_DECLARE_FLAGS(Options, Mode) - - /** - * @brief Intitializes a SingleApplication instance with argc command line - * arguments in argv - * @arg {int &} argc - Number of arguments in argv - * @arg {const char *[]} argv - Supplied command line arguments - * @arg {bool} allowSecondary - Whether to start the instance as secondary - * if there is already a primary instance. - * @arg {Mode} mode - Whether for the SingleApplication block to be applied - * User wide or System wide. - * @arg {int} timeout - Timeout to wait in milliseconds. - * @note argc and argv may be changed as Qt removes arguments that it - * recognizes - * @note Mode::SecondaryNotification only works if set on both the primary - * instance and the secondary instance. - * @note The timeout is just a hint for the maximum time of blocking - * operations. It does not guarantee that the SingleApplication - * initialisation will be completed in given time, though is a good hint. - * Usually 4*timeout would be the worst case (fail) scenario. - * @see See the corresponding QAPPLICATION_CLASS constructor for reference - */ - explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, const QString &userData = {} ); - ~SingleApplication() override; - - /** - * @brief Returns if the instance is the primary instance - * @returns {bool} - */ - bool isPrimary() const; - - /** - * @brief Returns if the instance is a secondary instance - * @returns {bool} - */ - bool isSecondary() const; - - /** - * @brief Returns a unique identifier for the current instance - * @returns {qint32} - */ - quint32 instanceId() const; - - /** - * @brief Returns the process ID (PID) of the primary instance - * @returns {qint64} - */ - qint64 primaryPid() const; - - /** - * @brief Returns the username of the user running the primary instance - * @returns {QString} - */ - QString primaryUser() const; - - /** - * @brief Returns the username of the current user - * @returns {QString} - */ - QString currentUser() const; - - /** - * @brief Sends a message to the primary instance. Returns true on success. - * @param {int} timeout - Timeout for connecting - * @returns {bool} - * @note sendMessage() will return false if invoked from the primary - * instance. - */ - bool sendMessage( const QByteArray &message, int timeout = 100 ); - - /** - * @brief Get the set user data. - * @returns {QStringList} - */ - QStringList userData() const; - -Q_SIGNALS: - void instanceStarted(); - void receivedMessage( quint32 instanceId, QByteArray message ); - -private: - SingleApplicationPrivate *d_ptr; - Q_DECLARE_PRIVATE(SingleApplication) - void abortSafely(); -}; - -Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) - -#endif // SINGLE_APPLICATION_H diff --git a/client/3rd/SingleApplication/singleapplication.pri b/client/3rd/SingleApplication/singleapplication.pri deleted file mode 100644 index 80283fc4..00000000 --- a/client/3rd/SingleApplication/singleapplication.pri +++ /dev/null @@ -1,15 +0,0 @@ -QT += core network -CONFIG += c++11 - -HEADERS += \ - $$PWD/singleapplication.h \ - $$PWD/singleapplication_p.h -SOURCES += $$PWD/singleapplication.cpp \ - $$PWD/singleapplication_p.cpp - -INCLUDEPATH += $$PWD - -win32 { - msvc:LIBS += Advapi32.lib - gcc:LIBS += -ladvapi32 -} diff --git a/client/3rd/SingleApplication/singleapplication_p.cpp b/client/3rd/SingleApplication/singleapplication_p.cpp deleted file mode 100644 index e65bd955..00000000 --- a/client/3rd/SingleApplication/singleapplication_p.cpp +++ /dev/null @@ -1,486 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2020 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// -// W A R N I N G !!! -// ----------------- -// -// This file is not part of the SingleApplication API. It is used purely as an -// implementation detail. This header file may change from version to -// version without notice, or may even be removed. -// - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) -#include -#else -#include -#endif - -#include "singleapplication.h" -#include "singleapplication_p.h" - -#ifdef Q_OS_UNIX - #include - #include - #include -#endif - -#ifdef Q_OS_WIN - #ifndef NOMINMAX - #define NOMINMAX 1 - #endif - #include - #include -#endif - -SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) - : q_ptr( q_ptr ) -{ - server = nullptr; - socket = nullptr; - memory = nullptr; - instanceNumber = 0; -} - -SingleApplicationPrivate::~SingleApplicationPrivate() -{ - if( socket != nullptr ){ - socket->close(); - delete socket; - } - - if( memory != nullptr ){ - memory->lock(); - auto *inst = static_cast(memory->data()); - if( server != nullptr ){ - server->close(); - delete server; - inst->primary = false; - inst->primaryPid = -1; - inst->primaryUser[0] = '\0'; - inst->checksum = blockChecksum(); - } - memory->unlock(); - - delete memory; - } -} - -QString SingleApplicationPrivate::getUsername() -{ -#ifdef Q_OS_WIN - wchar_t username[UNLEN + 1]; - // Specifies size of the buffer on input - DWORD usernameLength = UNLEN + 1; - if( GetUserNameW( username, &usernameLength ) ) - return QString::fromWCharArray( username ); -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - return QString::fromLocal8Bit( qgetenv( "USERNAME" ) ); -#else - return qEnvironmentVariable( "USERNAME" ); -#endif -#endif -#ifdef Q_OS_UNIX - QString username; - uid_t uid = geteuid(); - struct passwd *pw = getpwuid( uid ); - if( pw ) - username = QString::fromLocal8Bit( pw->pw_name ); - if ( username.isEmpty() ){ -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - username = QString::fromLocal8Bit( qgetenv( "USER" ) ); -#else - username = qEnvironmentVariable( "USER" ); -#endif - } - return username; -#endif -} - -void SingleApplicationPrivate::genBlockServerName() -{ - QCryptographicHash appData( QCryptographicHash::Sha256 ); - appData.addData( "SingleApplication", 17 ); - appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); - appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); - appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); - - if ( ! appDataList.isEmpty() ) - appData.addData( appDataList.join( "" ).toUtf8() ); - - if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){ - appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); - } - - if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){ -#ifdef Q_OS_WIN - appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); -#else - appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); -#endif - } - - // User level block requires a user specific data in the hash - if( options & SingleApplication::Mode::User ){ - appData.addData( getUsername().toUtf8() ); - } - - // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with - // server naming requirements. - blockServerName = appData.result().toBase64().replace("/", "_"); -} - -void SingleApplicationPrivate::initializeMemoryBlock() const -{ - auto *inst = static_cast( memory->data() ); - inst->primary = false; - inst->secondary = 0; - inst->primaryPid = -1; - inst->primaryUser[0] = '\0'; - inst->checksum = blockChecksum(); -} - -void SingleApplicationPrivate::startPrimary() -{ - // Reset the number of connections - auto *inst = static_cast ( memory->data() ); - - inst->primary = true; - inst->primaryPid = QCoreApplication::applicationPid(); - qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) ); - inst->checksum = blockChecksum(); - instanceNumber = 0; - // Successful creation means that no main process exists - // So we start a QLocalServer to listen for connections - QLocalServer::removeServer( blockServerName ); - server = new QLocalServer(); - - // Restrict access to the socket according to the - // SingleApplication::Mode::User flag on User level or no restrictions - if( options & SingleApplication::Mode::User ){ - server->setSocketOptions( QLocalServer::UserAccessOption ); - } else { - server->setSocketOptions( QLocalServer::WorldAccessOption ); - } - - server->listen( blockServerName ); - QObject::connect( - server, - &QLocalServer::newConnection, - this, - &SingleApplicationPrivate::slotConnectionEstablished - ); -} - -void SingleApplicationPrivate::startSecondary() -{ - auto *inst = static_cast ( memory->data() ); - - inst->secondary += 1; - inst->checksum = blockChecksum(); - instanceNumber = inst->secondary; -} - -bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) -{ - QElapsedTimer time; - time.start(); - - // Connect to the Local Server of the Primary Instance if not already - // connected. - if( socket == nullptr ){ - socket = new QLocalSocket(); - } - - if( socket->state() == QLocalSocket::ConnectedState ) return true; - - if( socket->state() != QLocalSocket::ConnectedState ){ - - while( true ){ - randomSleep(); - - if( socket->state() != QLocalSocket::ConnectingState ) - socket->connectToServer( blockServerName ); - - if( socket->state() == QLocalSocket::ConnectingState ){ - socket->waitForConnected( static_cast(msecs - time.elapsed()) ); - } - - // If connected break out of the loop - if( socket->state() == QLocalSocket::ConnectedState ) break; - - // If elapsed time since start is longer than the method timeout return - if( time.elapsed() >= msecs ) return false; - } - } - - // Initialisation message according to the SingleApplication protocol - QByteArray initMsg; - QDataStream writeStream(&initMsg, QIODevice::WriteOnly); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - writeStream.setVersion(QDataStream::Qt_5_6); -#endif - - writeStream << blockServerName.toLatin1(); - writeStream << static_cast(connectionType); - writeStream << instanceNumber; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum(QByteArray(initMsg, static_cast(initMsg.length()))); -#else - quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); -#endif - writeStream << checksum; - - // The header indicates the message length that follows - QByteArray header; - QDataStream headerStream(&header, QIODevice::WriteOnly); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion(QDataStream::Qt_5_6); -#endif - headerStream << static_cast ( initMsg.length() ); - - socket->write( header ); - socket->write( initMsg ); - bool result = socket->waitForBytesWritten( static_cast(msecs - time.elapsed()) ); - socket->flush(); - return result; -} - -quint16 SingleApplicationPrivate::blockChecksum() const -{ -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum(QByteArray(static_cast(memory->constData()), offsetof(InstancesInfo, checksum))); -#else - quint16 checksum = qChecksum(static_cast(memory->constData()), offsetof(InstancesInfo, checksum)); -#endif - return checksum; -} - -qint64 SingleApplicationPrivate::primaryPid() const -{ - qint64 pid; - - memory->lock(); - auto *inst = static_cast( memory->data() ); - pid = inst->primaryPid; - memory->unlock(); - - return pid; -} - -QString SingleApplicationPrivate::primaryUser() const -{ - QByteArray username; - - memory->lock(); - auto *inst = static_cast( memory->data() ); - username = inst->primaryUser; - memory->unlock(); - - return QString::fromUtf8( username ); -} - -/** - * @brief Executed when a connection has been made to the LocalServer - */ -void SingleApplicationPrivate::slotConnectionEstablished() -{ - QLocalSocket *nextConnSocket = server->nextPendingConnection(); - connectionMap.insert(nextConnSocket, ConnectionInfo()); - - QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, - [nextConnSocket, this](){ - auto &info = connectionMap[nextConnSocket]; - Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); - } - ); - - QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); - - QObject::connect(nextConnSocket, &QLocalSocket::destroyed, - [nextConnSocket, this](){ - connectionMap.remove(nextConnSocket); - } - ); - - QObject::connect(nextConnSocket, &QLocalSocket::readyRead, - [nextConnSocket, this](){ - auto &info = connectionMap[nextConnSocket]; - switch(info.stage){ - case StageHeader: - readInitMessageHeader(nextConnSocket); - break; - case StageBody: - readInitMessageBody(nextConnSocket); - break; - case StageConnected: - Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId ); - break; - default: - break; - }; - } - ); -} - -void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) -{ - if (!connectionMap.contains( sock )){ - return; - } - - if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){ - return; - } - - QDataStream headerStream( sock ); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion( QDataStream::Qt_5_6 ); -#endif - - // Read the header to know the message length - quint64 msgLen = 0; - headerStream >> msgLen; - ConnectionInfo &info = connectionMap[sock]; - info.stage = StageBody; - info.msgLen = msgLen; - - if ( sock->bytesAvailable() >= (qint64) msgLen ){ - readInitMessageBody( sock ); - } -} - -void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) -{ - Q_Q(SingleApplication); - - if (!connectionMap.contains( sock )){ - return; - } - - ConnectionInfo &info = connectionMap[sock]; - if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ - return; - } - - // Read the message body - QByteArray msgBytes = sock->read(info.msgLen); - QDataStream readStream(msgBytes); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - readStream.setVersion( QDataStream::Qt_5_6 ); -#endif - - // server name - QByteArray latin1Name; - readStream >> latin1Name; - - // connection type - ConnectionType connectionType = InvalidConnection; - quint8 connTypeVal = InvalidConnection; - readStream >> connTypeVal; - connectionType = static_cast ( connTypeVal ); - - // instance id - quint32 instanceId = 0; - readStream >> instanceId; - - // checksum - quint16 msgChecksum = 0; - readStream >> msgChecksum; - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast(msgBytes.length() - sizeof(quint16)))); -#else - const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16))); -#endif - - bool isValid = readStream.status() == QDataStream::Ok && - QLatin1String(latin1Name) == blockServerName && - msgChecksum == actualChecksum; - - if( !isValid ){ - sock->close(); - return; - } - - info.instanceId = instanceId; - info.stage = StageConnected; - - if( connectionType == NewInstance || - ( connectionType == SecondaryInstance && - options & SingleApplication::Mode::SecondaryNotification ) ) - { - Q_EMIT q->instanceStarted(); - } - - if (sock->bytesAvailable() > 0){ - Q_EMIT this->slotDataAvailable( sock, instanceId ); - } -} - -void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) -{ - Q_Q(SingleApplication); - Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); -} - -void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) -{ - if( closedSocket->bytesAvailable() > 0 ) - Q_EMIT slotDataAvailable( closedSocket, instanceId ); -} - -void SingleApplicationPrivate::randomSleep() -{ -#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 ) - QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u )); -#else - qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits::max() ); - QThread::msleep( 8 + static_cast ( static_cast ( qrand() ) / RAND_MAX * 10 )); -#endif -} - -void SingleApplicationPrivate::addAppData(const QString &data) -{ - appDataList.push_back(data); -} - -QStringList SingleApplicationPrivate::appData() const -{ - return appDataList; -} diff --git a/client/3rd/SingleApplication/singleapplication_p.h b/client/3rd/SingleApplication/singleapplication_p.h deleted file mode 100644 index c49a46dd..00000000 --- a/client/3rd/SingleApplication/singleapplication_p.h +++ /dev/null @@ -1,104 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2020 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// -// W A R N I N G !!! -// ----------------- -// -// This file is not part of the SingleApplication API. It is used purely as an -// implementation detail. This header file may change from version to -// version without notice, or may even be removed. -// - -#ifndef SINGLEAPPLICATION_P_H -#define SINGLEAPPLICATION_P_H - -#include -#include -#include -#include "singleapplication.h" - -struct InstancesInfo { - bool primary; - quint32 secondary; - qint64 primaryPid; - char primaryUser[128]; - quint16 checksum; // Must be the last field -}; - -struct ConnectionInfo { - qint64 msgLen = 0; - quint32 instanceId = 0; - quint8 stage = 0; -}; - -class SingleApplicationPrivate : public QObject { -Q_OBJECT -public: - enum ConnectionType : quint8 { - InvalidConnection = 0, - NewInstance = 1, - SecondaryInstance = 2, - Reconnect = 3 - }; - enum ConnectionStage : quint8 { - StageHeader = 0, - StageBody = 1, - StageConnected = 2, - }; - Q_DECLARE_PUBLIC(SingleApplication) - - SingleApplicationPrivate( SingleApplication *q_ptr ); - ~SingleApplicationPrivate() override; - - static QString getUsername(); - void genBlockServerName(); - void initializeMemoryBlock() const; - void startPrimary(); - void startSecondary(); - bool connectToPrimary( int msecs, ConnectionType connectionType ); - quint16 blockChecksum() const; - qint64 primaryPid() const; - QString primaryUser() const; - void readInitMessageHeader(QLocalSocket *socket); - void readInitMessageBody(QLocalSocket *socket); - static void randomSleep(); - void addAppData(const QString &data); - QStringList appData() const; - - SingleApplication *q_ptr; - QSharedMemory *memory; - QLocalSocket *socket; - QLocalServer *server; - quint32 instanceNumber; - QString blockServerName; - SingleApplication::Options options; - QMap connectionMap; - QStringList appDataList; - -public Q_SLOTS: - void slotConnectionEstablished(); - void slotDataAvailable( QLocalSocket*, quint32 ); - void slotClientConnectionClosed( QLocalSocket*, quint32 ); -}; - -#endif // SINGLEAPPLICATION_P_H diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 526b9fa9..2d06b443 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "logger.h" #include "ui/models/installedAppsModel.h" @@ -28,13 +30,7 @@ #include #endif -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) -#else -AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options, int timeout, - const QString &userData) - : SingleApplication(argc, argv, allowSecondary, options, timeout, userData) -#endif { setQuitOnLastWindowClosed(false); @@ -180,16 +176,6 @@ void AmneziaApplication::init() m_pageController->showOnStartup(); #endif - // TODO - fix -#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) - if (isPrimary()) { - QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() { - qDebug() << "Secondary instance started, showing this window instead"; - emit m_pageController->raiseMainWindow(); - }); - } -#endif - // Android TextArea clipboard workaround // Text from TextArea always has "text/html" mime-type: // /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1865 @@ -294,6 +280,24 @@ bool AmneziaApplication::parseCommands() return true; } +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +void AmneziaApplication::startLocalServer() { + const QString serverName("AmneziaVPNInstance"); + QLocalServer::removeServer(serverName); + + QLocalServer* server = new QLocalServer(this); + server->listen(serverName); + + QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() { + if (server) { + QLocalSocket* clientConnection = server->nextPendingConnection(); + clientConnection->deleteLater(); + } + emit m_pageController->raiseMainWindow(); + }); +} +#endif + QQmlApplicationEngine *AmneziaApplication::qmlEngine() const { return m_engine; diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 6fb61f44..64566216 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -53,22 +53,14 @@ #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #define AMNEZIA_BASE_CLASS QGuiApplication #else - #define AMNEZIA_BASE_CLASS SingleApplication - #define QAPPLICATION_CLASS QApplication - #include "singleapplication.h" + #define AMNEZIA_BASE_CLASS QApplication #endif class AmneziaApplication : public AMNEZIA_BASE_CLASS { Q_OBJECT public: -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) AmneziaApplication(int &argc, char *argv[]); -#else - AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false, - SingleApplication::Options options = SingleApplication::User, int timeout = 1000, - const QString &userData = {}); -#endif virtual ~AmneziaApplication(); void init(); @@ -78,6 +70,10 @@ public: void updateTranslator(const QLocale &locale); bool parseCommands(); +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + void startLocalServer(); +#endif + QQmlApplicationEngine *qmlEngine() const; QNetworkAccessManager *manager() { return m_nam; } diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 087f4961..2b5036c5 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -2,10 +2,6 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}") -if(NOT IOS AND NOT ANDROID) - include(${CLIENT_ROOT_DIR}/3rd/SingleApplication/singleapplication.cmake) -endif() - add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel) set(LIBS ${LIBS} SortFilterProxyModel) include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake) diff --git a/client/main.cpp b/client/main.cpp index 3a719096..aca9e62b 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -15,13 +15,24 @@ #include "platforms/ios/QtAppDelegate-C-Interface.h" #endif +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +bool isAnotherInstanceRunning() +{ + QLocalSocket socket; + socket.connectToServer("AmneziaVPNInstance"); + if (socket.waitForConnected(500)) { + qWarning() << "AmneziaVPN is already running"; + return true; + } + return false; +} +#endif + int main(int argc, char *argv[]) { Migrations migrationsManager; migrationsManager.doMigrations(); - QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); - #ifdef Q_OS_WIN AllowSetForegroundWindow(ASFW_ANY); #endif @@ -32,16 +43,14 @@ int main(int argc, char *argv[]) qputenv("ANDROID_OPENSSL_SUFFIX", "_3"); #endif -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) AmneziaApplication app(argc, argv); -#else - AmneziaApplication app(argc, argv, true, - SingleApplication::Mode::User | SingleApplication::Mode::SecondaryNotification); - if (!app.isPrimary()) { +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (isAnotherInstanceRunning()) { QTimer::singleShot(1000, &app, [&]() { app.quit(); }); return app.exec(); } + app.startLocalServer(); #endif // Allow to raise app window if secondary instance launched From 2ef267bc446e3cf0b48af1e29473ea813afe586e Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Wed, 25 Sep 2024 10:10:36 -0700 Subject: [PATCH 40/90] Revert iOS OpenVPN version (#1078) --- client/3rd/OpenVPNAdapter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/3rd/OpenVPNAdapter b/client/3rd/OpenVPNAdapter index dea60409..7c821a8d 160000 --- a/client/3rd/OpenVPNAdapter +++ b/client/3rd/OpenVPNAdapter @@ -1 +1 @@ -Subproject commit dea6040996298e947d63fb172709e6abfec2ba93 +Subproject commit 7c821a8d5c1ad5ad94e0763b4f25a875b5a6fe1b From c3805195af6d48e88c3d9aa18de65c3e38850a33 Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 27 Sep 2024 20:02:46 +0300 Subject: [PATCH 41/90] Bump version to 4.8.1.1 (#1096) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 79b0c18c..f72e9cf3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.1.0 +project(${PROJECT} VERSION 4.8.1.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 62) +set(APP_ANDROID_VERSION_CODE 63) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 371cadcc02d741ad2d77d09f6de7e665bc2b86d1 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 29 Sep 2024 18:29:36 +0400 Subject: [PATCH 42/90] chore: bump version to 4.8.1.8 (#1099) - fixed m_isDevGatewayEnv initialization in Settings class --- CMakeLists.txt | 2 +- client/core/controllers/apiController.cpp | 4 ++-- client/settings.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f72e9cf3..91032c9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.1.1 +project(${PROJECT} VERSION 4.8.1.8 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 3f8684e0..5cdaa7ae 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -357,9 +357,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co EVP_PKEY *publicKey = nullptr; try { - QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QSimpleCrypto::QRsa rsa; - publicKey = rsa.getPublicKeyFromByteArray(key); + publicKey = rsa.getPublicKeyFromByteArray(rsaKey); } catch (...) { qCritical() << "error loading public key from environment variables"; return ErrorCode::ApiMissingAgwPublicKey; diff --git a/client/settings.h b/client/settings.h index c0ab0559..f41f4d29 100644 --- a/client/settings.h +++ b/client/settings.h @@ -237,7 +237,7 @@ private: mutable SecureQSettings m_settings; QString m_gatewayEndpoint; - bool m_isDevGatewayEnv; + bool m_isDevGatewayEnv = false; }; #endif // SETTINGS_H From e8736102bf900b410a905698a13cc2bf01b4f910 Mon Sep 17 00:00:00 2001 From: albexk Date: Sun, 29 Sep 2024 18:37:07 +0300 Subject: [PATCH 43/90] Bump Android version code (#1100) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 91032c9c..24693d37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 63) +set(APP_ANDROID_VERSION_CODE 64) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 4c08e9f3bc85e8b94e0cdff227893d69c027dee9 Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 30 Sep 2024 13:38:48 +0300 Subject: [PATCH 44/90] Fix UnknownHostException --- client/android/src/org/amnezia/vpn/AmneziaVpnService.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 6a7da7c7..937127ee 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -22,6 +22,7 @@ import androidx.annotation.MainThread import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import java.net.UnknownHostException import java.util.concurrent.ConcurrentHashMap import kotlin.LazyThreadSafetyMode.NONE import kotlinx.coroutines.CoroutineExceptionHandler @@ -127,6 +128,8 @@ open class AmneziaVpnService : VpnService() { is LoadLibraryException -> onError("${e.message}. Caused: ${e.cause?.message}") + is UnknownHostException -> onError("Unknown host") + else -> throw e } } From f0903c32f36c323452cb4164837163cce319f00a Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 30 Sep 2024 17:31:54 +0300 Subject: [PATCH 45/90] Bump version to 4.8.1.9 (#1103) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 24693d37..fba4183c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.1.8 +project(${PROJECT} VERSION 4.8.1.9 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 64) +set(APP_ANDROID_VERSION_CODE 65) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 2763da960f608dd2b6b27b7751f6dd63d1ba86c0 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 2 Oct 2024 13:20:16 +0800 Subject: [PATCH 46/90] chore: added clear() after extractConfigFromData() on android --- client/amnezia_application.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 2d06b443..4e25097d 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -111,10 +111,11 @@ void AmneziaApplication::init() qFatal("Android controller initialization failed"); } - connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) { - m_pageController->goToPageHome(); + connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); + data.clear(); + emit m_pageController->goToPageViewConfig(); }); m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); @@ -122,16 +123,16 @@ void AmneziaApplication::init() #ifdef Q_OS_IOS IosController::Instance()->initialize(); - connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) { - m_pageController->goToPageHome(); + connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); + emit m_pageController->goToPageViewConfig(); }); - connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) { - m_pageController->goToPageHome(); + connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) { + emit m_pageController->goToPageHome(); m_pageController->goToPageSettingsBackup(); - m_settingsController->importBackupFromOutside(filePath); + emit m_settingsController->importBackupFromOutside(filePath); }); QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); From dce08b3eccec04007e30ec8a8f2187106ebc1472 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sun, 6 Oct 2024 13:19:06 +0800 Subject: [PATCH 47/90] added processing of auth_data section when requesting api config - fixed saving api_config section when processing backend response --- client/core/controllers/apiController.cpp | 40 ++++++++++++------- client/core/controllers/apiController.h | 2 +- .../ui/controllers/connectionController.cpp | 3 ++ client/ui/controllers/importController.cpp | 3 +- client/ui/controllers/installController.cpp | 13 +++--- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 5cdaa7ae..47197ad9 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -40,6 +40,9 @@ namespace constexpr char apiPayload[] = "api_payload"; constexpr char keyPayload[] = "key_payload"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; } const QStringList proxyStorageUrl = { "" }; @@ -94,8 +97,8 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); } else if (protocol == configKey::awg) { configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); - auto serverConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - auto containers = serverConfig.value(config_key::containers).toArray(); + auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + auto containers = newServerConfig.value(config_key::containers).toArray(); if (containers.isEmpty()) { return; // todo process error } @@ -114,25 +117,30 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); container[containerName] = containerConfig; containers.replace(0, container); - serverConfig[config_key::containers] = containers; - configStr = QString(QJsonDocument(serverConfig).toJson()); + newServerConfig[config_key::containers] = containers; + configStr = QString(QJsonDocument(newServerConfig).toJson()); } - QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1); - serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2); - serverConfig[config_key::containers] = apiConfig.value(config_key::containers); - serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName); + QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1); + serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2); + serverConfig[config_key::containers] = newServerConfig.value(config_key::containers); + serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName); - if (apiConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { - serverConfig[config_key::configVersion] = apiConfig.value(config_key::configVersion); - serverConfig[config_key::description] = apiConfig.value(config_key::description); - serverConfig[config_key::name] = apiConfig.value(config_key::name); + if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { + serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion); + serverConfig[config_key::description] = newServerConfig.value(config_key::description); + serverConfig[config_key::name] = newServerConfig.value(config_key::name); } - auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString(); + auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString(); serverConfig[config_key::defaultContainer] = defaultContainer; + QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap(); + map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap()); + auto apiConfig = QJsonObject::fromVariantMap(map); + serverConfig[configKey::apiConfig] = apiConfig; + return; } @@ -316,7 +324,8 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) } ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig) + const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, + QJsonObject &serverConfig) { #ifdef Q_OS_IOS IosController::Instance()->requestInetAccess(); @@ -339,6 +348,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } apiPayload[configKey::serviceType] = serviceType; apiPayload[configKey::uuid] = installationUuid; + apiPayload[configKey::authData] = authData; QSimpleCrypto::QBlockCipher blockCipher; QByteArray key = blockCipher.generatePrivateSalt(32); diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index 1f811498..bcb25f96 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -21,7 +21,7 @@ public slots: ErrorCode getServicesList(QByteArray &responseBody); ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig); + const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig); signals: void errorOccurred(ErrorCode errorCode); diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index c7f95000..a51556a1 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -51,6 +51,9 @@ void ConnectionController::openConnection() if (configVersion == ApiConfigSources::Telegram && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromTelegram(); + } else if (configVersion == ApiConfigSources::AmneziaGateway + && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { qDebug() << "attempt to update api config by end_date event"; if (configVersion == ApiConfigSources::Telegram) { diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 261551ea..2b7681eb 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -39,11 +39,12 @@ namespace const QString amneziaConfigPatternUserName = "userName"; const QString amneziaConfigPatternPassword = "password"; const QString amneziaFreeConfigPattern = "api_key"; + const QString amneziaPremiumConfigPattern = "auth_data"; const QString backupPattern = "Servers/serversList"; if (config.contains(backupPattern)) { return ConfigTypes::Backup; - } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) + } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) || config.contains(amneziaPremiumConfigPattern) || (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName) && config.contains(amneziaConfigPatternPassword))) { return ConfigTypes::Amnezia; diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index c6f17057..628ea59d 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -32,6 +32,7 @@ namespace constexpr char availableCountries[] = "available_countries"; constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; } #ifdef Q_OS_WINDOWS @@ -826,7 +827,7 @@ bool InstallController::installServiceFromApi() ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), - m_apiServicesModel->getSelectedServiceProtocol(), "", serverConfig); + m_apiServicesModel->getSelectedServiceProtocol(), "", QJsonObject(), serverConfig); if (errorCode != ErrorCode::NoError) { emit installationErrorOccurred(errorCode); return false; @@ -853,19 +854,19 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + auto authData = serverConfig.value(configKey::authData).toObject(); QJsonObject newServerConfig; - ErrorCode errorCode = - apiController.getConfigForService(m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(), - apiConfig.value(configKey::serviceType).toString(), - apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode, newServerConfig); + ErrorCode errorCode = apiController.getConfigForService( + m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(), + apiConfig.value(configKey::serviceType).toString(), apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode, + authData, newServerConfig); if (errorCode != ErrorCode::NoError) { emit installationErrorOccurred(errorCode); return false; } QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject(); - newApiConfig.insert(configKey::serviceInfo, apiConfig.value(configKey::serviceInfo)); newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode)); newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); From 399a8c6d287504cfe26ea3a939041da30fed211e Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 11 Oct 2024 05:58:30 +0400 Subject: [PATCH 48/90] bugfix: fixed qml warnings when loading user list on PageShare (#1119) --- client/ui/models/clientManagementModel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7d3be2cb..7445d60f 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -77,6 +77,7 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co { beginResetModel(); m_clientsTable = QJsonArray(); + endResetModel(); ErrorCode error = ErrorCode::NoError; @@ -90,10 +91,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co const QByteArray clientsTableString = serverController->getTextFileFromContainer(container, credentials, clientsTableFile, error); if (error != ErrorCode::NoError) { logger.error() << "Failed to get the clientsTable file from the server"; - endResetModel(); return error; } + beginResetModel(); m_clientsTable = QJsonDocument::fromJson(clientsTableString).array(); if (m_clientsTable.isEmpty()) { @@ -601,5 +602,6 @@ QHash ClientManagementModel::roleNames() const roles[LatestHandshakeRole] = "latestHandshake"; roles[DataReceivedRole] = "dataReceived"; roles[DataSentRole] = "dataSent"; + roles[AllowedIpsRole] = "allowedIps"; return roles; } From 694e781beb8e2d065dfb6d5a7ec1b24c1873727d Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 11 Oct 2024 05:58:53 +0400 Subject: [PATCH 49/90] bugfix: fixed path to log folder for wireguard on windows (#1137) --- client/platforms/windows/windowscommons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp index c0a14dda..4c0d8176 100644 --- a/client/platforms/windows/windowscommons.cpp +++ b/client/platforms/windows/windowscommons.cpp @@ -21,7 +21,7 @@ #include "platforms/windows/windowsutils.h" constexpr const char* VPN_NAME = "AmneziaVPN"; -constexpr const char* WIREGUARD_DIR = "WireGuard"; +constexpr const char* WIREGUARD_DIR = "AmneziaWG"; constexpr const char* DATA_DIR = "Data"; namespace { From 7b838e77a02fa01ef8a157c109201d0fdf4a4e96 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 13 Oct 2024 15:14:43 +0400 Subject: [PATCH 50/90] bugfix: removed the importErrorOccurred() signal overload, since qml does not know how to handle signal overloads (#1111) --- client/core/defs.h | 1 + client/core/errorstrings.cpp | 1 + client/ui/controllers/importController.cpp | 6 +++--- client/ui/controllers/importController.h | 1 - client/ui/controllers/installController.cpp | 2 +- client/ui/controllers/installController.h | 2 +- client/ui/qml/Pages2/PageSetupWizardViewConfig.qml | 2 +- client/ui/qml/Pages2/PageStart.qml | 4 ++++ 8 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/core/defs.h b/client/core/defs.h index ebc07f4b..802eca45 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -96,6 +96,7 @@ namespace amnezia // import and install errors ImportInvalidConfigError = 900, + ImportOpenConfigError = 901, // Android errors AndroidError = 1000, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 8c16d786..00e94995 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -50,6 +50,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break; case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break; + case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr(""); break; // Android errors case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 261551ea..168e3564 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -84,7 +84,7 @@ bool ImportController::extractConfigFromFile(const QString &fileName) return extractConfigFromData(data); } - emit importErrorOccurred(tr("Unable to open file"), false); + emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); return false; } @@ -188,12 +188,12 @@ bool ImportController::extractConfigFromData(QString data) if (!m_serversModel->getServersCount()) { emit restoreAppConfig(config.toUtf8()); } else { - emit importErrorOccurred(tr("Invalid configuration file"), false); + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); } break; } case ConfigTypes::Invalid: { - emit importErrorOccurred(tr("Invalid configuration file"), false); + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); break; } } diff --git a/client/ui/controllers/importController.h b/client/ui/controllers/importController.h index 61205253..05e320a5 100644 --- a/client/ui/controllers/importController.h +++ b/client/ui/controllers/importController.h @@ -54,7 +54,6 @@ public slots: signals: void importFinished(); - void importErrorOccurred(const QString &errorMessage, bool goToPageHome); void importErrorOccurred(ErrorCode errorCode, bool goToPageHome); void qrDecodingFinished(); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index c6f17057..31aa1fb1 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -768,7 +768,7 @@ bool InstallController::checkSshConnection(QSharedPointer serv } else { if (output.contains(tr("Please login as the user"))) { output.replace("\n", ""); - emit installationErrorOccurred(output); + emit wrongInstallationUser(output); return false; } } diff --git a/client/ui/controllers/installController.h b/client/ui/controllers/installController.h index 7eea216a..d7ab3553 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -75,8 +75,8 @@ signals: void removeAllContainersFinished(const QString &finishedMessage); void removeProcessedContainerFinished(const QString &finishedMessage); - void installationErrorOccurred(const QString &errorMessage); void installationErrorOccurred(ErrorCode errorCode); + void wrongInstallationUser(const QString &message); void serverAlreadyExists(int serverIndex); diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 3aac1555..92048f36 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -37,7 +37,7 @@ PageType { Connections { target: ImportController - function onImportErrorOccurred(errorMessage, goToPageHome) { + function onImportErrorOccurred(error, goToPageHome) { if (goToPageHome) { PageController.goToStartPage() } else { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index bb6663fb..640c61ef 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -123,6 +123,10 @@ PageType { } } + function onWrongInstallationUser(message) { + onInstallationErrorOccurred(message) + } + function onUpdateContainerFinished(message) { PageController.showNotificationMessage(message) PageController.closePage() From 2c9067b0de3e4a80979cbc9343220873fc54ea45 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 18 Oct 2024 14:57:20 +0800 Subject: [PATCH 51/90] bugfix: added missing text in the errors --- client/core/errorstrings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 00e94995..c5c0363b 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -50,7 +50,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break; case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break; - case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr(""); break; + case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break; // Android errors case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; From 60de146f031116bcc273a1f88045c838bb63b876 Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 18 Oct 2024 13:47:53 +0400 Subject: [PATCH 52/90] chore/mozilla upstream (#1136) * cherry-pick commit 5a51e292d44ec0fb07867aff0401b4c2a8fca1e8 from mozila upstream * cherry-pick commit e8ecb857dcfb804b7766a54e725b442fc6c0e661 from mozila upstream * cherry-pick commit 16269ffa600905b09678014f64951748fb0ff8ad from mozila upstream --- client/daemon/daemon.cpp | 23 ++++--------------- client/daemon/daemon.h | 1 - client/daemon/daemonlocalserverconnection.cpp | 17 ++++++++++---- client/mozilla/localsocketcontroller.cpp | 4 ++-- client/platforms/linux/daemon/linuxdaemon.h | 1 - client/platforms/macos/daemon/macosdaemon.h | 1 - .../platforms/windows/daemon/windowsdaemon.h | 1 - .../windows/daemon/windowssplittunnel.cpp | 2 +- 8 files changed, 21 insertions(+), 29 deletions(-) diff --git a/client/daemon/daemon.cpp b/client/daemon/daemon.cpp index 3e237e9c..a234860b 100644 --- a/client/daemon/daemon.cpp +++ b/client/daemon/daemon.cpp @@ -78,7 +78,7 @@ bool Daemon::activate(const InterfaceConfig& config) { return false; } - if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { + if (!dnsutils()->restoreResolvers()) { return false; } @@ -165,10 +165,6 @@ bool Daemon::activate(const InterfaceConfig& config) { } bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) { - if (!supportDnsUtils()) { - return true; - } - if ((config.m_hopType == InterfaceConfig::MultiHopExit) || (config.m_hopType == InterfaceConfig::SingleHop)) { QList resolvers; @@ -423,13 +419,8 @@ bool Daemon::deactivate(bool emitSignals) { } // Cleanup DNS - if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { - return false; - } - - if (!wgutils()->interfaceExists()) { - logger.warning() << "Wireguard interface does not exist."; - return false; + if (!dnsutils()->restoreResolvers()) { + logger.warning() << "Failed to restore DNS resolvers."; } // Cleanup peers and routing @@ -449,13 +440,9 @@ bool Daemon::deactivate(bool emitSignals) { } m_excludedAddrSet.clear(); - // Delete the interface - if (!wgutils()->deleteInterface()) { - return false; - } - m_connections.clear(); - return true; + // Delete the interface + return wgutils()->deleteInterface(); } QString Daemon::logs() { diff --git a/client/daemon/daemon.h b/client/daemon/daemon.h index d3d8c34d..3d418d70 100644 --- a/client/daemon/daemon.h +++ b/client/daemon/daemon.h @@ -69,7 +69,6 @@ class Daemon : public QObject { virtual WireguardUtils* wgutils() const = 0; virtual bool supportIPUtils() const { return false; } virtual IPUtils* iputils() { return nullptr; } - virtual bool supportDnsUtils() const { return false; } virtual DnsUtils* dnsutils() { return nullptr; } static bool parseStringList(const QJsonObject& obj, const QString& name, diff --git a/client/daemon/daemonlocalserverconnection.cpp b/client/daemon/daemonlocalserverconnection.cpp index 1a49b7e5..edbc4c9b 100644 --- a/client/daemon/daemonlocalserverconnection.cpp +++ b/client/daemon/daemonlocalserverconnection.cpp @@ -92,6 +92,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { logger.debug() << "Command received:" << type; + // It is expected that sometimes the client will request backend logs + // before the first authentication. In these cases we just return empty + // logs. + if (type == "logs") { + QJsonObject obj; + obj.insert("type", "logs"); + obj.insert("logs", ""); + write(obj); + return; + } + if (type == "activate") { InterfaceConfig config; if (!Daemon::parseConfig(obj, config)) { @@ -115,8 +126,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { if (type == "status") { QJsonObject obj = Daemon::instance()->getStatus(); obj.insert("type", "status"); - m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - m_socket->write("\n"); + write(obj); return; } @@ -124,8 +134,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { QJsonObject obj; obj.insert("type", "logs"); obj.insert("logs", Daemon::instance()->logs().replace("\n", "|")); - m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - m_socket->write("\n"); + write(obj); return; } diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 4d040288..5e9f0f97 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -34,8 +34,8 @@ LocalSocketController::LocalSocketController() { m_socket = new QLocalSocket(this); connect(m_socket, &QLocalSocket::connected, this, &LocalSocketController::daemonConnected); - connect(m_socket, &QLocalSocket::disconnected, this, - &LocalSocketController::disconnected); + connect(m_socket, &QLocalSocket::disconnected, this, + [&] { errorOccurred(QLocalSocket::PeerClosedError); }); connect(m_socket, &QLocalSocket::errorOccurred, this, &LocalSocketController::errorOccurred); connect(m_socket, &QLocalSocket::readyRead, this, diff --git a/client/platforms/linux/daemon/linuxdaemon.h b/client/platforms/linux/daemon/linuxdaemon.h index 7f5d27b7..dbac8cee 100644 --- a/client/platforms/linux/daemon/linuxdaemon.h +++ b/client/platforms/linux/daemon/linuxdaemon.h @@ -22,7 +22,6 @@ class LinuxDaemon final : public Daemon { protected: WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } bool supportIPUtils() const override { return true; } IPUtils* iputils() override { return m_iputils; } diff --git a/client/platforms/macos/daemon/macosdaemon.h b/client/platforms/macos/daemon/macosdaemon.h index a48c326c..4181648e 100644 --- a/client/platforms/macos/daemon/macosdaemon.h +++ b/client/platforms/macos/daemon/macosdaemon.h @@ -21,7 +21,6 @@ class MacOSDaemon final : public Daemon { protected: WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } bool supportIPUtils() const override { return true; } IPUtils* iputils() override { return m_iputils; } diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h index 9d051bae..7e38c41e 100644 --- a/client/platforms/windows/daemon/windowsdaemon.h +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -26,7 +26,6 @@ class WindowsDaemon final : public Daemon { protected: bool run(Op op, const InterfaceConfig& config) override; WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } private: diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp index 39941933..c4e893b2 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.cpp +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -502,7 +502,7 @@ QString WindowsSplitTunnel::convertPath(const QString& path) { // device should contain : for e.g C: return ""; } - QByteArray buffer(2048, 0xFF); + QByteArray buffer(2048, 0xFFu); auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter), (wchar_t*)buffer.data(), buffer.size() / 2); From d63bf15011f074ef498af5d91c5c46092856ada4 Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 18 Oct 2024 12:52:24 +0300 Subject: [PATCH 53/90] Android qt 6.7.3 (#1143) * Up Qt to 6.7.3 * Bump version to 4.8.2.0 * Raise the minimum Android version to 8 (API 26) * Update version code to separate versions for new and old Androids * Fix mouse not working on TVs * Refactor logging * Bump version code --- .github/workflows/deploy.yml | 2 +- CMakeLists.txt | 4 +- client/android/gradle.properties | 2 +- .../protocolApi/src/main/kotlin/Protocol.kt | 4 - .../src/org/amnezia/vpn/AmneziaActivity.kt | 73 +++++++++++++++++-- .../src/org/amnezia/vpn/AmneziaVpnService.kt | 6 +- .../src/org/amnezia/vpn/AuthActivity.kt | 2 +- .../org/amnezia/vpn/ImportConfigActivity.kt | 8 +- .../org/amnezia/vpn/ServiceNotification.kt | 12 ++- .../utils/src/main/kotlin/LibraryLoader.kt | 2 +- client/android/utils/src/main/kotlin/Log.kt | 17 +---- .../utils/src/main/kotlin/net/NetworkState.kt | 20 ++--- .../vpn/protocol/wireguard/Wireguard.kt | 2 +- client/android/xray/src/main/kotlin/Xray.kt | 4 +- client/cmake/android.cmake | 2 +- 15 files changed, 97 insertions(+), 63 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d9138516..f9fb19a5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -301,7 +301,7 @@ jobs: env: ANDROID_BUILD_PLATFORM: android-34 - QT_VERSION: 6.7.2 + QT_VERSION: 6.7.3 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} diff --git a/CMakeLists.txt b/CMakeLists.txt index fba4183c..ce5777e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.1.9 +project(${PROJECT} VERSION 4.8.2.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 65) +set(APP_ANDROID_VERSION_CODE 2067) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/android/gradle.properties b/client/android/gradle.properties index 5a27838c..ce651e1c 100644 --- a/client/android/gradle.properties +++ b/client/android/gradle.properties @@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false # For development copy and set local values for these parameters in local.properties #androidCompileSdkVersion=android-34 #androidBuildToolsVersion=34.0.0 -#qtMinSdkVersion=24 +#qtMinSdkVersion=26 #qtTargetSdkVersion=34 #androidNdkVersion=26.1.10909125 #qtTargetAbiList=x86_64 diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index b5c382be..6e682aa4 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -1,6 +1,5 @@ package org.amnezia.vpn.protocol -import android.annotation.SuppressLint import android.content.Context import android.net.IpPrefix import android.net.VpnService @@ -8,9 +7,6 @@ import android.net.VpnService.Builder import android.os.Build import android.system.OsConstants import androidx.annotation.RequiresApi -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipFile import kotlinx.coroutines.flow.MutableStateFlow import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index d5026425..b2c2ff71 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -21,6 +21,7 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.provider.Settings +import android.view.MotionEvent import android.view.WindowManager.LayoutParams import android.webkit.MimeTypeMap import android.widget.Toast @@ -158,7 +159,7 @@ class AmneziaActivity : QtActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d(TAG, "Create Amnezia activity: $intent") + Log.d(TAG, "Create Amnezia activity") loadLibs() window.apply { addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) @@ -200,7 +201,7 @@ class AmneziaActivity : QtActivity() { NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED ) ) { - Log.d( + Log.v( TAG, "Notification state changed: ${it?.action}, blocked = " + "${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}" ) @@ -214,7 +215,7 @@ class AmneziaActivity : QtActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Log.d(TAG, "onNewIntent: $intent") + Log.v(TAG, "onNewIntent: $intent") intent?.let(::processIntent) } @@ -403,7 +404,7 @@ class AmneziaActivity : QtActivity() { @MainThread private fun startVpn(vpnConfig: String) { getVpnProto(vpnConfig)?.let { proto -> - Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto") + Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto") if (isServiceConnected) { if (proto.serviceClass == vpnProto?.serviceClass) { vpnProto = proto @@ -516,7 +517,7 @@ class AmneziaActivity : QtActivity() { startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( onSuccess = { it?.data?.let { uri -> - Log.d(TAG, "Save file to $uri") + Log.v(TAG, "Save file to $uri") try { contentResolver.openOutputStream(uri)?.use { os -> os.bufferedWriter().use { it.write(data) } @@ -565,7 +566,7 @@ class AmneziaActivity : QtActivity() { startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( onAny = { val uri = it?.data?.toString() ?: "" - Log.d(TAG, "Open file: $uri") + Log.v(TAG, "Open file: $uri") mainScope.launch { qtInitialized.await() QtAndroidController.onFileOpened(uri) @@ -720,6 +721,66 @@ class AmneziaActivity : QtActivity() { } } + // workaround for a bug in Qt that causes the mouse click event not to be handled + // also disable right-click, as it causes the application to crash + private var lastButtonState = 0 + private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain( + downTime, + eventTime, + action, + pointerCount, + (0 until pointerCount).map { i -> + MotionEvent.PointerProperties().apply { + getPointerProperties(i, this) + } + }.toTypedArray(), + (0 until pointerCount).map { i -> + MotionEvent.PointerCoords().apply { + getPointerCoords(i, this) + } + }.toTypedArray(), + metaState, + MotionEvent.BUTTON_PRIMARY, + xPrecision, + yPrecision, + deviceId, + edgeFlags, + source, + flags + ) + + private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + lastButtonState = ev.buttonState + if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true + } + + MotionEvent.ACTION_UP -> { + when (lastButtonState) { + MotionEvent.BUTTON_SECONDARY -> return true + MotionEvent.BUTTON_PRIMARY -> { + val modEvent = ev.fixCopy() + return superDispatch(modEvent).apply { modEvent.recycle() } + } + } + } + } + return superDispatch(ev) + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + return handleMouseEvent(ev) { super.dispatchTouchEvent(it) } + } + return super.dispatchTouchEvent(ev) + } + + override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { + ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }} + return super.dispatchTrackballEvent(ev) + } + /** * Utils methods */ diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 937127ee..8d108bc3 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -300,7 +300,7 @@ open class AmneziaVpnService : VpnService() { arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED ) { it?.action?.let { action -> - Log.d(TAG, "Broadcast request received: $action") + Log.v(TAG, "Broadcast request received: $action") when (action) { ACTION_CONNECT -> connect() ACTION_DISCONNECT -> disconnect() @@ -317,7 +317,7 @@ open class AmneziaVpnService : VpnService() { ) ) { val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false) - Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state") + Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state") if (state == false) { enableNotification() } else { @@ -450,7 +450,7 @@ open class AmneziaVpnService : VpnService() { serviceNotification.isNotificationEnabled() && getSystemService()?.isInteractive != false ) { - Log.d(TAG, "Launch traffic stats update") + Log.v(TAG, "Launch traffic stats update") trafficStats.reset() startTrafficStatsUpdateJob() } diff --git a/client/android/src/org/amnezia/vpn/AuthActivity.kt b/client/android/src/org/amnezia/vpn/AuthActivity.kt index 2593315c..46401548 100644 --- a/client/android/src/org/amnezia/vpn/AuthActivity.kt +++ b/client/android/src/org/amnezia/vpn/AuthActivity.kt @@ -66,7 +66,7 @@ class AuthActivity : FragmentActivity() { object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: AuthenticationResult) { super.onAuthenticationSucceeded(result) - Log.d(TAG, "Authentication succeeded") + Log.v(TAG, "Authentication succeeded") QtAndroidController.onAuthResult(true) finish() } diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index 9faa30d0..49823a36 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d(TAG, "Create Import Config Activity: $intent") + Log.v(TAG, "Create Import Config Activity: $intent") intent?.let(::readConfig) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - Log.d(TAG, "onNewIntent: $intent") + Log.v(TAG, "onNewIntent: $intent") intent.let(::readConfig) } private fun readConfig(intent: Intent) { when (intent.action) { ACTION_SEND -> { - Log.d(TAG, "Process SEND action, type: ${intent.type}") + Log.v(TAG, "Process SEND action, type: ${intent.type}") when (intent.type) { "application/octet-stream" -> { intent.getUriCompat()?.let { uri -> @@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() { } ACTION_VIEW -> { - Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}") + Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}") when (intent.scheme) { "file", "content" -> { intent.data?.let { uri -> diff --git a/client/android/src/org/amnezia/vpn/ServiceNotification.kt b/client/android/src/org/amnezia/vpn/ServiceNotification.kt index f4707731..47e8f263 100644 --- a/client/android/src/org/amnezia/vpn/ServiceNotification.kt +++ b/client/android/src/org/amnezia/vpn/ServiceNotification.kt @@ -62,7 +62,7 @@ class ServiceNotification(private val context: Context) { fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification { val speedString = if (state == CONNECTED) zeroSpeed else null - Log.d(TAG, "Build notification: $serverName, $state") + Log.v(TAG, "Build notification: $serverName, $state") return notificationBuilder .setSmallIcon(R.drawable.ic_amnezia_round) @@ -88,17 +88,15 @@ class ServiceNotification(private val context: Context) { fun isNotificationEnabled(): Boolean { if (!context.isNotificationPermissionGranted()) return false if (!notificationManager.areNotificationsEnabled()) return false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) - ?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true - } - return true + return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let { + it.importance != NotificationManager.IMPORTANCE_NONE + } ?: true } @SuppressLint("MissingPermission") fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) { if (context.isNotificationPermissionGranted()) { - Log.d(TAG, "Update notification: $serverName, $state") + Log.v(TAG, "Update notification: $serverName, $state") notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state)) } } diff --git a/client/android/utils/src/main/kotlin/LibraryLoader.kt b/client/android/utils/src/main/kotlin/LibraryLoader.kt index f1c6465e..8def18d0 100644 --- a/client/android/utils/src/main/kotlin/LibraryLoader.kt +++ b/client/android/utils/src/main/kotlin/LibraryLoader.kt @@ -46,7 +46,7 @@ object LibraryLoader { System.loadLibrary(libraryName) return } catch (_: UnsatisfiedLinkError) { - Log.d(TAG, "Failed to load library, try to extract it from apk") + Log.w(TAG, "Failed to load library, try to extract it from apk") } var tempFile: File? = null try { diff --git a/client/android/utils/src/main/kotlin/Log.kt b/client/android/utils/src/main/kotlin/Log.kt index a656b9ea..da11c200 100644 --- a/client/android/utils/src/main/kotlin/Log.kt +++ b/client/android/utils/src/main/kotlin/Log.kt @@ -1,8 +1,6 @@ package org.amnezia.vpn.util import android.content.Context -import android.icu.text.DateFormat -import android.icu.text.SimpleDateFormat import android.os.Build import android.os.Process import java.io.File @@ -12,8 +10,6 @@ import java.nio.channels.FileChannel import java.nio.channels.FileLock import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.Date -import java.util.Locale import java.util.concurrent.locks.ReentrantLock import org.amnezia.vpn.util.Log.Priority.D import org.amnezia.vpn.util.Log.Priority.E @@ -41,11 +37,7 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024 * | | | create a report and/or terminate the process | */ object Log { - private val dateTimeFormat: Any = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) - else object : ThreadLocal() { - override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US) - } + private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) private lateinit var logDir: File private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) } @@ -143,12 +135,7 @@ object Log { } private fun formatLogMsg(tag: String, msg: String, priority: Priority): String { - val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter) - } else { - @Suppress("UNCHECKED_CAST") - (dateTimeFormat as ThreadLocal).get()?.format(Date()) - } + val date = LocalDateTime.now().format(dateTimeFormat) return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " + "$tag: $msg\n" } diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index b71bf393..1cab5535 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -42,18 +42,12 @@ class NetworkState( private val networkCallback: NetworkCallback by lazy(NONE) { object : NetworkCallback() { override fun onAvailable(network: Network) { - Log.d(TAG, "onAvailable: $network") + Log.v(TAG, "onAvailable: $network") } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - checkNetworkState(network, networkCapabilities) - } else { - handler.post { - checkNetworkState(network, networkCapabilities) - } - } + Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") + checkNetworkState(network, networkCapabilities) } private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) { @@ -73,11 +67,11 @@ class NetworkState( } override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { - Log.d(TAG, "onBlockedStatusChanged: $network, $blocked") + Log.v(TAG, "onBlockedStatusChanged: $network, $blocked") } override fun onLost(network: Network) { - Log.d(TAG, "onLost: $network") + Log.v(TAG, "onLost: $network") } } } @@ -87,7 +81,7 @@ class NetworkState( Log.d(TAG, "Bind network listener") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + } else { val numberAttempts = 300 var attemptCount = 0 while(true) { @@ -108,8 +102,6 @@ class NetworkState( } } } - } else { - connectivityManager.requestNetwork(networkRequest, networkCallback) } isListenerBound = true } diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index ac11374b..e93834f4 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -66,7 +66,7 @@ open class Wireguard : Protocol() { try { delay(1000) var log = getLogcat(time) - Log.d(TAG, "First waiting log: $log") + Log.v(TAG, "First waiting log: $log") // check that there is a connection log, // to avoid infinite connection if (!log.contains("Attaching to interface")) { diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index 6e37c9c2..08242525 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -130,8 +130,8 @@ class Xray : Protocol() { LibXray.initXray(assetsPath) val geoDir = File(assetsPath, "geo").absolutePath val configPath = File(context.cacheDir, "config.json") - Log.d(TAG, "xray.location.asset: $geoDir") - Log.d(TAG, "config: $configPath") + Log.v(TAG, "xray.location.asset: $geoDir") + Log.v(TAG, "config: $configPath") try { configPath.writeText(configJson) } catch (e: IOException) { diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index c96d9ab8..34ca5bff 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -1,6 +1,6 @@ message("Client android ${CMAKE_ANDROID_ARCH_ABI} build") -set(APP_ANDROID_MIN_SDK 24) +set(APP_ANDROID_MIN_SDK 26) set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING "The minimum API level supported by the application or library" FORCE) From 74802f30ed9b838f0ad55d5d167c96f44dd3d459 Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 18 Oct 2024 13:57:38 +0400 Subject: [PATCH 54/90] feature/proxy storage bypass (#1179) * feature: added proxy storage bypass - added encryption error handling to apiController * chore: fixed include --- client/CMakeLists.txt | 2 ++ client/core/controllers/apiController.cpp | 37 +++++++++++++++++++---- client/core/defs.h | 1 + client/core/errorstrings.cpp | 1 + client/utilities.cpp | 19 ++++++++++++ client/utilities.h | 3 ++ 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2de5db48..2ec4082c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -26,9 +26,11 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") +add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}") +add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}") if(IOS) set(PACKAGES ${PACKAGES} Multimedia) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 5cdaa7ae..96c28a81 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -12,6 +12,7 @@ #include "configurators/wireguard_configurator.h" #include "core/enums/apiEnums.h" #include "version.h" +#include "utilities.h" namespace { @@ -42,8 +43,6 @@ namespace constexpr char keyPayload[] = "key_payload"; } - const QStringList proxyStorageUrl = { "" }; - ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { if (!sslErrors.empty()) { @@ -146,6 +145,15 @@ QStringList ApiController::getProxyUrls() QList sslErrors; QNetworkReply *reply; + QStringList proxyStorageUrl; + if (m_isDevEnvironment) { + proxyStorageUrl = QStringList { DEV_S3_ENDPOINT }; + } else { + proxyStorageUrl = QStringList { PROD_S3_ENDPOINT }; + } + + QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + for (const auto &proxyStorageUrl : proxyStorageUrl) { request.setUrl(proxyStorageUrl); reply = amnApp->manager()->get(request); @@ -166,11 +174,23 @@ QStringList ApiController::getProxyUrls() EVP_PKEY *privateKey = nullptr; QByteArray responseBody; try { - QByteArray key = PROD_PROXY_STORAGE_KEY; - QSimpleCrypto::QRsa rsa; - privateKey = rsa.getPrivateKeyFromByteArray(key, ""); - responseBody = rsa.decrypt(encryptedResponseBody, privateKey, RSA_PKCS1_PADDING); + if (!m_isDevEnvironment) { + QCryptographicHash hash(QCryptographicHash::Sha512); + hash.addData(key); + QByteArray hashResult = hash.result().toHex(); + + QByteArray key = QByteArray::fromHex(hashResult.left(64)); + QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); + + QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); + + QSimpleCrypto::QBlockCipher blockCipher; + responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); + } else { + responseBody = encryptedResponseBody; + } } catch (...) { + Utils::logException(); qCritical() << "error loading private key from environment variables or decrypting payload"; return {}; } @@ -361,6 +381,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co QSimpleCrypto::QRsa rsa; publicKey = rsa.getPublicKeyFromByteArray(rsaKey); } catch (...) { + Utils::logException(); qCritical() << "error loading public key from environment variables"; return ErrorCode::ApiMissingAgwPublicKey; } @@ -370,7 +391,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); } catch (...) { // todo change error handling in QSimpleCrypto? + Utils::logException(); qCritical() << "error when encrypting the request body"; + return ErrorCode::ApiConfigDecryptionError; } QJsonObject requestBody; @@ -416,7 +439,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig); } catch (...) { // todo change error handling in QSimpleCrypto? + Utils::logException(); qCritical() << "error when decrypting the request body"; + return ErrorCode::ApiConfigDecryptionError; } return errorCode; diff --git a/client/core/defs.h b/client/core/defs.h index 802eca45..d00d347b 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -108,6 +108,7 @@ namespace amnezia ApiConfigTimeoutError = 1103, ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, + ApiConfigDecryptionError = 1106, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index c5c0363b..49534606 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -62,6 +62,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break; case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; + case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; diff --git a/client/utilities.cpp b/client/utilities.cpp index 4047365f..731781b5 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -244,3 +244,22 @@ bool Utils::signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent) } #endif + +void Utils::logException(const std::exception &e) +{ + qCritical() << e.what(); + try { + std::rethrow_if_nested(e); + } catch (const std::exception &nested) { + logException(nested); + } catch (...) {} +} + +void Utils::logException(const std::exception_ptr &eptr) +{ + try { + if (eptr) std::rethrow_exception(eptr); + } catch (const std::exception &e) { + logException(e); + } catch (...) {} +} diff --git a/client/utilities.h b/client/utilities.h index 9bf8c82a..2a17a1f4 100644 --- a/client/utilities.h +++ b/client/utilities.h @@ -34,6 +34,9 @@ public: static QString certUtilPath(); static QString tun2socksPath(); + static void logException(const std::exception &e); + static void logException(const std::exception_ptr &eptr = std::current_exception()); + #ifdef Q_OS_WIN static bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent); #endif From 5601bc4fdfcf4cd663c58df57c345bb9a6f8e6d0 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 18 Oct 2024 21:39:09 +0800 Subject: [PATCH 55/90] chore: added new env for workflows --- .github/workflows/deploy.yml | 15 +++++++++++++++ .github/workflows/tag-deploy.yml | 3 +++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f9fb19a5..64a4986d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,7 +16,10 @@ jobs: QT_VERSION: 6.6.2 QIF_VERSION: 4.7 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install Qt' @@ -83,7 +86,10 @@ jobs: QIF_VERSION: 4.7 BUILD_ARCH: 64 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Get sources' @@ -146,7 +152,10 @@ jobs: CC: cc CXX: c++ PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Setup xcode' @@ -238,7 +247,10 @@ jobs: QT_VERSION: 6.4.3 QIF_VERSION: 4.6 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Setup xcode' @@ -304,7 +316,10 @@ jobs: QT_VERSION: 6.7.3 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install desktop Qt' diff --git a/.github/workflows/tag-deploy.yml b/.github/workflows/tag-deploy.yml index dffb3ab1..2bcbd8c6 100644 --- a/.github/workflows/tag-deploy.yml +++ b/.github/workflows/tag-deploy.yml @@ -16,7 +16,10 @@ jobs: QT_VERSION: 6.4.1 QIF_VERSION: 4.5 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install desktop Qt' From 928c4f18c98e6f601b634015ff472be8ecb56fbe Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 22 Oct 2024 22:24:23 +0800 Subject: [PATCH 56/90] chore/using the global network manager --- client/CMakeLists.txt | 1 - client/core/controllers/apiController.cpp | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2ec4082c..05f9f17c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -25,7 +25,6 @@ execute_process( add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") -add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 31a561d8..a7c304f3 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -352,7 +352,6 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co QThread::msleep(10); #endif - QNetworkAccessManager manager; QNetworkRequest request; request.setTransferTimeout(7000); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -410,7 +409,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply *reply = manager.post(request, QJsonDocument(requestBody).toJson()); + QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -425,7 +424,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } for (const QString &proxyUrl : m_proxyUrls) { request.setUrl(QString("%1v1/config").arg(proxyUrl)); - reply = manager.post(request, QJsonDocument(requestBody).toJson()); + reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); From e31a2066c080358be07fe48ffe0b60674db335ae Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 22 Oct 2024 23:05:58 +0800 Subject: [PATCH 57/90] feature/added support tag to PageSetupWizardConfigSource --- .../Pages2/PageSetupWizardConfigSource.qml | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 7f7cf9e1..7c031997 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -49,6 +49,8 @@ PageType { HeaderType { + property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() + Layout.fillWidth: true Layout.topMargin: 24 Layout.rightMargin: 16 @@ -56,7 +58,7 @@ PageType { headerText: qsTr("Connection") - actionButtonImage: PageController.isStartPageVisible() ? "qrc:/images/controls/more-vertical.svg" : "" + actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : "" actionButtonFunction: function() { moreActionsDrawer.open() } @@ -67,18 +69,19 @@ PageType { parent: root anchors.fill: parent - expandedHeight: root.height * 0.35 + expandedHeight: root.height * 0.5 expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 + spacing: 0 HeaderType { Layout.fillWidth: true Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 headerText: qsTr("Settings") } @@ -87,9 +90,12 @@ PageType { id: switcher Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 text: qsTr("Enable logs") + visible: PageController.isStartPageVisible() checked: SettingsController.isLoggingEnabled onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { @@ -98,6 +104,28 @@ PageType { } } + LabelWithButtonType { + id: supportUuid + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Support tag") + descriptionText: SettingsController.getInstallationUuid() + + descriptionOnTop: true + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + + visible: SettingsController.getInstallationUuid() !== "" + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } } } } From 5358aaeb00cd2315d5cc82e5fe1d6ae959867c64 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 22 Oct 2024 23:14:41 +0800 Subject: [PATCH 58/90] chore/displaying route addresses when adding to split tunneling fails --- client/platforms/windows/daemon/wireguardutilswindows.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/platforms/windows/daemon/wireguardutilswindows.cpp b/client/platforms/windows/daemon/wireguardutilswindows.cpp index a68551d7..1a220235 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.cpp +++ b/client/platforms/windows/daemon/wireguardutilswindows.cpp @@ -248,7 +248,7 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { } if (result != NO_ERROR) { logger.error() << "Failed to create route to" - << logger.sensitive(prefix.toString()) + << prefix.toString() << "result:" << result; } return result == NO_ERROR; @@ -265,7 +265,7 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { } if (result != NO_ERROR) { logger.error() << "Failed to delete route to" - << logger.sensitive(prefix.toString()) + << prefix.toString() << "result:" << result; } return result == NO_ERROR; From 92b19eccf6753ce3734b6ba51acaaee6ebeb93cb Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 23 Oct 2024 00:33:22 +0800 Subject: [PATCH 59/90] bugfix/removed adding routes in vpnconnection class for awg and wg protocols --- client/vpnconnection.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index 591e396f..ac881bd7 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -56,14 +56,15 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) { #ifdef AMNEZIA_DESKTOP - QString proto = m_settings->defaultContainerName(m_settings->defaultServerIndex()); + auto container = m_settings->defaultContainer(m_settings->defaultServerIndex()); if (IpcClient::Interface()) { if (state == Vpn::ConnectionState::Connected) { IpcClient::Interface()->resetIpStack(); IpcClient::Interface()->flushDns(); - if (!m_vpnConfiguration.value(config_key::configVersion).toInt()) { + if (!m_vpnConfiguration.value(config_key::configVersion).toInt() && container != DockerContainer::Awg + && container != DockerContainer::WireGuard) { QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); From 923e358aaaf458cfa9b319064326b2ec6c308e25 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 24 Oct 2024 01:02:30 +0800 Subject: [PATCH 60/90] added a check to trigger proxy bypass --- client/core/controllers/apiController.cpp | 56 +++++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 31a561d8..1f8257ee 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -11,8 +11,8 @@ #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" #include "core/enums/apiEnums.h" -#include "version.h" #include "utilities.h" +#include "version.h" namespace { @@ -65,6 +65,28 @@ namespace return ErrorCode::ApiConfigDownloadError; } } + + bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", + const QByteArray &iv = "", const QByteArray &salt = "") + { + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + qDebug() << "Timeout occurred"; + return true; + } else if (responseBody.contains("html")) { + qDebug() << "The response contains an html tag"; + return true; + } else if (checkEncryption) { + try { + QSimpleCrypto::QBlockCipher blockCipher; + static_cast(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt)); + } catch (...) { + qDebug() << "Failed to decrypt the data"; + return true; + } + } + return false; + } } ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent) @@ -320,24 +342,27 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) { + responseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { m_proxyUrls = getProxyUrls(); for (const QString &proxyUrl : m_proxyUrls) { + qDebug() << "Go to the next endpoint"; request.setUrl(QString("%1v1/services").arg(proxyUrl)); + reply->deleteLater(); // delete the previous reply reply = amnApp->manager()->get(request); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError - && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + + responseBody = reply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, responseBody, false)) { break; } - reply->deleteLater(); } } - responseBody = reply->readAll(); auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); return errorCode; @@ -419,32 +444,33 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) { - if (m_proxyUrls.isEmpty()) { - m_proxyUrls = getProxyUrls(); - } + auto encryptedResponseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { + m_proxyUrls = getProxyUrls(); for (const QString &proxyUrl : m_proxyUrls) { + qDebug() << "Go to the next endpoint"; request.setUrl(QString("%1v1/config").arg(proxyUrl)); + reply->deleteLater(); // delete the previous reply reply = manager.post(request, QJsonDocument(requestBody).toJson()); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError - && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + + encryptedResponseBody = reply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, false)) { break; } - reply->deleteLater(); } } auto errorCode = checkErrors(sslErrors, reply); + reply->deleteLater(); if (errorCode) { return errorCode; } - auto encryptedResponseBody = reply->readAll(); - reply->deleteLater(); try { auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig); From d511220f8befbe076f320e94d667000f0e9843dc Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 24 Oct 2024 01:03:54 +0800 Subject: [PATCH 61/90] added a randomized proxy bypass --- client/core/controllers/apiController.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 1f8257ee..dbd621a8 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -1,5 +1,8 @@ #include "apiController.h" +#include +#include + #include #include #include @@ -346,6 +349,9 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { m_proxyUrls = getProxyUrls(); + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator); for (const QString &proxyUrl : m_proxyUrls) { qDebug() << "Go to the next endpoint"; request.setUrl(QString("%1v1/services").arg(proxyUrl)); @@ -448,6 +454,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { m_proxyUrls = getProxyUrls(); + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator); for (const QString &proxyUrl : m_proxyUrls) { qDebug() << "Go to the next endpoint"; request.setUrl(QString("%1v1/config").arg(proxyUrl)); From 4685d3b543b19a7bf4ac1ab81de31c20021c71a7 Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 24 Oct 2024 19:12:53 +0400 Subject: [PATCH 62/90] bugfix/api auth data saving (#1195) * bugfix: fixed authData saving * bugfix: added serviceInfo processing from api response --- client/core/controllers/apiController.cpp | 6 ++++++ client/ui/controllers/installController.cpp | 2 ++ client/ui/models/apiCountryModel.cpp | 4 ++++ client/ui/models/apiCountryModel.h | 3 ++- client/ui/models/servers_model.cpp | 2 +- client/ui/qml/Pages2/PageSettingsApiLanguageList.qml | 2 +- 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index a7cbd40a..193ac481 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -37,6 +37,7 @@ namespace constexpr char userCountryCode[] = "user_country_code"; constexpr char serverCountryCode[] = "server_country_code"; constexpr char serviceType[] = "service_type"; + constexpr char serviceInfo[] = "service_info"; constexpr char aesKey[] = "aes_key"; constexpr char aesIv[] = "aes_iv"; @@ -163,6 +164,11 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap(); map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap()); auto apiConfig = QJsonObject::fromVariantMap(map); + + if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { + apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject()); + } + serverConfig[configKey::apiConfig] = apiConfig; return; diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 4ac0bc32..306e7f38 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -847,6 +847,8 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); newServerConfig.insert(configKey::apiConfig, newApiConfig); + newServerConfig.insert(configKey::authData, authData); + newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { diff --git a/client/ui/models/apiCountryModel.cpp b/client/ui/models/apiCountryModel.cpp index ae58329f..922a9d56 100644 --- a/client/ui/models/apiCountryModel.cpp +++ b/client/ui/models/apiCountryModel.cpp @@ -39,6 +39,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const case CountryNameRole: { return countryInfo.value(configKey::serverCountryName).toString(); } + case CountryImageCodeRole: { + return countryInfo.value(configKey::serverCountryCode).toString().toUpper(); + } } return QVariant(); @@ -76,5 +79,6 @@ QHash ApiCountryModel::roleNames() const QHash roles; roles[CountryNameRole] = "countryName"; roles[CountryCodeRole] = "countryCode"; + roles[CountryImageCodeRole] = "countryImageCode"; return roles; } diff --git a/client/ui/models/apiCountryModel.h b/client/ui/models/apiCountryModel.h index 8789158b..b9e243d0 100644 --- a/client/ui/models/apiCountryModel.h +++ b/client/ui/models/apiCountryModel.h @@ -11,7 +11,8 @@ class ApiCountryModel : public QAbstractListModel public: enum Roles { CountryNameRole = Qt::UserRole + 1, - CountryCodeRole + CountryCodeRole, + CountryImageCodeRole }; explicit ApiCountryModel(QObject *parent = nullptr); diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 85e5dae2..c87499a7 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -771,5 +771,5 @@ const QString ServersModel::getDefaultServerImagePathCollapsed() if (countryCode.isEmpty()) { return ""; } - return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode); + return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode.toUpper()); } diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 234e5142..120313cd 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -90,7 +90,7 @@ PageType { Layout.rightMargin: 32 Layout.alignment: Qt.AlignRight - source: "qrc:/countriesFlags/images/flagKit/" + countryCode + ".svg" + source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" } } From 5065262aac9ebd70f1c8614cb794650d2a9f14e1 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 24 Oct 2024 14:05:26 +0800 Subject: [PATCH 63/90] bugfix: fixed clientInfoDrawer recursive rearrange --- client/ui/qml/Pages2/PageShare.qml | 83 ++++++++++++++---------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 6640df36..617b1091 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -772,7 +772,8 @@ PageType { } } - anchors.fill: parent + width: root.width + height: root.height expandedContent: ColumnLayout { id: expandedContent @@ -783,8 +784,6 @@ PageType { anchors.leftMargin: 16 anchors.rightMargin: 16 - spacing: 8 - onImplicitHeightChanged: { clientInfoDrawer.expandedHeight = expandedContent.implicitHeight + 32 } @@ -797,57 +796,54 @@ PageType { } } - Header2Type { - Layout.fillWidth: true - - headerText: clientName - } - - ColumnLayout - { - id: textColumn - property string textColor: AmneziaStyle.color.mutedGray + Header2TextType { + Layout.maximumWidth: parent.width Layout.bottomMargin: 24 - ParagraphTextType { - color: textColumn.textColor - visible: creationDate - Layout.fillWidth: true + text: clientName + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight + } - text: qsTr("Creation date: %1").arg(creationDate) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: creationDate + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: latestHandshake - Layout.fillWidth: true + text: qsTr("Creation date: %1").arg(creationDate) + } - text: qsTr("Latest handshake: %1").arg(latestHandshake) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: latestHandshake + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: dataReceived - Layout.fillWidth: true + text: qsTr("Latest handshake: %1").arg(latestHandshake) + } - text: qsTr("Data received: %1").arg(dataReceived) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: dataReceived + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: dataSent - Layout.fillWidth: true + text: qsTr("Data received: %1").arg(dataReceived) + } - text: qsTr("Data sent: %1").arg(dataSent) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: dataSent + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: allowedIps - Layout.fillWidth: true + text: qsTr("Data sent: %1").arg(dataSent) + } - text: qsTr("Allowed IPs: %1").arg(allowedIps) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: allowedIps + Layout.fillWidth: true + + text: qsTr("Allowed IPs: %1").arg(allowedIps) } Item { @@ -952,6 +948,7 @@ PageType { BasicButtonType { id: revokeButton Layout.fillWidth: true + Layout.topMargin: 8 defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite From 7261a86c48c80f7c745f0e055fbafcac52c66243 Mon Sep 17 00:00:00 2001 From: albexk Date: Thu, 24 Oct 2024 19:25:44 +0300 Subject: [PATCH 64/90] Bump version to 4.8.2.1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce5777e4..b94e7e73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.2.0 +project(${PROJECT} VERSION 4.8.2.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2067) +set(APP_ANDROID_VERSION_CODE 2068) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 9f3f215452639c698c6756445702320ce8a4c780 Mon Sep 17 00:00:00 2001 From: Aftershock669 Date: Thu, 24 Oct 2024 22:27:53 +0300 Subject: [PATCH 65/90] Update README - add website mirror links - remove direct platform download links - add "Testiny" sponsored badge --- README.md | 12 ++++-------- metadata/img-readme/andr.png | Bin 13621 -> 0 bytes metadata/img-readme/apl.png | Bin 14495 -> 0 bytes metadata/img-readme/download.png | Bin 0 -> 3451 bytes metadata/img-readme/lin.png | Bin 11749 -> 0 bytes metadata/img-readme/mac.png | Bin 9513 -> 0 bytes metadata/img-readme/testiny.png | Bin 0 -> 5313 bytes 7 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 metadata/img-readme/andr.png delete mode 100644 metadata/img-readme/apl.png create mode 100644 metadata/img-readme/download.png delete mode 100644 metadata/img-readme/lin.png delete mode 100644 metadata/img-readme/mac.png create mode 100644 metadata/img-readme/testiny.png diff --git a/README.md b/README.md index e4a6bf0c..eed800f5 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,17 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
    - - - - - -
    - + +[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads) [All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
    + ## Features @@ -37,7 +33,7 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website +- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) - [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) diff --git a/metadata/img-readme/andr.png b/metadata/img-readme/andr.png deleted file mode 100644 index a39cd52f8ac97ba1b3a4bda78cfee2f725ead50a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13621 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 KGBr|W;r|C110n?g diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png deleted file mode 100644 index 6dedfa12ea1d6605b1419bb2c079d64dac02080f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14495 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 diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6a885004aba6dd61392155d4edbe021d4a304d GIT binary patch literal 3451 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 D@^Fny literal 0 HcmV?d00001 diff --git a/metadata/img-readme/lin.png b/metadata/img-readme/lin.png deleted file mode 100644 index 352eae5a3bb23b4f197723bdcd91dc524db2c408..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11749 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 C$Vk=z diff --git a/metadata/img-readme/mac.png b/metadata/img-readme/mac.png deleted file mode 100644 index 2cbb32ae41f1b18c0bb5550671fe56d683fc404d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9513 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 diff --git a/metadata/img-readme/testiny.png b/metadata/img-readme/testiny.png new file mode 100644 index 0000000000000000000000000000000000000000..4f38a3a91c82b2e3a3409e46e7548f93c2c69f6b GIT binary patch literal 5313 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 T$;wY@00000NkvXXu0mjf(4uGS literal 0 HcmV?d00001 From af55af5e760daf00cebe1124d67e9e40d79c9bb3 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 25 Oct 2024 17:48:22 +0800 Subject: [PATCH 66/90] bugfix: fixed proxy bypass encryption check --- client/core/controllers/apiController.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index dbd621a8..075f2cc2 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -399,7 +399,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } apiPayload[configKey::serviceType] = serviceType; apiPayload[configKey::uuid] = installationUuid; - apiPayload[configKey::authData] = authData; + if (!authData.isEmpty()) { + apiPayload[configKey::authData] = authData; + } QSimpleCrypto::QBlockCipher blockCipher; QByteArray key = blockCipher.generatePrivateSalt(32); @@ -452,7 +454,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co auto encryptedResponseBody = reply->readAll(); - if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { m_proxyUrls = getProxyUrls(); std::random_device randomDevice; std::mt19937 generator(randomDevice()); @@ -468,7 +470,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co wait.exec(); encryptedResponseBody = reply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, false)) { + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { break; } } From 4f3bae4a9ae68cc13965633a18d6a072f3d7bd15 Mon Sep 17 00:00:00 2001 From: Aftershock669 Date: Fri, 25 Oct 2024 17:00:28 +0300 Subject: [PATCH 67/90] Fix / Update README --- metadata/img-readme/apl.png | Bin 0 -> 14495 bytes metadata/img-readme/win.png | Bin 11018 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 metadata/img-readme/apl.png delete mode 100644 metadata/img-readme/win.png diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png new file mode 100644 index 0000000000000000000000000000000000000000..6dedfa12ea1d6605b1419bb2c079d64dac02080f GIT binary patch literal 14495 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 literal 0 HcmV?d00001 diff --git a/metadata/img-readme/win.png b/metadata/img-readme/win.png deleted file mode 100644 index 5a35cf490cff7b584ec68df4785dd454b7edfa31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11018 zcmZviWmsEH7p`$F)*{6kT#8F^cc*x8C@#TW8z}BhaHlxM-QAty?heK6_XWV8Rf9MOUgN(K_3=I0G z{~oX~X&IlPjj+zD(qb@`6C_8_4+s{bilQ(ue`3&Ij1ggAzGllxh^l+Qo@F9usL$UH zPTHD?ZeoSYiJVef<02AEeK+X~rbZ>wo%?KqYx5%l@xwH5Dkn@zQ8b?_9EnmTkm(PV zWjG>w-VX^%iW%>vYyXYW^{w+2_lveAcK%lHj_k>m%%}bFbjXI_eTMVou#BA7{kgub ziVFEjMPn9ScphDl#Zrx#?I-9p?h0vx7Gmxc$GZ0I_5Tj1#Wkx8nCk26RW&rIp%=<+ z(K9e4C}r{y3TE-USB_=ya?srwfB~#VBA{#`-x~e^gLW@M^eh4uTuBu)_^qk! z-;8{X`8d&zakndn>+u}Yf{!3rdewae#I0R#og{&km>0*A~~SFR+-* z+K*<|Z~1(y1)b4l8t=1ut-EU}VvviAY{4~+XNUj$TN-8wwAWyA-T}=oA}jKI?&pl7 zDJ&@+zR%EZ!e<)7R?i0%16ew#5{6S5)>_>OBvo(=L9_KXi>`1i&`ac6B@hjuO__Oc z(2>n3seqw9)^O6Hw@=Z*V48UEJU+9s8yUEeTr zK))t$_CC&_tGK!0c|iKOo1m=l1;jkmc(z!TU1DUBj!EaY?y-Xb5%g|o9`m$`(Vscw z={xrLYh70Ht7AeD?A!iyTy|@>;xen4-nJ3_6U_(xPfLsDKIQhP^=@t9{Pj_dHRZlD zWrVLL)$WD#d$Go%SGM7NxIr4RU6bf}>qW<0jKvWwTg@9Tbj&nnS@Zx%P~OeM@Kc+UPu1m{9KWb1NwIr zmAJ*7KQc>2is>|+&ubUEZ8sAM#P)3}iNW8A`g+29??U-+8TsRA?`1QV#1lr8?ybqp2^$cG`M4a+3|(gtzrNR@;`7WH z%z8Z;#JoRuz9&$|2&ih71LHh|NxRm4@AV%ac4Ed|{UW*7ToR559tVZrpeun58rW** zO*_%$O|vDI8@~77HU>*ezw*9Z_DT*%q4|8UiQAzdM`_{vcoP)o8g2HbVhZVO|1D<_ zvC3?yF8u=^^)37mRX!io%n69ZsF$R=Y)a?Nt}kr2Q=V2U(f9KHUqJe12OqzaH#=w?h%D{kL&jxv~q7Q9^gT- zY(DRLf%~Jco($Jj0`+Z0bbW0yA0FDsBKEgJ07c+dbuBI8`*D5uwIsRC;Y1^y4yV15 zQxJlEgdj&qWInZga_VSOQqsOhdTo{1jOUVF>lx84(dnT1CGa=ldYM*L+daXCmPyPo z@;qpCWMM?j%Bln+TeNjEDV!1`^z?fj6eY-IUUaiR&XK~a3}Uk>)<+kBFd_&O%NP}|kO`T^^! z-`$eM-dW>DCBgRdqPq>$eSRT+@sedOF;^DgZl6 z{fv7Z`@QS{Mh)Mev>5~Yfe$Wk+V}p%AmE&sZ{lxHnWP)tX z#H1Cv@wZbbinxy?((kihfG>9X2@Eqx`&hZ61@C~o2N}-GiBpZlVA>srFq|vvM+&Du zV+IXegFeij`*9I_y@&7UWj1C`?~%51^@(B+O9%KXPI=4A%MDgNi;jIb(nG&cKSf>| z5AVbjpLk~w82YG82;7Vz3Co~2)(YOQI`0o2gd-;>Deg%C#REnd1598fD5x$&$;o^Q z*x{8_jBkg_He+bT?!%N{$`D(NI4qKbD&?i6vb(Z4*A%;2ZIU5`6w;^s*&$znvib~J^^cuXIGVn3uO>Vwvk7x>&}2@Rnkd#V6m zX4kmp#zF6Weg!w!YfHA#MOGU$t1P@0nud5X+ftgUUkwa@#>oV{U54yG#3Ox|m?9P6 z?eyW|>Sy&E+pWA=mTi9Y2`h=_g@mroPIoh-4!BW@u_VkU z@8T(7i2a=r&0|7O;j&gv$Uubwo#KxHF66v6|M^`jlNnoQz9DLd3)YUvCmEm7fIuQ8 zl9&M>xGDTJ9kUDF^U5qpjoH{M8ic3#FWS`HHhkSpz~qc5)%8w)Ug8n5O76Q|}{;Si-ag1`+7Hs93+V^!PsRrC9Wnf}2lkMk95H z&*8HL0z+)JVu-$tGzG6hPy>?~&c~!29-G)O?)q(eGbLYNFZ{U!Wd!aV$Gdb<_*}w(;%dxE+w?dY-i> z<;OeiIMbS4^jE{R~nb6|Hlx47Z0wvK#mJ&@CpRRer`}9x9Y3KW! z)XxV;3dTF+Nc$`R%VxC9$D&0;|Msj+2gXnxy-kRULMGq5-u_@FClJ`gLhqUg4(UcH zBWe=xefp>F=k%G|PEWHAXIdKITi5OExdGf??SK|X>G#3ZF)-R-=_dUYB#xg{Wf{K4 zac?gN!rX7pz-l9~XNUI}UT!zDuRlf)@b1Bs$zzrKliAseUk`Z}#P)CML^U_t`-OF* zSNs}21S<<%B-L1F>p?#@L{4A@#)& zQHZm(=Vdo?d(7ENqkU@$j(QzjXSiI%c-v2u<>;oy4V59J>*~Z{OwzQQs~4UM9usi0 zn4KiHJm$R^3BQ_1B5FoU)y0p%Y7`Hd&64H>+=ANJ^+h0{PEa!%)`o-NgTtNR)tloL zOfv7_!<0A{L|aiN*3tQ83(Mbe@^M}ATg|xS0{p4acq|@KW4ZiacL6N zZlQG}UY*(d>sGd&xAb|j;JZs6{Z~{slRYfYGzAJ|Gt4VEi{4Jyt+kj{t%wRLsRi`) zOHDs*>W*kt;`jlVA+wK15UZ-QNwjiO%T_YZ!|;+}1aR5mIG zc|%VoHDY}tM)$Jj$|#F0s~JrtA)!gc1$SWLZYF~UkE?B@81lbQ=^HNt>EUVU0mXMW zKL&!HDF}!`b#Zl^Zm=I6FlR!qmH5xpS-5tcvkM6sm(iA|Wv$$*@z(%Gmfx(2$leoNBLPQ=j9)o=YHW*qxVQ`~gviaX7o5 zh8~1wO%d=~W+?MA_nWq3P{(za(V-JhgbZqB^>tJGG<;WGJvD@Pf+hUIIxnPOww7rQ z56cx%zwGGwSCYFjJKKl>yP($$9Aq8n8mdpeiZ0uf{7_>y`jOyMzcIG6mUm0w9@`*^ zn!Qx7!ciAI7UxlkDaD_jRB*f~Yn@DF_tI+{kA447{?N;{O}*WK$+XRiAP-sZ;@k4?`QIZn_HL#K8_=h}^0n7-C3Wf6gxNUQwh~>~k^%Mt>qQz){Jh)vY4Hw#qo1L8 znPoz+|J0lTLDM?uNCfET6`V`lZH7qj5w|f32N&k^n*$UvYup}eD>^MMR@=V7_C8V% zrt6zE2qeaXDK)t(YVA@#Vv;j{vvmtIl>fXbzs`LwKr4-u`yh>T=wZh0xmWC`zE&7k ztJmxgt=1MGLnZ`%iO#LFt)UerG{Y;Prj2WX;YX+g4y~mk_<#o_5YWv?pTvQ=hkd$< zz!j1q;LqQ+?PaX_DGgE$aNn~fIbH{5`M-G%_jkU(+FMy+dl0XXzx8-eAaZau+I_bf zUqF++Lh}j1s%cQ9BQak?fAC0f8VLD77~dXQ2(j@R55Q*-K~P9wx0eC!O54m*52u89 z=l}vE^y5OYmj?fAe2+>K+bklODPkr-2tV^QMl$jpt4vCxc^L8&YvQO*G4$iLXiww( zv7!zPJ2uzcavSM4e7jCLHCqJz&7xoDlvP%*u1s_o*Wt0|7pkeh%MJ#%Bea7-fHmZUWqtkQ z1$lRl=xRF<^*EIW26sEIR@BKItkZD?de{`=;sRz#>rBq~W^iT6?Iz_lEvZ}O1L0yW^)s=dG+(mQN^qdip%GQ~Pf~Z4R_MlN* zqLofPM?S**-HD)!O0m~{%~k7W1B9HyiH%1zfeQYPZuXg{>Kta!42kYuk3-%uCR;l^ z-EYL#M{1!e2HUxEsgdc{=&IQon0t&V#OdZJ%);!fYYD63EbfrX{sDg?is{0ZToKo1 zt!NOgkXfvdZu!&KOF3-6YFwsk5O8RY#!>C+Vfs%v`|gg!Z~ZdJVlI2m8q4U&pONy- z*w-JUm=D!^zDzB7~P-k?5BXok)B57!$)97Z!h zx_w78@8TVY37LyN??wB)LEsKWeT5I9a7VxPoR zB{>`AGf|Ai%|L?-gES#pDH?R~c~ChaIrMu=r3{nj{Mts{>eNer zZu8~QnCNjg#Tw76-5^RK;ND}MZhjr}8$ zSJX$yb={Md%xrAhr+7@a|KT|;NcY#4p8cw%X18vvEUM3eiX%cfd#0hrAAUzj{l5|k zu4_ra{$1zF>jKTHrb78b#~-Ld0xwD2f1(i=*UMi;>c6@`Ks}u_W63aC-rg>dz}Dy* z{)I?TawL!8g@wg4Gf5N3Y-IB;-{VZPv$YbZ7>t92wwY`~^ z#ji}--Xw(D-fOlb7He#f_uVbN!K{+j*rF)ugTryxi-H6ONtHCIX|xwvez&+C-=iwX zlbDsHMqjus&2y^4slTVyKxjiDyXv*zlUuyLmzF7j^8Dtw^5GX>LWw^~yo2{P@sOi^ z*l}wfonnC{Vu!_nDm?-U8$L(+%w0}3{^L!#Q{mYc&*MY{8rn|m%pjfgKPH1|h>Eh2 zUj$p$kJSoya>Ewn(hD0J7UqZ?ZSua0M9znE#=p&Gzs-@S@zI6HZwpVr)XF- zdfJYbnRV1M8R@EkzND{dE)3#y^Iw6(uXB)(fCETg5fam2gq@Qp~605rL<}{CJi`quaB|v${n5LHnHUAhW($bGaT*)=f;&D+jdqdW?d|7W z-kujM8bfZx$ri7dWfXgvP}QJK&_4>bI{YupamzcT<-n<8uAOC;^{+Lc^X~Te%Z1(~ zl3C`7n?JQ{_ma|dptm&GuywhCmv{3gyuzn_D!SF(WFtj*I~>6MhJU9cd3opCQ+)Dw zbm3CT7LAY2t^`i$#&mMh&{_AJDAjrFo!PHKd!OxkHp z77<0kbdgN{iH=gdC25Gd)g>va^_xEl#axl?YLnvth0+QqB+vAI&E4>ltxk4SdUM0? z8Fb-07r^3}H>uz1mV_N;rB^|B7MABaZPex#w@;gJ-5|z6d%ZLkI#6ph9r?p*_qqf` zPOJ3$VfOv)zO${|;5huiI_<1sSw65To*a6PVZN=rKU?7jO2+mWgwMp$>wxX)r_T=2 zLt9)=Xe=@ugYW$q&+FQr&s%u`J6t+Lh*8zFN*Rak!s{zZ*L5ZXk!<61dP8}+Ab

      +L8K;_5Rl+2J)|0FVzuRpeTta{VBxvdk|D>ej0JNo1kE6@~po*3KH$A64IOI{Z<#*QTmH`Pfmd zNa1Ro;J%Z=hMr-zkTE=)0^dVpJ2~Jqp=o1z51RR_cDFO#*JayC(fKe*!<@#^QgNEb z3wQcnFGh4Y)nrgr>U#a3Pv^l{> zb9i|8rjX_7neJbtIvl7}!Xe%o+aE!I>yydic_LyP&&Y+dZ@;V2nL&MHx#NSBMWfa3JqmvTJM9;*BxHP*_61AIP)O3G}C`#nK* z$(a*>dpjD1R5EDM!1{PM#s?g7g`S^@7d1#EHuiKko|5$(`|;R~x?qM0o^phsGM3W> z??Gz!L{Qg#fowv}BJ^}->#PteD-(Ggw7&rL8%lBHHh%EHEnPWx|Ead-of~z5YAD|b z^<)Eio~icm!T-9+M){(!GxGJ|0PkaZz`Zm6><=OtDJyoZCR;Zu7@bu9X3UF;zy8I)o>9_$RZ-VKf14b7NaIQE2Wp^5OIXav$U(<{WZa=YI<&h z%05xCapQg7vH@>7SCF`f$vq2Wq&S;#M8z)9jxAt}%7?`3Fc-tlgiBnXa@|m7)%UW4 z_4DT!1CK34dd_}^O9X96EBec|r&4S<{3NUmYc)nzd;oQK48pWRzzU8uAn2~sgiZLe z3*PCfpFrNEE57(pn<#a8$E)e6D4hyW{R_etq$)q#v)!;0BfMTtZ;6INcwh_laKi4f zz6kt%Ue((ULCg%E;=?pI&F$tM&K%^6-ihW{N<(8#UjTY6ZZUe8kv^Z+#@u&IsVC30 z$!=z^IrL(D@~1<|vQIiaU8;?;Aqx}7GvW3!(7-FN>*Zn7O(Eh^~s4MHc5%BoBFJP-o8@P(ET z=NRVoHwTj`pOghu(%@HtOgJpVk|DZ>1-_gMIMs3)!N_y!w&MC+NzF*(gzcn?XFf2 zjx>lbK7}9j-b*JfCUQg@WJp$Vg>*RHGu2i4)>^_==laOSgH!K61?W5qT_S%P!|Z-V zVYPDSWHyGOrgr?#Q%I&1oi2)H`1~@)F_~KaMF_rwVQc~=IsQv{SI{;Mij(SA(^SqO z180gUOor14{rZx6Jm8B7b=^dOGf4>SJaTXx3w5${P$y~Qhd>Ot4INz9*m|b3#tJcG zW`L|PZZ=i|i9-a7II>Mrn=t`&aJfc&hL-QGqssnIYW7o~Jth@~eH|)&z-E3A2wA?d zHSVW>2EEqQLQ!DH2h69*Jh<0A^6%fEhUCmYOcd}lqk2laEZ3DheM3E)^SX}VS zY;0#kVK*O@Bs)QF4=8qbKS0k6g5$w&a@oB} zKx8Bq>-PDptf=yeo`a9+#sk@2;-aIm5e3Vtw2vUdQOgHhN81U3S_qGym#qaK;7s?Mrp&2gc z-&)aT(whIevNLQ-_i3tCCcrU>%NVr;PE3tm*8*!Gu&zODtAy0wAOOiP;weiPIBrFHgobg__)Lo-sD%vNtrGw zy_%1@T+I9s{L8wyHJPUHSAxt}F0KiKW~V~#%bwMOx)ObAU(KGG-wgO2dEx&6wF zKhS)G=R`;FKE(8fYt=XnQK9Ya5d`GnJydYhrBHURN-{6yr@wf~xDz&NaTB&W6EvGg zC^>0XvGeBIgpuH*zH2eC*MO*A9(~5YP3Zt?D180`kRybEXju~Rg;8edxG?Z1Dg7-s zsQNQCS7I~3Q30j`(kB@kL2)8()sM~W64&IM*c_p>IM(NOQD{iH1tKo1Oo^OMOSO(iJyRtOIc=7u z}PdPy@r=@JXU`zoSkf+=+r{D<6?={IhbS}_S`U+l@SKzWS>>59n zgQ~6KI*90ZVee0uIIe^?#=4%acVKx^7AE}d%N>dk7gMpa>1d{bTPPStxqK3`vG-@a z+`ypY=amaYhKz(z3oGi!sbQ7IgXu=OG%({?&$m*K8V?(&)fL7OjQtP8oLt>ziiW_A zVrGpmX(;(5Wi&^TA?nbOwK`poy}C@V#hDXEBG129Gs#K56RnxT=_+HUeGd3Eh3{2^ z@3lwMt_V@0=}t)#DN~LV*NkF;v4FSAEpfJa+CUezT=Ei%niL;>7XKx;uS&*n)&!c47WUm2hn+OuuMc77Ve3(KvDzc8_JbYUN2gvg8LQcIpE53IoRN#c zZH&i5{Rg{W+o&B&q(6=r&TP=@sgtT@oCY^%mD8@+;5Q$9YLypLva$L|fz|zT8h46> zL3(yf6DrGIeB-jM%;Ty!j~-mWD)okN##gW}-|$og-)NEGNYu$-0J>#smYI{M=@CF( zTcS9>D_>~{YZ7;rd%RFC3u~2R*LiSDx?9Lsbj7E*3lygpVdzxIrX%*Yhm9MI9#{5D zYg0wR_$ugECTSb)ZB^|yq*r3_(5b$B! zR-94UAj^NHPx$FlKcx=1)TaU4Z_xheXxqJ$7z#krGigRZ0XQ#ncJTPD19LmT&)vuX zP6!#fdu~P2dEjeETHCJ~Y6yf1@zxh!DqcA|!^N;XP@@U>Hja2g{`*Mzo+kT*<}7C^ z!{uQ++{c%JGVWrjP`zWWLZRVIgWUW6gsPX4U~;e3H3=EnJ>~s$Uh%EY#A0VwxLLR4 z1D$Sq^K4@neR@B_LIcZ5f1}JVl5M>QbEMoL+OxHHOoV!I23LB#P4j@hZfPOEmj@*D z!I^+`@)BT5&~mnmZKXvdP?J!H)1hVY_KZWH$(Z2ngMeK`jaA zffl-G0y%_G@d3r(|D>1eF~-9*C!@M848DZ9A6!SG?%sv#39S74n&Qh{m3Sfb_QLk; zk|K{iC*RD3OdrbMFi1+%@DpOr4yZ-FN^ixe2V-s9FJ05l{ z)kAlP_0Kv2?7;835?#_`W2o-Q7+y6pAKYQyHmL5kgh-$VHPH>WlG4OaQgk3GwD)GE zi{4RFE48wat&qIs!Ba46q#1u3RQX6F|XWn z!wTaj7diq(6LOp40wqVQY+Y?lYoBL+J9Tno=|w1&k@*PK8}4STA%xAY<`OIAL-Vyc zrJDPaD;63Qv!%1Gp6oMxM;YY8J|D^7OM?JZgb2U_t3#AGcl6PfKfB_7Kdduh&N<2# z_y6tu5Z&2Eb!F@s!P0&bI&VJ6$}l78N@0em5Adjm#oK;1&#`M2Els4+_`0XJPuY;D zU|u^Yf&1AzKoet5ll*uV>2&kM&yo>20kWbiYv6)EQuEv7!ovKaL;6H2R;@Xzh4u@q81 zY%t-XQ^Hd!g51joIbU^dg7A8QzhxEYh|xu*`C0eiauuI5M>PN;upElQ+Toes+{lU9 z&<$595a0tFPHm)JqE`9SZy6H=xsFU#xh7ZZ%Q70}wVEx9nVtGE^>-~++>-gZPN*ok znNu!!J)gQlR2R^)KfvbIRUBW-vdOomBJc&guOV_H1)7-{#c8WCc`b;8(Oi^r8Y?hMG9bkF}(CI#Et<~thYCcdJYo0Ae&V-g4t0#h zTqO!b`;E8}o+|LlTv+2d>6B2)Yg#Ku*q~^^YZM`uki9`vaNAq_$_DG_0cYAg)VLt+ z4U6-c->v*;CJ~7!E>~SGg+DAK6A%oh`F@&=)ZuSFQMvNW^ z*O9akMA5t5`TgukZ)zdd>FT_r0Tp~;6r4CL)O%wKX*?EpklIDj~x%EB{|1idq8VaHK7cRFY@9iO5&zxlb z|Bn>IgCc75UoW9d*%>aL2b2SIlS9=w2Ighi1El_g&K`1d`O=hJM?dNW#<4(@0W$wJ z@)+Z2YHl92hj!KKfVO&q{x1M$;JJsFzr%T%3jhs64R;y`v&ZYbZcO(7cs0iz>xT?p zXPQKbul{3D-)am4>U35A532i=7QLC(9WK41h>syeEElY*>$TnYAC;F^$5aUg5O>0I z^NP=?Z4msH>#T}!Ni>e3$OTgZtup@qpg57{qd|bb-qXJ$N)J;CFn3xO$ny*2hF{A4 zJk(P;P+JZ>)A}9CHqsjm!~pDdm{qYlOH1XleD9WQ&-T3}GrXZ-rTn;6%P2OuF!L`I zR!J!Jtc3Qi%JX~PAL@LjjX}n_Ruuk&Dxs$abwNe@?}dQ4;@`rEFINXsp+=XR-9-r= z?{BZr(MVd6qGZj4n#~l*vYU1t+J~XD`kop&HJ`BVvPQTg}1&sVEsCl$-lTN zH4Ahr`tKjY$7d$rJ!GqN`A29zo+e39Cc%Vtx3p{&i1B3^N(sd@3eln1uk z0+V*H_x>0JquecJE(+Iit$JsGb6x|=8m3|5`_v&yuI;_U>N%1x9HOy-q39}%tfZ1e JrI=B`{{h}5>cap4 From 9e71e64cbdc514e04878d33aa4568e8a130090de Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 25 Oct 2024 19:19:28 +0400 Subject: [PATCH 68/90] chore: bump version to 4.8.2.3 (#1203) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b94e7e73..420a7eb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.2.1 +project(${PROJECT} VERSION 4.8.2.3 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) From e7b25719e44271dd445bb8a7ab3644d6dfcc1024 Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 25 Oct 2024 19:23:05 +0300 Subject: [PATCH 69/90] Chore/bump version (#1204) * chore: bump Android version code --------- Co-authored-by: Nethius --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 420a7eb2..b5e64e32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2068) +set(APP_ANDROID_VERSION_CODE 2069) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 1533270e4e875d52ce0989f10c43c144a32cf306 Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 2 Nov 2024 00:54:24 +0300 Subject: [PATCH 70/90] Fix connection check for AWG/WG --- .../vpn/protocol/wireguard/Wireguard.kt | 85 +++++++++++-------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index e93834f4..80cab96d 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,11 +1,12 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder -import java.io.IOException -import java.util.Locale +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED @@ -27,6 +28,8 @@ open class Wireguard : Protocol() { private var tunnelHandle: Int = -1 protected open val ifName: String = "amn0" + private lateinit var scope: CoroutineScope + private var statusJob: Job? = null override val statistics: Statistics get() { @@ -49,46 +52,17 @@ open class Wireguard : Protocol() { override fun internalInit() { if (!isInitialized) loadSharedLibrary(context, "wg-go") + if (this::scope.isInitialized) { + scope.cancel() + } + scope = CoroutineScope(Dispatchers.IO) } override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) - val startTime = System.currentTimeMillis() start(wireguardConfig, vpnBuilder, protect) - waitForConnection(startTime) - state.value = CONNECTED } - private suspend fun waitForConnection(startTime: Long) { - Log.d(TAG, "Waiting for connection") - withContext(Dispatchers.IO) { - val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0) - try { - delay(1000) - var log = getLogcat(time) - Log.v(TAG, "First waiting log: $log") - // check that there is a connection log, - // to avoid infinite connection - if (!log.contains("Attaching to interface")) { - Log.w(TAG, "Logs do not contain a connection log") - return@withContext - } - while (!log.contains("Received handshake response")) { - delay(1000) - log = getLogcat(time) - } - } catch (e: IOException) { - Log.e(TAG, "Failed to get logcat: $e") - } - } - } - - private fun getLogcat(time: String): String = - ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time) - .redirectErrorStream(true) - .start() - .inputStream.reader().readText() - protected open fun parseConfig(config: JSONObject): WireguardConfig { val configData = config.getJSONObject("wireguard_config_data") return WireguardConfig.build { @@ -178,6 +152,43 @@ open class Wireguard : Protocol() { tunnelHandle = -1 throw VpnStartException("Protect VPN interface: permission not granted or revoked") } + launchStatusJob() + } + + private fun launchStatusJob() { + Log.d(TAG, "Launch status job") + statusJob = scope.launch { + while (true) { + val lastHandshake = getLastHandshake() + Log.v(TAG, "lastHandshake=$lastHandshake") + if (lastHandshake == 0L) { + delay(1000) + continue + } + if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED + else if (lastHandshake == -1L) state.value = DISCONNECTED + statusJob = null + break + } + } + } + + private fun getLastHandshake(): Long { + if (tunnelHandle == -1) { + Log.e(TAG, "Trying to get config of a non-existent tunnel") + return -1 + } + val config = GoBackend.awgGetConfig(tunnelHandle) + if (config == null) { + Log.e(TAG, "Failed to get tunnel config") + return -2 + } + val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong() + if (lastHandshake == null) { + Log.e(TAG, "Failed to get last_handshake_time_sec") + return -2 + } + return lastHandshake } override fun stopVpn() { @@ -185,6 +196,8 @@ open class Wireguard : Protocol() { Log.w(TAG, "Tunnel already down") return } + statusJob?.cancel() + statusJob = null val handleToClose = tunnelHandle tunnelHandle = -1 GoBackend.awgTurnOff(handleToClose) From 576e2226fe2742d14b326d9609f55a19dd02dcd1 Mon Sep 17 00:00:00 2001 From: albexk Date: Sun, 3 Nov 2024 16:11:23 +0300 Subject: [PATCH 71/90] fix(android): add CHANGE_NETWORK_STATE permission for all Android versions --- client/android/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 179def86..9e44e022 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -20,7 +20,7 @@ - + From 31867993ce5a77732b95c5e1717c6d53395f59b5 Mon Sep 17 00:00:00 2001 From: Nethius Date: Wed, 6 Nov 2024 09:57:39 +0400 Subject: [PATCH 72/90] chore: minor fixes (#1235) --- client/ui/controllers/installController.cpp | 1 - client/ui/qml/Controls2/BusyIndicatorType.qml | 2 +- client/ui/qml/Controls2/CardType.qml | 2 +- client/ui/qml/Controls2/DrawerType2.qml | 2 +- client/ui/qml/Controls2/PopupType.qml | 2 +- client/ui/qml/Controls2/TopCloseButtonType.qml | 2 +- client/ui/qml/Modules/Style/AmneziaStyle.qml | 4 ++++ client/ui/qml/Pages2/PageHome.qml | 4 ++-- client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 8 ++++---- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 306e7f38..ae0804cb 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -848,7 +848,6 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, authData); - newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { diff --git a/client/ui/qml/Controls2/BusyIndicatorType.qml b/client/ui/qml/Controls2/BusyIndicatorType.qml index 55af280f..480f25c1 100644 --- a/client/ui/qml/Controls2/BusyIndicatorType.qml +++ b/client/ui/qml/Controls2/BusyIndicatorType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Controls2/CardType.qml b/client/ui/qml/Controls2/CardType.qml index 50f84dbf..f584a8fc 100644 --- a/client/ui/qml/Controls2/CardType.qml +++ b/client/ui/qml/Controls2/CardType.qml @@ -19,7 +19,7 @@ RadioButton { property string textColor: AmneziaStyle.color.midnightBlack - property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3) + property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot property string selectedBorderColor: AmneziaStyle.color.goldenApricot property string defaultBodredColor: AmneziaStyle.color.transparent property int borderWidth: 0 diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index 6647bc88..c4b584c1 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -92,7 +92,7 @@ Item { id: background anchors.fill: parent - color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack Behavior on color { PropertyAnimation { duration: 200 } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index bd4aa4fb..7a6a770e 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -24,7 +24,7 @@ Popup { Overlay.modal: Rectangle { visible: root.closeButtonVisible - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } onOpened: { diff --git a/client/ui/qml/Controls2/TopCloseButtonType.qml b/client/ui/qml/Controls2/TopCloseButtonType.qml index 1bd7fef6..3a652da6 100644 --- a/client/ui/qml/Controls2/TopCloseButtonType.qml +++ b/client/ui/qml/Controls2/TopCloseButtonType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index c0038246..1abfbe3a 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -22,5 +22,9 @@ QtObject { readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12) readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08) readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05) + readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8) + readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3) + readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8) + readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8074337a..5689e4d4 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -316,8 +316,8 @@ PageType { rootButtonImageColor: AmneziaStyle.color.midnightBlack rootButtonBackgroundColor: AmneziaStyle.color.paleGray - rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) - rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) + rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray + rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray rootButtonHoveredBorderColor: AmneziaStyle.color.transparent rootButtonDefaultBorderColor: AmneziaStyle.color.transparent rootButtonTextTopMargin: 8 diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index f23e36d9..2d6c1d9b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -132,8 +132,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Reload API config") @@ -172,8 +172,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Remove from application") From 23806e1defcf827cec2f7727f432329eb5c0399c Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 8 Nov 2024 11:22:16 +0300 Subject: [PATCH 73/90] chore: bump version to 4.8.2.4 (#1240) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b5e64e32..cb695631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.2.3 +project(${PROJECT} VERSION 4.8.2.4 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2069) +set(APP_ANDROID_VERSION_CODE 2071) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From aa871bd1c99a693cd88e8cd635bc7d1bf69f75db Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 12 Nov 2024 10:22:34 +0400 Subject: [PATCH 74/90] feature: added country selection on home page drawer (#1215) --- .../qml/Components/ShareConnectionDrawer.qml | 6 +-- client/ui/qml/Controls2/BasicButtonType.qml | 22 ++++++---- .../qml/Controls2/TextFieldWithHeaderType.qml | 2 +- client/ui/qml/Pages2/PageHome.qml | 41 ++++++++++++++----- client/ui/qml/Pages2/PageShare.qml | 2 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 2 +- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 3235ad0a..d2bf28ab 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -84,7 +84,7 @@ DrawerType2 { Layout.topMargin: 16 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" KeyNavigation.tab: copyConfigTextButton @@ -120,7 +120,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } Keys.onEnterPressed: { copyConfigTextButton.clicked() } @@ -143,7 +143,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy config string") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" KeyNavigation.tab: showSettingsButton } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 5c599013..828c32bc 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -22,9 +22,10 @@ Button { property int borderWidth: 0 property int borderFocusedWidth: 1 - property string imageSource + property string leftImageSource property string rightImageSource - property string leftImageColor: textColor + property string leftImageColor + property bool changeLeftImageSize: true property bool squareLeftSide: false @@ -127,18 +128,23 @@ Button { anchors.centerIn: parent Image { - Layout.preferredHeight: 20 - Layout.preferredWidth: 20 - - source: root.imageSource - visible: root.imageSource === "" ? false : true + id: leftImage + source: root.leftImageSource + visible: root.leftImageSource === "" ? false : true layer { - enabled: true + enabled: leftImageColor !== "" ? true : false effect: ColorOverlay { color: leftImageColor } } + + Component.onCompleted: { + if (root.changeLeftImageSize) { + leftImage.Layout.preferredHeight = 20 + leftImage.Layout.preferredWidth = 20 + } + } } ButtonTextType { diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 4ec0976b..365faa94 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -183,7 +183,7 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText - imageSource: root.buttonImageSource + leftImageSource: root.buttonImageSource anchors.top: content.top anchors.bottom: content.bottom diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 5689e4d4..8422a10f 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -98,7 +98,6 @@ PageType { pressedColor: AmneziaStyle.color.sheerWhite disabledColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.mutedGray - leftImageColor: AmneziaStyle.color.transparent borderWidth: 0 buttonTextLabel.lineHeight: 20 @@ -110,7 +109,7 @@ PageType { text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") - imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() @@ -166,6 +165,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right + spacing: 0 Component.onCompleted: { drawer.collapsedHeight = collapsed.implicitHeight @@ -267,18 +267,39 @@ PageType { RowLayout { Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44 + Layout.topMargin: 8 + Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16 spacing: 0 - Image { - Layout.rightMargin: 8 - visible: source !== "" - source: ServersModel.defaultServerImagePathCollapsed - } + BasicButtonType { + enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsed + hoverEnabled: enabled + + implicitHeight: 36 + + leftPadding: 16 + rightPadding: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.transparent + textColor: AmneziaStyle.color.mutedGray + + buttonTextLabel.lineHeight: 16 + buttonTextLabel.font.pixelSize: 13 + buttonTextLabel.font.weight: 400 - LabelTextType { - id: collapsedServerMenuDescription text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded + leftImageSource: ServersModel.defaultServerImagePathCollapsed + changeLeftImageSize: false + + rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : "" + + onClicked: { + ServersModel.processedIndex = ServersModel.defaultIndex + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 617b1091..995fa3e7 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -573,7 +573,7 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 2a565230..404ba563 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -135,7 +135,7 @@ PageType { Layout.topMargin: 40 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) From 8547de82ea940cfb3220bc6c013c9ed4eb66dd39 Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 14 Nov 2024 07:58:04 +0400 Subject: [PATCH 75/90] bump xcode-version for macos build (#1249) --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 64a4986d..0ce8d576 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -256,7 +256,7 @@ jobs: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.3.1' + xcode-version: '15.4.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v3 From e0b091b47424b87ac980c9e7e2bcd80bd6e0e476 Mon Sep 17 00:00:00 2001 From: Aftershock669 <58403826+Aftershock669@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:51:46 +0300 Subject: [PATCH 76/90] Update readme (#1267) --- README.md | 19 ++++++++++--------- metadata/img-readme/apl.png | Bin 14495 -> 0 bytes metadata/img-readme/download-alt.svg | 8 ++++++++ metadata/img-readme/download-website.svg | 8 ++++++++ metadata/img-readme/download.png | Bin 3451 -> 0 bytes metadata/img-readme/play.png | Bin 15126 -> 0 bytes 6 files changed, 26 insertions(+), 9 deletions(-) delete mode 100644 metadata/img-readme/apl.png create mode 100644 metadata/img-readme/download-alt.svg create mode 100644 metadata/img-readme/download-website.svg delete mode 100644 metadata/img-readme/download.png delete mode 100644 metadata/img-readme/play.png diff --git a/README.md b/README.md index eed800f5..27a29edf 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,21 @@ [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) -Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. -![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png) +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -
      +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) - - - +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). -[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads) + + [All releases](https://github.com/amnezia-vpn/amnezia-client/releases) -
      +
      @@ -33,7 +33,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [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://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) diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png deleted file mode 100644 index 6dedfa12ea1d6605b1419bb2c079d64dac02080f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14495 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 diff --git a/metadata/img-readme/download-alt.svg b/metadata/img-readme/download-alt.svg new file mode 100644 index 00000000..f97c9c3d --- /dev/null +++ b/metadata/img-readme/download-alt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website.svg b/metadata/img-readme/download-website.svg new file mode 100644 index 00000000..d0cf8375 --- /dev/null +++ b/metadata/img-readme/download-website.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png deleted file mode 100644 index 0e6a885004aba6dd61392155d4edbe021d4a304d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3451 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 D@^Fny diff --git a/metadata/img-readme/play.png b/metadata/img-readme/play.png deleted file mode 100644 index 2fb316c8d0edd9b8767f460417bf7cbe469c682a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15126 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 b?l Date: Tue, 26 Nov 2024 11:41:17 +0700 Subject: [PATCH 77/90] bugfix: added scroll on page with services list (#1262) * added scroll on page with services list * fixed margins on PageSetupWizardApiServicesList --- client/ui/qml/Controls2/CardWithIconsType.qml | 1 + .../Pages2/PageSetupWizardApiServicesList.qml | 119 +++++++++--------- .../Pages2/PageSetupWizardConfigSource.qml | 1 - 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index fea65116..18a29b87 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -145,6 +145,7 @@ Button { cursorShape: Qt.PointingHandCursor hoverEnabled: true + enabled: root.enabled onEntered: { backgroundRect.color = root.hoveredColor diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index 85a50393..f726cd49 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -16,83 +16,82 @@ PageType { defaultActiveFocusItem: focusItem - FlickableType { - id: fl + ColumnLayout { + id: header + anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.height + anchors.left: parent.left + anchors.right: parent.right - ColumnLayout { - id: content + spacing: 0 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Item { + id: focusItem + KeyNavigation.tab: backButton + } - spacing: 0 - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - - BackButtonType { - id: backButton - Layout.topMargin: 20 + BackButtonType { + id: backButton + Layout.topMargin: 20 // KeyNavigation.tab: fileButton.rightButton - } + } - HeaderType { - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 32 + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - headerText: qsTr("VPN by Amnezia") - descriptionText: qsTr("Choose a VPN service that suits your needs.") - } + headerText: qsTr("VPN by Amnezia") + descriptionText: qsTr("Choose a VPN service that suits your needs.") + } + } - ListView { - id: containers - width: parent.width - height: containers.contentItem.height - spacing: 16 + ListView { + id: servicesListView + anchors.top: header.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 16 + spacing: 0 - currentIndex: 1 - interactive: false - model: ApiServicesModel + currentIndex: 1 + clip: true + model: ApiServicesModel - delegate: Item { - implicitWidth: containers.width - implicitHeight: delegateContent.implicitHeight + ScrollBar.vertical: ScrollBar {} - ColumnLayout { - id: delegateContent + delegate: Item { + implicitWidth: servicesListView.width + implicitHeight: delegateContent.implicitHeight - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ColumnLayout { + id: delegateContent - CardWithIconsType { - id: card + anchors.fill: parent - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 + CardWithIconsType { + id: card - headerText: name - bodyText: cardDescription - footerText: price + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - rightImageSource: "qrc:/images/controls/chevron-right.svg" + headerText: name + bodyText: cardDescription + footerText: price - onClicked: { - if (isServiceAvailable) { - ApiServicesModel.setServiceIndex(index) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) - } - } + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + enabled: isServiceAvailable + + onClicked: { + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 7c031997..f973c89c 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -47,7 +47,6 @@ PageType { KeyNavigation.tab: textKey.textField } - HeaderType { property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() From 1d721ffb9abb4c0c5a123f33edb5c066a6eee40b Mon Sep 17 00:00:00 2001 From: Anton Sosnin Date: Wed, 27 Nov 2024 04:55:23 +0200 Subject: [PATCH 78/90] SteamDeck/OS installation fix (#1270) --- deploy/data/linux/post_install.sh | 10 ++++++++++ deploy/data/linux/post_uninstall.sh | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/deploy/data/linux/post_install.sh b/deploy/data/linux/post_install.sh index b3345bac..324462d9 100755 --- a/deploy/data/linux/post_install.sh +++ b/deploy/data/linux/post_install.sh @@ -19,6 +19,11 @@ date > $LOG_FILE echo "Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + if sudo systemctl is-active --quiet $APP_NAME; then sudo systemctl stop $APP_NAME >> $LOG_FILE sudo systemctl disable $APP_NAME >> $LOG_FILE @@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE echo "user desktop creation loop ended" >> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh index 5849a90e..98090d20 100755 --- a/deploy/data/linux/post_uninstall.sh +++ b/deploy/data/linux/post_uninstall.sh @@ -13,6 +13,11 @@ date >> $LOG_FILE echo "Uninstall Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do sudo unlink $dir >> $LOG_FILE done @@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then fi +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service after uninstall status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE From 9d96b1cd13ac1e6a06a0f413ceca9d187f839c71 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 29 Nov 2024 22:10:35 +0000 Subject: [PATCH 79/90] Update Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27a29edf..8b453907 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
      USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
      USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
      -XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 - +XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
      +TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns ## Acknowledgments This project is tested with BrowserStack. From 4efaf20a1ca5e88c137b6116eeba4a6765f48804 Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 2 Dec 2024 10:46:20 +0300 Subject: [PATCH 80/90] chore: fix deploy workflow (#1280) --- .github/workflows/deploy.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ce8d576..a51c19b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -217,7 +217,11 @@ jobs: export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin" export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos" export PATH=$PATH:~/go/bin - sh deploy/build_ios.sh + sh deploy/build_ios.sh | \ + sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d' env: IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }} IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }} From 5dc16c06f158660ee011ea5cdb46005c1121d5a2 Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 3 Dec 2024 08:47:33 +0300 Subject: [PATCH 81/90] chore: increased the api request timeout (#1276) --- client/core/controllers/apiController.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 75a3f93c..c50165e7 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -50,6 +50,8 @@ namespace constexpr char authData[] = "auth_data"; } + const int requestTimeoutMsecs = 12 * 1000; // 12 secs + ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { if (!sslErrors.empty()) { @@ -177,7 +179,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle QStringList ApiController::getProxyUrls() { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; @@ -280,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c if (serverConfig.value(config_key::configVersion).toInt()) { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); @@ -336,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); @@ -390,7 +392,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint)); From 1c1e74d06f6e6738a1976d2c3af7bf9b6f1e2286 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 6 Dec 2024 12:40:04 +0000 Subject: [PATCH 82/90] ru readme --- README_RU.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 README_RU.md diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 00000000..8b453907 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,191 @@ +# Amnezia VPN +## _The best client for self-hosted VPN_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. + +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) + +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) + +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). + + + + +[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) + +
      + + + +## Features + +- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. +- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. +- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). +- Windows, MacOS, Linux, Android, iOS releases. +- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). + +## 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://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 + +## Tech + +AmneziaVPN uses several open-source projects to work: + +- [OpenSSL](https://www.openssl.org/) +- [OpenVPN](https://openvpn.net/) +- [Shadowsocks](https://shadowsocks.org/) +- [Qt](https://www.qt.io/) +- [LibSsh](https://libssh.org) - forked from Qt Creator +- and more... + +## Checking out the source code + +Make sure to pull all submodules after checking out the repo. + +```bash +git submodule update --init --recursive +``` + +## Development + +Want to contribute? Welcome! + +### Help with translations + +Download the most actual translation files. + +Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. +Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". + +Unzip this file. +Each *.ts file contains strings for one corresponding language. + +Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. +You can do it via a web-interface or any other method you're familiar with. + +### Building sources and deployment + +Check deploy folder for build scripts. + +### How to build an iOS app from source code on MacOS + +1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. + +2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: + - MacOS + - iOS + - Qt 5 Compatibility Module + - Qt Shader Tools + - Additional Libraries: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + +3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) + +4. You also need to install go >= v1.16. If you don't have it installed already, +download go from the [official website](https://golang.org/dl/) or use Homebrew. +The latest version is recommended. Install gomobile +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Build the project +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment + + +If you get `gomobile: command not found` make sure to set PATH to the location +of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Open the XCode project. You can then run /test/archive/ship the app. + +If the build fails with the following error +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with +key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +if the above error persists on your M1 Mac, then most probably you need to install arch based CMake +``` +arch -arm64 brew install cmake +``` + +Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that +require them. In this case, simply restart the build. + +## How to build the Android app + +_Tested on Mac OS_ + +The Android app has the following requirements: +* JDK 11 +* Android platform SDK 33 +* CMake 3.25.0 + +After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. + +- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. +- Set path to JDK 11 +- Set path to Android SDK (`$ANDROID_HOME`) + +In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  +Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  +Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. + +That's it! You should be ready to compile the project from QT Creator! + +### Development flow + +After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. +If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! + +You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. + +## License + +GPL v3.0 + +## Donate + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
      +USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
      +USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
      +XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
      +TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns +## Acknowledgments + +This project is tested with BrowserStack. +We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. From ea910ba30054d5a88a9bfc62f35b04e1ece2986f Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Dec 2024 22:15:01 +0200 Subject: [PATCH 83/90] Update README_RU.md --- README_RU.md | 181 +++++++++------------------------------------------ 1 file changed, 30 insertions(+), 151 deletions(-) diff --git a/README_RU.md b/README_RU.md index 8b453907..6ebdb97f 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,182 +1,60 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ +## _Лучший клиент для создания VPN на собственном сервере_ -[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) -[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) - -[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) +### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting) > [!TIP] -> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). +> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). -[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
      -## Features +## Особенности -- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. -- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. -- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. -- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). -- Windows, MacOS, Linux, Android, iOS releases. -- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). +- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN. +- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2. +- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них. +- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS. +- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). -## 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) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация - [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://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\) -## Tech +## Технологии -AmneziaVPN uses several open-source projects to work: +AmneziaVPN использует несколько проектов с открытым исходным кодом: - [OpenSSL](https://www.openssl.org/) - [OpenVPN](https://openvpn.net/) - [Shadowsocks](https://shadowsocks.org/) - [Qt](https://www.qt.io/) -- [LibSsh](https://libssh.org) - forked from Qt Creator -- and more... +- [LibSsh](https://libssh.org) +- и другие... -## Checking out the source code - -Make sure to pull all submodules after checking out the repo. - -```bash -git submodule update --init --recursive -``` - -## Development - -Want to contribute? Welcome! - -### Help with translations - -Download the most actual translation files. - -Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. -Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". - -Unzip this file. -Each *.ts file contains strings for one corresponding language. - -Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. -You can do it via a web-interface or any other method you're familiar with. - -### Building sources and deployment - -Check deploy folder for build scripts. - -### How to build an iOS app from source code on MacOS - -1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. - -2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: - - MacOS - - iOS - - Qt 5 Compatibility Module - - Qt Shader Tools - - Additional Libraries: - - Qt Image Formats - - Qt Multimedia - - Qt Remote Objects - -3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) - -4. You also need to install go >= v1.16. If you don't have it installed already, -download go from the [official website](https://golang.org/dl/) or use Homebrew. -The latest version is recommended. Install gomobile -```bash -export PATH=$PATH:~/go/bin -go install golang.org/x/mobile/cmd/gomobile@latest -gomobile init -``` - -5. Build the project -```bash -export QT_BIN_DIR="/Qt//ios/bin" -export QT_MACOS_ROOT_DIR="/Qt//macos" -export QT_IOS_BIN=$QT_BIN_DIR -export PATH=$PATH:~/go/bin -mkdir build-ios -$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -``` -Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment - - -If you get `gomobile: command not found` make sure to set PATH to the location -of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. -```bash -export PATH=$(PATH):/path/to/GOPATH/bin -``` - -6. Open the XCode project. You can then run /test/archive/ship the app. - -If the build fails with the following error -``` -make: *** -[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] -Error 1 -``` -Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with -key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. - -if the above error persists on your M1 Mac, then most probably you need to install arch based CMake -``` -arch -arm64 brew install cmake -``` - -Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that -require them. In this case, simply restart the build. - -## How to build the Android app - -_Tested on Mac OS_ - -The Android app has the following requirements: -* JDK 11 -* Android platform SDK 33 -* CMake 3.25.0 - -After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. - -- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. -- Set path to JDK 11 -- Set path to Android SDK (`$ANDROID_HOME`) - -In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  -Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  -Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. - -That's it! You should be ready to compile the project from QT Creator! - -### Development flow - -After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. -If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! - -You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. - -## License +## Лицензия GPL v3.0 -## Donate +## Донаты Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) @@ -185,7 +63,8 @@ USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
      USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
      XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
      TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns -## Acknowledgments -This project is tested with BrowserStack. -We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. From 569d63ef0f750f9938dfdda2fc69c9559a7be4c8 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 7 Dec 2024 15:53:40 +0200 Subject: [PATCH 84/90] Add files via upload --- metadata/img-readme/download-website-ru.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 metadata/img-readme/download-website-ru.svg diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 00000000..386ae4fe --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + From d67201ede9dc2f39b525ec55e04ac0225e551462 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:34:18 +0200 Subject: [PATCH 85/90] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b453907..8f887808 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ + +### _The best client for self-hosted VPN_ + [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) +### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md) + + [Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) From c5aa070bf4cd7ff1de931dc22a887aea3104ae92 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:49:26 +0200 Subject: [PATCH 86/90] Update README_RU.md --- README_RU.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README_RU.md b/README_RU.md index 6ebdb97f..fe9dd286 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,6 +1,11 @@ # Amnezia VPN -## _Лучший клиент для создания VPN на собственном сервере_ +### _Лучший клиент для создания VPN на собственном сервере_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский [AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) @@ -10,8 +15,8 @@ > [!TIP] > Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). - - + + [Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) From 6ea6ab1bd983fd2be880e9e14a9184bda9b79349 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 8 Dec 2024 08:14:22 +0300 Subject: [PATCH 87/90] chore: added clang-format config files (#1293) --- .clang-format | 39 +++++++++++++++++++++++++++++++++++++++ .clang-format-ignore | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-format-ignore diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..5c459fd2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,39 @@ +BasedOnStyle: WebKit +AccessModifierOffset: '-4' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortEnumsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: None +AlwaysBreakTemplateDeclarations: 'No' +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakConstructorInitializers: BeforeColon +ColumnLimit: '120' +CommentPragmas: '"^!|^:"' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '8' +IndentPPDirectives: BeforeHash +NamespaceIndentation: All +PenaltyExcessCharacter: '10' +PointerAlignment: Right +SortIncludes: 'true' +SpaceAfterTemplateKeyword: 'false' +Standard: Auto diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 00000000..4019357f --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,20 @@ +/client/3rd +/client/3rd-prebuild +/client/android +/client/cmake +/client/core/serialization +/client/daemon +/client/fonts +/client/images +/client/ios +/client/mozilla +/client/platforms/dummy +/client/platforms/linux +/client/platforms/macos +/client/platforms/windows +/client/server_scripts +/client/translations +/deploy +/docs +/metadata +/service/src From 2db99715b1fc5a7ef1f8b800c72d6e5b3422ce1f Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Dec 2024 09:32:49 +0300 Subject: [PATCH 88/90] feature: added subscription expiration date for premium v2 (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: added subscription expiration date for premium v2 * feature: added a check for the presence of the “services” field in the response body of the getServicesList() function * feature: added prohibition to change location when connection is active * bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend --- client/core/controllers/apiController.cpp | 7 + client/core/defs.h | 1 + client/core/errorstrings.cpp | 3 +- .../ui/controllers/connectionController.cpp | 2 +- client/ui/models/apiServicesModel.cpp | 114 ++++++--- client/ui/models/apiServicesModel.h | 40 ++- client/ui/models/servers_model.cpp | 33 ++- client/ui/models/servers_model.h | 5 + .../Pages2/PageSettingsApiLanguageList.qml | 6 + .../qml/Pages2/PageSettingsApiServerInfo.qml | 7 +- .../ui/qml/Pages2/PageSettingsServerInfo.qml | 227 +++++++++--------- 11 files changed, 285 insertions(+), 160 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index c50165e7..6562632a 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -379,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); + + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + return ErrorCode::ApiServicesMissingError; + } + } + return errorCode; } diff --git a/client/core/defs.h b/client/core/defs.h index d00d347b..c0db2e12 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 49534606..70f433c6 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -63,7 +63,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; - + case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f8516f6e..f9491d4e 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -55,7 +55,7 @@ void ConnectionController::openConnection() && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by end_date event"; + qDebug() << "attempt to update api config by expires_at event"; if (configVersion == ApiConfigSources::Telegram) { emit updateApiConfigFromTelegram(); } else { diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 2a87bde3..81a10f87 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -27,6 +27,9 @@ namespace constexpr char storeEndpoint[] = "store_endpoint"; constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; } namespace serviceType @@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject service = m_services.at(index.row()).toObject(); - QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); - auto serviceType = service.value(configKey::serviceType).toString(); + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; switch (role) { case NameRole: { - return serviceInfo.value(configKey::name).toString(); + return apiServiceData.serviceInfo.name; } case CardDescriptionRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); + auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); } else if (serviceType == serviceType::amneziaFree){ QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { description += tr("

      Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); } return description; @@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case IsServiceAvailableRole: { if (serviceType == serviceType::amneziaFree) { - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { return false; } } return true; } case SpeedRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); - return tr("%1 MBit/s").arg(speed); + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); } - case WorkPeriodRole: { - auto timelimit = serviceInfo.value(configKey::timelimit).toString(); - if (timelimit == "0") { + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { return ""; } - return tr("%1 days").arg(timelimit); + return tr("%1 days").arg(timeLimit); } case RegionRole: { - return serviceInfo.value(configKey::region).toString(); + return apiServiceData.serviceInfo.region; } case FeaturesRole: { if (serviceType == serviceType::amneziaPremium) { @@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } } case PriceRole: { - auto price = serviceInfo.value(configKey::price).toString(); + auto price = apiServiceData.serviceInfo.price; if (price == "free") { return tr("Free"); } return tr("%1 $/month").arg(price); } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } } return QVariant(); @@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data) { beginResetModel(); - m_countryCode = data.value(configKey::userCountryCode).toString(); - m_services = data.value(configKey::services).toArray(); - if (m_services.isEmpty()) { - QJsonObject service; - service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo)); - service.insert(configKey::serviceType, data.value(configKey::serviceType)); + m_services.clear(); - m_services.push_back(service); + m_countryCode = data.value(configKey::userCountryCode).toString(); + auto services = data.value(configKey::services).toArray(); + + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + m_services.push_back(getApiServicesData(service.toObject())); + } } endResetModel(); @@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index) QJsonObject ApiServicesModel::getSelectedServiceInfo() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceInfo).toObject(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; } QString ApiServicesModel::getSelectedServiceType() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceType).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.type; } QString ApiServicesModel::getSelectedServiceProtocol() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceProtocol).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; } QString ApiServicesModel::getSelectedServiceName() { - auto modelIndex = index(m_selectedServiceIndex, 0); - return data(modelIndex, ApiServicesModel::Roles::NameRole).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; } QJsonArray ApiServicesModel::getSelectedServiceCountries() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::availableCountries).toArray(); + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; } QString ApiServicesModel::getCountryCode() @@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode() QString ApiServicesModel::getStoreEndpoint() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::storeEndpoint).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; } QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) @@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; - roles[WorkPeriodRole] = "workPeriod"; + roles[TimeLimitRole] = "timeLimit"; roles[RegionRole] = "region"; roles[FeaturesRole] = "features"; roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; return roles; } + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString(); + + if (serviceInfo.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 49918940..c96a49ab 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -3,6 +3,7 @@ #include #include +#include class ApiServicesModel : public QAbstractListModel { @@ -15,10 +16,11 @@ public: ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ protected: QHash roleNames() const override; private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + QString m_countryCode; - QJsonArray m_services; + QVector m_services; int m_selectedServiceIndex; }; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index c87499a7..b72b10c3 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -22,7 +22,7 @@ namespace constexpr char serviceProtocol[] = "service_protocol"; constexpr char publicKeyInfo[] = "public_key"; - constexpr char endDate[] = "end_date"; + constexpr char expiresAt[] = "expires_at"; } } @@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); - const QString endDate = publicKeyInfo.value(configKey::endDate).toString(); - if (endDate.isEmpty()) { - publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); serverConfig.insert(configKey::apiConfig, apiConfig); editServer(serverConfig, serverIndex); @@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) return false; } - auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC(); - if (endDateDateTime < QDateTime::currentDateTimeUtc()) { + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 0f18ea30..78bc22cc 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -115,6 +116,7 @@ public slots: QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); @@ -127,6 +129,9 @@ protected: signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 120313cd..600db85d 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -54,8 +54,14 @@ PageType { imageSource: "qrc:/images/controls/download.svg" checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + if (index !== ApiCountryModel.currentIndex) { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 2d6c1d9b..167e56e5 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -56,12 +56,15 @@ PageType { } LabelWithImageType { + property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable") + Layout.fillWidth: true Layout.margins: 16 imageSource: "qrc:/images/controls/history.svg" - leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period") + rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate") + : ApiServicesModel.getSelectedServiceData("workPeriod") visible: rightText !== "" } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 95ae5c8a..ffcfb441 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -25,6 +25,8 @@ PageType { property int pageSettingsApiServerInfo: 3 property int pageSettingsApiLanguageList: 4 + property var processedServer + defaultActiveFocusItem: focusItem Connections { @@ -35,8 +37,18 @@ PageType { } } + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -44,147 +56,139 @@ PageType { value: true } ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } Item { id: focusItem - KeyNavigation.tab: header + //KeyNavigation.tab: header } ColumnLayout { anchors.fill: parent - spacing: 16 + spacing: 4 - Repeater { - id: header - model: proxyServersModel + BackButtonType { + id: backButton - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() + Layout.topMargin: 20 + KeyNavigation.tab: headerContent.actionButton + + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } + + HeaderType { + id: headerContent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" + : "qrc:/images/controls/edit-3.svg" + + headerText: root.processedServer.name + descriptionText: { + if (root.processedServer.isServerFromGatewayApi) { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate") + } else { + return ApiServicesModel.getSelectedServiceData("serviceDescription") + } + } else if (root.processedServer.isServerFromTelegramApi) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName + } } - delegate: ColumnLayout { + KeyNavigation.tab: tabBar - property alias focusItem: backButton + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.open() + } + } + } - id: content + DrawerType2 { + id: serverNameEditDrawer - Layout.topMargin: 20 + parent: root - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton + anchors.fill: parent + expandedHeight: root.height * 0.35 - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } + onClosed: { + if (!GC.isMobile()) { + headerContent.actionButton.forceActiveFocus() + } + } + + expandedContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Connections { + target: serverNameEditDrawer + enabled: !GC.isMobile() + function onOpened() { + serverName.textField.forceActiveFocus() } } - HeaderType { - id: headerContent + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } + + TextFieldWithHeaderType { + id: serverName + Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) { - return serverDescription - } else if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar - - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + KeyNavigation.tab: saveButton } - DrawerType2 { - id: serverNameEditDrawer + BasicButtonType { + id: saveButton - parent: root + Layout.fillWidth: true - anchors.fill: parent - expandedHeight: root.height * 0.35 + text: qsTr("Save") + KeyNavigation.tab: focusItem1 - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Connections { - target: serverNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - serverName.textField.forceActiveFocus() - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return } - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } - - TextFieldWithHeaderType { - id: serverName - - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true - - KeyNavigation.tab: saveButton - } - - BasicButtonType { - id: saveButton - - Layout.fillWidth: true - - text: qsTr("Save") - KeyNavigation.tab: focusItem1 - - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } - - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } + if (serverName.textFieldText !== root.processedServer.name) { + ServersModel.setProcessedServerData("name", serverName.textFieldText); } + serverNameEditDrawer.close() } } } @@ -257,8 +261,7 @@ PageType { StackLayout { id: nestedStackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + Layout.fillWidth: true currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? From d06924c59dd8684c28b6257efe5d1a11db34b19b Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Tue, 10 Dec 2024 03:17:16 +0100 Subject: [PATCH 89/90] feature/xray user management (#972) * feature: implement client management functionality for Xray --------- Co-authored-by: aiamnezia Co-authored-by: vladimir.kuznetsov --- client/configurators/xray_configurator.cpp | 165 +++++++++- client/configurators/xray_configurator.h | 4 + client/ui/controllers/exportController.cpp | 9 +- client/ui/controllers/exportController.h | 2 +- client/ui/models/clientManagementModel.cpp | 353 +++++++++++++++++++-- client/ui/models/clientManagementModel.h | 6 + client/ui/qml/Pages2/PageShare.qml | 2 +- 7 files changed, 495 insertions(+), 46 deletions(-) diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c..514aa821 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -3,38 +3,169 @@ #include #include #include +#include +#include "logger.h" #include "containers/containers_defs.h" #include "core/controllers/serverController.h" #include "core/scripts_registry.h" +namespace { +Logger logger("XrayConfigurator"); +} + XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent) : ConfiguratorBase(settings, serverController, parent) { } -QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode &errorCode) +QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) { - QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), - m_serverController->genVarsForScript(credentials, container, containerConfig)); - - QString xrayPublicKey = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - xrayPublicKey.replace("\n", ""); - - QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode); - xrayUuid.replace("\n", ""); - - QString xrayShortId = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); - xrayShortId.replace("\n", ""); - + // Generate new UUID for client + QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Get current server config + QString currentConfig = m_serverController->getTextFileFromContainer( + container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to get server config file"; return ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + // Parse current config as JSON + QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + logger.error() << "Failed to parse server config JSON"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject serverConfig = doc.object(); + + // Validate server config structure + if (!serverConfig.contains("inbounds")) { + logger.error() << "Server config missing 'inbounds' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray inbounds = serverConfig["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Server config has empty 'inbounds' array"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Inbound missing 'settings' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Settings missing 'clients' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray clients = settings["clients"].toArray(); + + // Create configuration for new client + QJsonObject clientConfig { + {"id", clientId}, + {"flow", "xtls-rprx-vision"} + }; + + clients.append(clientConfig); + + // Update config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + serverConfig["inbounds"] = inbounds; + + // Save updated config to server + QString updatedConfig = QJsonDocument(serverConfig).toJson(); + errorCode = m_serverController->uploadTextFileToContainer( + container, + credentials, + updatedConfig, + amnezia::protocols::xray::serverConfigPath, + libssh::ScpOverwriteMode::ScpOverwriteExisting + ); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to upload updated config"; + return ""; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + errorCode = m_serverController->runScript( + credentials, + m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container)) + ); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to restart container"; + return ""; + } + + return clientId; +} + +QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) +{ + // Get client ID from prepareServerConfig + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode); + if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { + logger.error() << "Failed to prepare server config"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), + m_serverController->genVarsForScript(credentials, container, containerConfig)); + + if (config.isEmpty()) { + logger.error() << "Failed to get config template"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString xrayPublicKey = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayPublicKey.replace("\n", ""); + + QString xrayShortId = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayShortId.replace("\n", ""); + + // Validate all required variables are present + if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { + logger.error() << "Config template missing required variables:" + << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") + << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") + << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); + errorCode = ErrorCode::InternalError; + return ""; + } + + config.replace("$XRAY_CLIENT_ID", xrayClientId); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_SHORT_ID", xrayShortId); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 2acfdf71..8ed4e775 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ public: QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2690b5b1..8681406e 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index b031ea39..a2c9fcfa 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -28,7 +28,7 @@ public slots: void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7445d60f..f07eae71 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + + return appendClient(clientId, clientName, container, credentials, serverController); } ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, @@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + break; } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + ErrorCode error = ErrorCode::NoError; + + // Get server config + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + // Get client ID to remove + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + // Remove client from server config + QJsonObject configObj = serverConfig.object(); + if (!configObj.contains("inbounds")) { + logger.error() << "Missing inbounds in xray config"; + return ErrorCode::InternalError; + } + + QJsonArray inbounds = configObj["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Empty inbounds array in xray config"; + return ErrorCode::InternalError; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings"; + return ErrorCode::InternalError; + } + + QJsonArray clients = settings["clients"].toArray(); + if (clients.isEmpty()) { + logger.error() << "Empty clients array in xray config"; + return ErrorCode::InternalError; + } + + for (int i = 0; i < clients.size(); ++i) { + QJsonObject clientObj = clients[i].toObject(); + if (clientObj.contains("id") && clientObj["id"].toString() == clientId) { + clients.removeAt(i); + break; + } + } + + // Update server config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + configObj["inbounds"] = inbounds; + + // Upload updated config + error = serverController->uploadTextFileToContainer( + container, + credentials, + QJsonDocument(configObj).toJson(), + serverConfigPath + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload updated xray config"; + return error; + } + + // Remove from local table + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + // Update clients table file on server + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable") + .arg(ContainerProps::containerTypeToString(container)); + + error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file"; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + error = serverController->runScript( + credentials, + serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container)) + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to restart xray container"; + return error; + } + + return error; +} + QHash ClientManagementModel::roleNames() const { QHash roles; @@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const roles[DataSentRole] = "dataSent"; roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 60132abe..989120a9 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -40,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -64,11 +66,15 @@ private: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 995fa3e7..d6ce7848 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -92,7 +92,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray" From 367789bda28f33a11a72d61b222453c5b502299e Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 14 Dec 2024 14:29:33 +0200 Subject: [PATCH 90/90] Update README_RU.md (#1300) * Update README_RU.md --- README_RU.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/README_RU.md b/README_RU.md index fe9dd286..59518f4b 100644 --- a/README_RU.md +++ b/README_RU.md @@ -55,6 +55,112 @@ AmneziaVPN использует несколько проектов с откр - [LibSsh](https://libssh.org) - и другие... +## Проверка исходного кода +После клонирования репозитория обязательно загрузите все подмодули. + +```bash +git submodule update --init --recursive +``` + + +## Разработка +Хотите внести свой вклад? Добро пожаловать! + +### Помощь с переводами + +Загрузите самые актуальные файлы перевода. + +Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations". + +Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка. + +Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом. + +### Сборка исходного кода и деплой +Проверьте папку deploy для скриптов сборки. + +### Как собрать iOS-приложение из исходного кода на MacOS +1. Убедитесь, что у вас установлен XCode версии 14 или выше. +2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули: +- MacOS +- iOS +- Модуль совместимости с Qt 5 +- Qt Shader Tools +- Дополнительные библиотеки: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + + +3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь. +4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile: + +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Соберите проект: +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Замените и на ваши значения. + +Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile: +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение. + +Если сборка завершится с ошибкой: +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM: +``` +arch -arm64 brew install cmake +``` + + При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку. + + +## Как собрать Android-приложение +Сборка тестировалась на MacOS. Требования: +- JDK 11 +- Android SDK 33 +- CMake 3.25.0 + +Установите QT, QT Creator и Android Studio. +Настройте QT Creator: + +- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`. +- Укажите путь к JDK 11. +- Укажите путь к Android SDK (`$ANDROID_HOME`) + +Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере! + +Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake. + +Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK. + +### Разработка Android-компонентов + +После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt__Clang_-`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt__Clang_-/client/android-build` в качестве корневой. +Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения. +Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`/client/android-build/.`). + + ## Лицензия GPL v3.0