Compare commits

..

85 Commits

Author SHA1 Message Date
vkamn
7d4f3e0f50 fix: various fixes (#3015)
* fix: fixed ios brandings

* chore: bump version

* chore: update translations

* fix: fixed ios awg3.1 parser

* chore: bump version

* fix: remove awg 3.1 params from native config if disbaled

* chore: bump version

* chore: remove comment
2026-08-21 22:43:01 +08:00
NickVs2015
b1a37b3779 feat: add brandings (#2839)
* feat: move amnezia hardcode from sources to cmake

* feat: move amnezia hardcode from sources to cmake

* refactor: remove prem v1 migration

* chore: move more branding

* fix: use host-matching architecture for iOS simulator builds

x86_64 was hardcoded; Apple Silicon Macs run arm64 simulators and Xcode 26
has no Rosetta simulator destinations, so simulators showed as unsupported.

* fix: mark the branded media assets catalog as a bundle resource

MACOSX_PACKAGE_LOCATION was set on the upstream Media.xcassets path while
target_sources uses CLIENT_IOS_MEDIA_ASSETS_PATH, so a branded catalog was
never compiled by actool and the app got a blank icon.

* fix: parametrize the Swift app-group identifier used by Log.swift

Log.swift hardcoded Amnezia's own app-group instead of the branded one,
since Swift -D flags can't carry the string value like the ObjC++ side
does. Generate a small Swift constants file instead, same pattern as
version.h.in, wired into all four targets that compile Log.swift.

* fix: use APPLICATION_NAME instead of hardcoded Amnezia in log line format

Every log line printed "Amnezia" regardless of branding; the log file name
two lines above this already used APPLICATION_NAME correctly.

* fix: restore branding fixes dropped by rebase, rename swiftInterop to swiftBridge

* fix: move more branding variables to cmake

* feat: move macOS branding from sources to cmake

- macos/app/Info.plist.in: app group, camera usage text, UTI ids and
  descriptions now come from CLIENT_APPLICATION_NAME / BUILD_* variables,
  mirroring ios/app/Info.plist.in
- macos/networkextension/Info.plist.in: bundle names and app group ids are
  templated; the plist is configured by CMake into the binary dir because
  Xcode only expands its own $(...) settings
- new knobs: CLIENT_MACOS_MEDIA_ASSETS_PATH, CLIENT_MACOS_APP_ICNS_PATH,
  CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER(_DEBUG),
  CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER(_DEBUG)

* fix: resolve macOS pf template paths relative to the client source dir

CMAKE_SOURCE_DIR points at the wrapper project when the client is built as a
subproject, so the pf template lookup failed there.

* chore: separate brandings defaults

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-08-18 20:41:45 +08:00
vkamn
86ae77e3d0 fix: treat the “off” toggle as a “missing” parameter (#3003) 2026-08-18 17:42:38 +08:00
vkamn
541a6e07d6 fix: return qml translations (#3002) 2026-08-18 16:48:56 +08:00
vkamn
f135400d3b chore: update translations (#2999)
* chore: update translations

* feat: add Korean translation (#2971)

* update(i18n): add Korean translation

* fix: add full QML catalog for Korean

---------

Co-authored-by: ParkSnoopy <ParkSnoopy@users.noreply.github.com>

* feat: add Spanish translation (#2035)

* feat: add spanish translation

* feat: add spanish translations to client

* fix: fix grammar

---------

Co-authored-by: vkamn <vk@amnezia.org>

* chore: update es and kr translations

---------

Co-authored-by: ParkSnoopy <117149837+ParkSnoopy@users.noreply.github.com>
Co-authored-by: ParkSnoopy <ParkSnoopy@users.noreply.github.com>
Co-authored-by: Stageddat <121111148+Stageddat@users.noreply.github.com>
2026-08-18 15:17:29 +08:00
Kushida
da184c0ddf fix: config import parsing and VPN route edge cases (#2820)
* fix: harden config import parsing

* fix: handle vpn route edge cases
2026-08-17 18:22:01 +08:00
vkamn
4116c5c4bc refactor: now iap and test purchase flags ships from backend (#2995) 2026-08-17 13:46:47 +08:00
yp
667d98f7d7 fix: remove dead code & save config (#2975) 2026-08-14 13:56:44 +08:00
vkamn
44c10b39e3 feat: awg3.1 support (#2984)
* feat: add awg3.1 support

* fix: add warning for big native awg qr codes

* fix: show default subnet address if not exist in server config

* fix: use range keep alive only with awg3 params

* chore: update awg go versions in conan

* chore: update awg-go version
2026-08-14 13:55:56 +08:00
vkamn
e4b7e09f86 feat: add distribution filed to api calls (#2983)
* feat: add distribution filed to api calls

* chore: remove debug log
2026-08-14 13:55:40 +08:00
NickVs2015
04c2b2af33 chore: bump android version code to 2148 (#2977) 2026-08-14 13:04:10 +08:00
NickVs2015
e643fa008c fix: ipc input validation (#2852)
* fix: resolve critical IPC security vulnerabilities

- Validate IP/CIDR values from IPC before passing to Linux firewall
- Replace shell interpolation with direct execve in firewall update functions
- Block dangerous OpenVPN/WireGuard arguments in sanitizeArguments()
- Add programId bounds check in IpcServerProcess::setProgram()
- Add SO_PEERCRED peer authentication for IPC connections on Linux

* fix: extend IPC security validation to macOS firewall

* fix: suppress linux firewall errors and add missing 400.allowPIA anchor

* fix: review changes

* fix: review changes next

* fix: remove UID check from IPC server

* fix: IPC security review fixes

* fix: address IPC review fixes
2026-08-13 14:24:54 +08:00
vkamn
b575df05c7 fix: fixed adding subnet to ip split tunnel (#2973) 2026-08-11 00:19:42 +08:00
vkamn
85bd102efa fix: return vcredist to windows bundle (#2949) 2026-08-11 00:18:21 +08:00
Маэстро автоматизации
717323f138 fix: restore ios condition for paste menu item to avoid clipboard permission prompt (#2955) 2026-08-10 18:46:55 +08:00
vkamn
8ed7a5a1d3 fix: android icons (#2953) 2026-08-10 18:44:49 +08:00
NickVs2015
6ece4e0ac5 chore: bump android version code to 2144 (#2961) 2026-08-10 12:10:52 +08:00
vkamn
9d45f3d7ad feat: add long country code support (#2969) 2026-08-10 12:10:16 +08:00
MrMirDan
dcf53b989e chore: update ru translations (#2928)
* update translation RU

* fix: some text fixes
2026-08-07 00:14:27 +08:00
vkamn
e27f74e4fc chore: add warning on home page (#2948) 2026-08-06 22:41:20 +08:00
vkamn
dbe1a06e35 feat: pass uuid, appversion, osversion, app language to all gateway requests (#2944) 2026-08-06 22:08:34 +08:00
NickVs2015
d251d5f0ac chore: add Android armv7/arm64-v8a build on Qt 6.10.1 (#2892)
* chore: add Android armv7/arm64-v8a build on Qt 6.10.1

* chore: split Qt 6.10.1/6.10.3 Android AAB by min/max SDK range

* chore: bump version to 2141

* chore: drop upper SDK bound on Android 11+ AAB

* fix: stage android package source snapshot after libxray.aar is copied in

* chore: align Build-Android APK/AAB versionCode and naming to android11+

* chore: add x86/x86_64 APK builds to Qt 6.10.1 Android job

* chore: simplify android deploy workflow

* chore: add Qt 6.10.3 android9-10 APK build as fallback

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-08-06 20:34:00 +08:00
yp
d702dc0b2a fix: various fixes for Telemt, XRay (VLESS), Captcha (#2903)
* fix worker & fix captha

* fixed read config (xxd) snapshot

* remove comment

* fix snapshot

* fixed xray save

* move subtitleText -> hint (Xray)

* xray config apply via reinstall

* apply server config via container reinstall

* transport compatibility and xray-core v26 config

* seed reality defaults on fresh deploy

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

* placeholder defaults and hint tooltip control

* wire placeholders and hints into settings pages

* gate flow and security by transport in UI

* encode raw tg link in QR

* remove comment

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

* remove drop SessionKey, add edit

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

* feat: add awg3 support to selfhosted

* chore: remove HeaderProtectionKey from ui

* fix: fixed regexp in configure_container.sh for awg

* chore: return server protocol version to config

* fix: fixed native wg obfuscation

* fix: fixed range validator for awg text field

* feat: add warning for awg 1/2

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

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

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

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

* chore: return error messages

---------

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

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

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

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

* feat: replace runpath for prebuilt openvpn

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

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

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

* fix: fix scrolling on user list

* fix: fixed container installation after adding a new server

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

* chore: add fixme annotation

---------

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

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

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

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

* fix: return default sni to googletagmanager

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

* chore: update xray

* chore: update awg-apple checksum

* chore: update awg-windows checksum
2026-07-20 15:38:02 +08:00
Yaroslav Gurov
e96a2b171c fix: get rid of empty i1-i5 params (#2846) 2026-07-20 10:44:07 +08:00
vkamn
06d219b92b chore: update xcode version in deploy.yml (#2838) 2026-07-16 23:22:22 +08:00
yp
28c77e570b fix: captcha keyboard overlap (#2827) 2026-07-15 14:11:44 +08:00
yp
6691d2e21c feat: add captha to update service (#2817)
* Feat: add Captha update service

* fixed fill captchaInfo from response
2026-07-15 14:11:27 +08:00
vkamn
c7b5ee1867 chore: minor fixes (#2833)
* fix: always reinstall xray after settings changes

* chore: bump version

* fix: return blocking clear profile
2026-07-15 14:09:35 +08:00
vkamn
e4127f47f3 chore: merge with upstream (#2826)
* feat: add cryptoUtils

* feat: move protocol selection to home page

* feat: add connection warmup
2026-07-13 14:26:00 +08:00
NickVs2015
4b4563818e fix: app icon IOS not appearing after installation (#2822) 2026-07-13 12:59:32 +08:00
communism420
dbb43272e6 chore: fix typo in traffic obfuscation protocols list (#2823) 2026-07-13 12:46:07 +08:00
yyy-amnezia
5e70eb20c9 feat: build macOS prebuilts for arm64 and x86_64 (#2601)
* Build macOS prebuilts for arm64 and x86_64

* chore: remove parallel thingy from CI/CD

* feat: rework amnezia-xray-bindings for multiarch

* Fix macOS universal recipe builds

* Sign macOS OpenVPN helper script

* Address macOS build review comments

* Isolate awg-go universal builds

* Disable Xcode signing for non-NE macOS

* Address remaining macOS recipe review comments

* feat(conan): rework go-based recipes to support Apple's multiarch

* fix(conan): amnezia-xray-bindings naming conventions

* chore(ci/cd): specify macos versions explicitly

* fix(conan): use escaped path for make in windows

* fix(ci/cd): checkout prior using of provisioning profiles

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-07-09 13:37:46 +08:00
NickVs2015
15176cd3ae chore: up GP version code (#2819) 2026-07-09 13:33:52 +08:00
NickVs2015
25df36ad73 fix: filepicker atv dev (#2763)
* fix: support filepicker android 9

* fix: double call filepicker

* fix: remove double connectionUiControllerProtected
2026-07-08 17:34:45 +08:00
MrMirDan
ea8e3fda56 chore: base client test dependency (#2790) 2026-07-08 13:58:57 +08:00
yp
31ae949ba0 fix: UI Telemt|MTProxy QA fixed (#2734)
* fixed UI 'Transport mode' 'Share QR Code'

* fix FakeTLS domain input & port space & workers

* fix Public host|Port|Promoted tag|FakeTLS domain|Internal/External IP

* fixed diagnostic mtproxy/telemt

* fixed SshSession parent

* fixed timeout & add message & fix disabled telemt|mtproxy

* fixed telemt=mtproxy algoritm

* fixed add/delete additional secret

* fix Additional secret

* remove comment

* fixed mtproxy update secret

* checking the parallelism of lines

* remove old code & & ref code

* chore: minor codestyle fixes

* chore: minor codestyle fixes

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-07-08 13:50:13 +08:00
NickVs2015
a17847a90b fix: add connectionUiControllerProtected accessor for tests (#2809) 2026-07-07 20:14:45 +08:00
cd-amn
2ca497cd06 fix: openvpn and openssl conan install (#2799) 2026-07-07 17:01:07 +08:00
vkamn
bd003b149d chore: updates from proxy-storage-cache branch (#2773)
* chore: updates from proxy-storage-cache branch

* fix: fixed tap controller paths
2026-07-06 14:47:32 +08:00
NickVs2015
35c1014a44 chore: up version code and compatibility with checks (#2806)
* chore: up version code

* fix: add compatibility with checks
2026-07-06 14:42:02 +08:00
NickVs2015
27857cde8a fix: error 3rdparty and compile (#2789)
* fix: add binary dir to add_subdirectory calls in 3rdparty.cmake

* fix: use list(APPEND LIBS) instead of target_link_libraries for gamepad on Android

* fix: add core skippltgormController
2026-07-06 13:25:06 +08:00
ULTRAVIOLENC3
b39f583097 fix: create DNS WFP sublayer for app split tunneling (#2768) 2026-07-02 20:15:30 +08:00
ULTRAVIOLENC3
6c7b65cac6 fix windows app split tunnel path conversion (#2691) 2026-07-02 20:12:52 +08:00
vkamn
0f6847219b chore: bump version (#2772) 2026-06-26 13:04:36 +08:00
yp
5d16645b84 fix: android icons (#2725)
* update icons android

* remove comment
2026-06-24 00:08:21 +08:00
Yaroslav Gurov
203a092dc9 fix: blobs for macos-ne and codesigning of qt blobs (#2754) 2026-06-24 00:07:42 +08:00
yp
d8b8590bc4 fix: XRay validation audit (#2749)
* flow default and config fixes

* host/SNI/path validation backend + flow-default flag

* input validation, numeric limits and live Save on settings pages

* MinMaxRowType clamping + DropDownType fit-content drawer
2026-06-24 00:07:26 +08:00
yp
9b8bfaa6f8 fix: regression testing vs 4.8.15.4 (#2730)
* fixed revoke

* fixed async update xray/mtproxy/telemt

* fixed connect premium config

* fixed autostart app hide

* fixed clear profile

* (6) fixed xtls-rprx-vision→empty

* (7) fixed appendClient abort & fix restore admin

* (8) fixed async|clientsUpdated

* fixed increment name server N

* remove comment & reset file

* chore: add tr to nextAvailableServerName

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-23 23:05:58 +08:00
Yaroslav Gurov
890103a16a fix: update amneziawg (#2743)
* chore(conan): update amneziawg

* fix(conan): use cmake 4.2+ to support MSVC26

* fix(ci/cd): use the latest cmake generator available on windows
2026-06-17 19:56:53 +07:00
yp
56ab82f87f fix: Use shared OpenSSL on Android (#2736) 2026-06-16 10:57:32 +07:00
lunardunno
3984acbb44 feat: updating install_docker.sh script (#2661)
* Updating install_docker.sh script

Implementing a Docker service status check.
The Docker reinstall step has been removed due to the implementation of Docker service checking.
Implementing locale checking and assignment.
Implementation of execution of some actions through commands with sudo, to reduce delays caused by differences in the values ​​of the PATH variable for the root user and the user included in the sudo group.
Implementation of a verification step for the install containerization app to avoid installing unsupported podman-docker applications.

* adding message handling to install controller

Adding handling for "Containerization app is not supported" and "Service status not active" messages to the controller.

* Error Codes added

Error Codes added for ServerContainerizationNotSupported & DockerServiceNotActive

* Adding extended descriptions of new errors

* fix last line in errorCodes.h

* fix last line in errorStrings.cpp

* Changing the names of errors

* various changes in the script

The messages output for processing by the server controller have been changed: "Container runtime is not supported" and "Container runtime service is not running."
The redundant check and output of the "Packet manager not found" message, as well as the interruption of script execution, have been eliminated, as this situation is handled by the server controller at an earlier stage (check_server_is_busy.sh) and only there.
Added installation of the whish package if it is missing from the OS, for subsequent re-execution of the install_docker.sh and check_server_is_busy.sh scripts.
Implemented an alternative method for detecting the package manager if the whish package is initially missing from the OS.
The algorithm for setting the $pm variable (package manager) has been changed.

* processed phrases have been changed

The phrases processed by the server controller have been changed.

* Attempting to use "command -v"

Switching to using "command -v" instead of "which".

* "which" as main, "command" as backup.

* "which" as main, "command" as backup for check user

* which  LOCK_CMD with sudo

Run the "which" with sudo to check the $LOCK_CMD variable in case the user's PATH variable has incorrect values ​​if the user is not root and is only a member of the sudo group.

* suppressing sudo password prompt

* suppressing sudo password prompt

* suppressing sudo password prompt install_docker.sh

* Changing the phrase for check stdout

"sudo:" with "not found" instead of "command not found"

* Changing phrases for check stdout check_user_in_sudo.sh‎

* sudo|docker and not found, in one line

* check only sudoers
2026-06-15 22:28:38 +07:00
yp
cc404378f9 fix: remove only amnezia- prefixed docker volumes (#2728) 2026-06-15 13:12:19 +07:00
yp
594635e5cf fix: script remove docker volume (#2686)
* move sudo docker volume rm -f

* fix: remove unnecessary function

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-04 22:58:39 +08:00
vkamn
f9b106cf5b fix: various fixes (#2693)
* fix: fixed country model update

* fix: fixed context menu crush on ios

* fix: fixed passphrase dialog freeze

* fix: fixed country switch

* fix: fixed start minimized

* fix: fixed black screen after remove container

* refactor: return cloak and ss only for view

* fix: fixed default server change after improt while connected

* fix: divider visibility

* fix: fixed revoke admin user

* fix: fixed language restore after backup

* fix: link hover for tor settings page

* fix: fixed openvpn connecntion status

* fix: fixed free color status

* fix: fixed client config update

* chore: bump version
2026-06-04 22:45:53 +08:00
yp
a9861d18b7 fix: wrong index on xray pages (#2669)
* test crash xray

* fixed save config xray

* reset file

* fixed text port & reset file

* fixed textFieldWithHeaderType.textField
2026-06-01 12:22:54 +08:00
lunardunno
c14138f031 fix: deleting volumes when cleaning the server (#2673)
* Deleting volumes when cleaning the server

* force the remove volumes
2026-06-01 11:54:34 +08:00
yyy-amnezia
60686fde24 fix: link OpenVPNAdapter statically (#2645)
* fix(ios): link OpenVPNAdapter statically

* chore(conan): simplify openvpnadapter merge process and make everything via XCrun

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-05-30 13:59:35 +08:00
Yaroslav Gurov
bd0747296e fix: networkextension proper framework linking (#2668) 2026-05-28 23:09:49 +08:00
Yaroslav Gurov
ba61019a50 fix: enable bundled openssl for every platform except NE-based ones (#2660)
* fix: enable bundled openssl for every platform except NE-based ones

* fix(conan): trigger CI/CD on crutial cmake changes

* fix: install dylibs/dlls from conan and use proper RPATH

* fix: adjust windows runtime deps
2026-05-28 19:17:27 +08:00
vkamn
113f967006 fix: various fixes (#2664)
* fix: fixed nextAvailableServerName

* fix: fixed password request for ssh key
2026-05-28 15:14:04 +08:00
309 changed files with 57842 additions and 25951 deletions

View File

@@ -23,9 +23,12 @@ jobs:
- 'recipes/**'
- 'conanfile.py'
- '.github/workflows/deploy.yml'
- 'cmake/conan_provider.cmake'
- 'cmake/platform_settings.cmake'
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
@@ -60,7 +63,7 @@ jobs:
# ------------------------------------------------------
Build-Linux-Ubuntu:
runs-on: android-runner
runs-on: ubuntu-22.04
needs: Bake-Prebuilts-Linux
if: ${{ always() }}
@@ -154,7 +157,7 @@ jobs:
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
run: cmake -S . -B build -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
@@ -411,6 +414,10 @@ jobs:
matrix:
xcode-version: [16.2, 16.4, 26.4]
include:
- xcode-version: 16.2
os: macos-15
- xcode-version: 16.4
os: macos-15
- xcode-version: 26.4
os: macos-26
@@ -447,7 +454,7 @@ jobs:
# ------------------------------------------------------
Build-MacOS:
runs-on: macos-latest
runs-on: macos-15
needs: Bake-Prebuilts-MacOS
if: ${{ always() }}
@@ -494,7 +501,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2.0'
xcode-version: '26.3.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v4
@@ -524,6 +531,8 @@ jobs:
QT_INSTALL_DIR: ${{ runner.temp }}
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
CODESIGN_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
CODESIGN_INSTALLER_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
@@ -547,6 +556,10 @@ jobs:
matrix:
xcode-version: [16.2, 16.4, 26.4]
include:
- xcode-version: 16.2
os: macos-15
- xcode-version: 16.4
os: macos-15
- xcode-version: 26.4
os: macos-26
@@ -583,7 +596,7 @@ jobs:
# ------------------------------------------------------
Build-MacOS-NE:
runs-on: macos-latest
runs-on: macos-26
needs: Bake-Prebuilts-MacOS-NE
if: ${{ always() }}
@@ -607,6 +620,12 @@ jobs:
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
steps:
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- uses: ./.github/actions/apple-setup-provisioning-profile
with:
provisioning_profile_base64: ${{ secrets.MAC_APP_PROVISIONING_PROFILE }}
@@ -644,12 +663,6 @@ jobs:
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: 'Setup python'
uses: actions/setup-python@v6
with:
@@ -732,11 +745,37 @@ jobs:
needs: Bake-Prebuilts-Android
if: ${{ always() }}
strategy:
fail-fast: false
matrix:
include:
- label: 'android9-10'
qt_version: '6.10.1'
min_sdk: 28
max_sdk: 29
version_code_offset: 0
build_aab: true
- label: 'android11+'
qt_version: '6.10.3'
min_sdk: 30
max_sdk: ''
version_code_offset: 1
build_aab: true
- label: 'android9-10-qt610-3'
qt_version: '6.10.3'
min_sdk: 28
max_sdk: 29
version_code_offset: 0
build_aab: false
env:
ANDROID_PLATFORM: android-28
ANDROID_PLATFORM: android-${{ matrix.min_sdk }}
NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.3
QT_VERSION: ${{ matrix.qt_version }}
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
APP_ANDROID_MIN_SDK: ${{ matrix.min_sdk }}
APP_ANDROID_MAX_SDK: ${{ matrix.max_sdk }}
APP_ANDROID_VERSION_CODE_OFFSET: ${{ matrix.version_code_offset }}
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
@@ -844,62 +883,82 @@ jobs:
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
LABEL: ${{ matrix.label }}
BUILD_AAB: ${{ matrix.build_aab }}
shell: bash
run: |
deploy/build.sh -t android --sign --aab
OUT=deploy/artifacts
rm -rf $OUT && mkdir -p $OUT
if [[ "$BUILD_AAB" == "true" ]]; then
deploy/build.sh -t android --sign --aab
else
deploy/build.sh -t android --sign
fi
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
(cd deploy/build/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_universal.apk)
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}.aab)
mv deploy/build/client/android-build/AmneziaVPN.apk \
$OUT/AmneziaVPN_${VERSION}_${LABEL}_universal.apk
if [[ "$BUILD_AAB" == "true" ]]; then
mv deploy/build/client/android-build/build/outputs/bundle/release/android-build-release.aab \
$OUT/AmneziaVPN_${VERSION}_${LABEL}.aab
fi
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
(cd deploy/build/${abi}/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_${abi}.apk)
mv deploy/build/${abi}/client/android-build/AmneziaVPN.apk \
$OUT/AmneziaVPN_${VERSION}_${LABEL}_${abi}.apk
done
ls -l $OUT
- name: 'Upload universal APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/client/android-build/*.apk
archive: false
retention-days: 7
- name: 'Upload AAB'
uses: actions/upload-artifact@v7
with:
path: deploy/build/client/android-build/build/outputs/bundle/release/*.aab
path: deploy/artifacts/*_universal.apk
archive: false
retention-days: 7
- name: 'Upload arm64-v8a APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/arm64-v8a/client/android-build/*.apk
path: deploy/artifacts/*_arm64-v8a.apk
archive: false
retention-days: 7
- name: 'Upload armeabi-v7a APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/armeabi-v7a/client/android-build/*.apk
path: deploy/artifacts/*_armeabi-v7a.apk
archive: false
retention-days: 7
- name: 'Upload x86 APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/x86/client/android-build/*.apk
path: deploy/artifacts/*_x86.apk
archive: false
retention-days: 7
- name: 'Upload x86_64 APK'
uses: actions/upload-artifact@v7
with:
path: deploy/build/x86_64/client/android-build/*.apk
path: deploy/artifacts/*_x86_64.apk
archive: false
retention-days: 7
- name: 'Upload AAB'
if: ${{ matrix.build_aab }}
uses: actions/upload-artifact@v7
with:
path: deploy/artifacts/*.aab
archive: false
retention-days: 7
# ------------------------------------------------------
Extra:
runs-on: ubuntu-latest
steps:

2
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

@@ -7,9 +7,6 @@
[submodule "client/3rd/amneziawg-apple"]
path = client/3rd/amneziawg-apple
url = https://github.com/amnezia-vpn/amneziawg-apple
[submodule "client/3rd/QSimpleCrypto"]
path = client/3rd/QSimpleCrypto
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
[submodule "client/3rd/qtgamepad"]
path = client/3rd/qtgamepad
url = https://github.com/amnezia-vpn/qtgamepad.git

View File

@@ -4,7 +4,9 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.1)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.0.1.5 CACHE STRING "Client app version")
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -18,9 +20,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
# trigger conan to kick off `conan install`
find_package(OpenSSL REQUIRED)
return()
endif()
@@ -28,7 +30,12 @@ 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 2122)
# bump by 2 on every release, because we're releasing two versions on the play store
set(APP_ANDROID_VERSION_CODE 2152)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
@@ -54,14 +61,14 @@ if(APPLE AND NOT IOS)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in"
"${CMAKE_CURRENT_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in"
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
@ONLY
)
file(COPY_FILE
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
"${CMAKE_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf"
"${CMAKE_CURRENT_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf"
ONLY_IF_DIFFERENT
)
endif()

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
include(${CMAKE_CURRENT_LIST_DIR}/cmake/clientBrandingDefaults.cmake)
set(PROJECT ${CLIENT_TARGET_NAME})
project(${PROJECT})
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
@@ -39,6 +41,7 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
endif()
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
find_package(OpenSSL REQUIRED)
set(LIBS ${LIBS}
Qt6::Core Qt6::Gui
@@ -56,6 +59,9 @@ qt_add_executable(${PROJECT} MANUAL_FINALIZATION)
target_include_directories(${PROJECT} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)
target_compile_definitions(${PROJECT} PRIVATE
CLIENT_KEYCHAIN_NAME_STR=\"${CLIENT_KEYCHAIN_NAME}\"
)
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
@@ -66,24 +72,34 @@ qt6_add_resources(QRC ${QRC}
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
${CMAKE_CURRENT_LIST_DIR}/ui/qml/qml.qrc
${CLIENT_QML_QRC_FILE}
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
${CLIENT_EXTRA_QRC}
)
# -- i18n begin
set(AMNEZIAVPN_TS_FILES
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru_RU.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ar_EG.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_my_MM.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_uk_UA.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ur_PK.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_hi_IN.ts
)
if(CLIENT_TS_FILES)
set(AMNEZIAVPN_TS_FILES ${CLIENT_TS_FILES})
else()
set(AMNEZIAVPN_TS_FILES
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ru_RU.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_zh_CN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fa_IR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ar_EG.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_my_MM.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_uk_UA.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ur_PK.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
)
endif()
file(GLOB_RECURSE AMNEZIAVPN_QML_FILES ${CMAKE_CURRENT_LIST_DIR}/ui/qml/*.qml)
qt6_add_translations(${PROJECT}
TS_FILES ${AMNEZIAVPN_TS_FILES}
SOURCES ${AMNEZIAVPN_QML_FILES}
RESOURCE_PREFIX "/translations"
)
# -- i18n end
@@ -100,12 +116,18 @@ endif()
include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
include_directories(
set(_client_include_dirs
${CMAKE_CURRENT_LIST_DIR}/../ipc
${CMAKE_CURRENT_LIST_DIR}/../common/logger
${CMAKE_CURRENT_LIST_DIR}/../common/crypto
${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_BINARY_DIR}
)
if(CLIENT_BRANDING_INCLUDE_DIR)
list(APPEND _client_include_dirs ${CLIENT_BRANDING_INCLUDE_DIR})
endif()
list(APPEND _client_include_dirs ${CMAKE_CURRENT_LIST_DIR})
include_directories(${_client_include_dirs})
if(MACOS_NE)
message("MACOS_NE is ON")
@@ -121,6 +143,26 @@ include_directories(mozilla/shared)
include_directories(mozilla/models)
configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/core/utils)
configure_file(
${CMAKE_CURRENT_LIST_DIR}/core/utils/appUiConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/core/utils/appUiConfig.h
@ONLY
)
configure_file(
${CMAKE_CURRENT_LIST_DIR}/core/utils/swiftBridgeConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/core/utils/swiftBridgeConfig.h
@ONLY
)
# Swift can't consume a `-DGROUP_ID="value"` compile define (Swift -D flags are
# presence-only), unlike the Objective-C++ sources that read GROUP_ID directly.
# Generate a Swift constant instead so Log.swift can use the branded app group.
set(CLIENT_GROUP_IDENTIFIER_SWIFT_FILE ${CMAKE_CURRENT_BINARY_DIR}/platforms/ios/GroupIdentifier.swift)
configure_file(
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/GroupIdentifier.swift.in
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
@ONLY
)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${PROJECT} PRIVATE "MZ_DEBUG")
@@ -148,18 +190,6 @@ if(APPLE)
cmake_policy(SET CMP0099 NEW)
cmake_policy(SET CMP0114 NEW)
if(NOT BUILD_OSX_APP_IDENTIFIER)
set(BUILD_OSX_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "OSX Application identifier")
endif()
if(NOT BUILD_IOS_APP_IDENTIFIER)
set(BUILD_IOS_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "iOS Application identifier")
endif()
if(NOT BUILD_IOS_GROUP_IDENTIFIER)
set(BUILD_IOS_GROUP_IDENTIFIER group.org.amnezia.AmneziaVPN CACHE STRING "iOS Group identifier")
endif()
if(NOT BUILD_VPN_DEVELOPMENT_TEAM)
set(BUILD_VPN_DEVELOPMENT_TEAM X7UJ388FXK CACHE STRING "Amnezia VPN Development Team")
endif()
set(CMAKE_XCODE_GENERATE_SCHEME FALSE)
set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${BUILD_VPN_DEVELOPMENT_TEAM})
@@ -173,6 +203,7 @@ endif()
if(LINUX AND NOT ANDROID)
set(LIBS ${LIBS} -static-libstdc++ -static-libgcc -ldl)
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
set(LIBS ${LIBS} -Wl,--no-as-needed $<TARGET_LINKER_FILE:Qt6::RemoteObjects> -Wl,--as-needed)
endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
@@ -212,11 +243,32 @@ endif()
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET client_deps
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
if(APPLE)
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/AmneziaVPN.app/Contents/Frameworks)
else()
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
endif()
install(RUNTIME_DEPENDENCY_SET client_deps
PRE_EXCLUDE_REGEXES
[[api-ms-win-.*]]
[[ext-ms-.*]]
[[kernel32\.dll]]
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^(lib)?[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]
[[^/usr/lib.*]]
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
)
set(deploy_tool_options "")
@@ -236,6 +288,10 @@ install(SCRIPT ${QT_DEPLOY_SCRIPT}
if (APPLE AND NOT IOS AND NOT MACOS_NE)
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/macos/update-resolv-conf.sh")
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
)
endif()
if (LINUX AND NOT ANDROID)
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/linux/update-resolv-conf.sh")

View File

@@ -22,9 +22,14 @@
#include "logger.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/models/installedAppsModel.h"
#include "ui/utils/mtProxyPublicHostInput.h"
#include "version.h"
#include "core/utils/appUiConfig.h"
#include "platforms/ios/QRCodeReaderBase.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ioscontextmenu.h"
#endif
bool AmneziaApplication::m_forceQuit = false;
@@ -98,7 +103,7 @@ void AmneziaApplication::init()
{
m_engine = new QQmlApplicationEngine;
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
const QUrl url(QStringLiteral(APP_QML_ENTRYPOINT));
QObject::connect(
m_engine, &QQmlApplicationEngine::objectCreated, this,
[this, url](QObject *obj, const QUrl &objUrl) {
@@ -119,7 +124,13 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
win->show();
}
#endif
}
},
Qt::QueuedConnection);
@@ -132,13 +143,20 @@ void AmneziaApplication::init()
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
#endif
#ifdef Q_OS_IOS
m_engine->rootContext()->setContextProperty("IosContextMenu", new IosContextMenu(this));
#endif
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
m_vpnConnectionThread.start();
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
m_engine->addImportPath("qrc:/ui/qml/Modules/");
m_marketplaceUpdateController.reset(new MarketplaceUpdateController());
m_marketplaceUpdateController->start();
m_engine->addImportPath(QStringLiteral(APP_QML_IMPORT_PATH));
if (m_parser.isSet(m_optImport)) {
const QString data = m_parser.value(m_optImport);
@@ -215,6 +233,9 @@ void AmneziaApplication::registerTypes()
qmlRegisterType<InstalledAppsModel>("InstalledAppsModel", 1, 0, "InstalledAppsModel");
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
qmlRegisterType<PublicHostInputValidator>("TelemtConfig", 1, 0, "PublicHostInputValidator");
amnezia::declareQmlProtocolEnum();
Vpn::declareQmlVpnConnectionStateEnum();
PageLoader::declareQmlPageEnum();
@@ -224,7 +245,7 @@ void AmneziaApplication::loadFonts()
{
QQuickStyle::setStyle("Basic");
QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf");
QFontDatabase::addApplicationFont(QStringLiteral(APP_UI_FONT_RESOURCE));
}
bool AmneziaApplication::parseCommands()
@@ -251,7 +272,7 @@ bool AmneziaApplication::parseCommands()
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
void AmneziaApplication::startLocalServer() {
const QString serverName("AmneziaVPNInstance");
const QString serverName(APP_INSTANCE_NAME);
QLocalServer::removeServer(serverName);
QLocalServer *server = new QLocalServer(this);

View File

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

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#2A2A2E"
android:centerColor="#17171A"
android:endColor="#0E0E11" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_amnezia_round"
android:insetLeft="19.5%"
android:insetTop="19.5%"
android:insetRight="19.5%"
android:insetBottom="19.5%" />

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/icon</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0E0E11</color>
</resources>

View File

@@ -3,6 +3,13 @@ package org.amnezia.vpn
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.app.NotificationManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
@@ -42,6 +49,7 @@ import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import java.io.File
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
@@ -99,6 +107,8 @@ class AmneziaActivity : QtActivity() {
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private var updateCoverDialog: Dialog? = null
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
@@ -486,6 +496,116 @@ class AmneziaActivity : QtActivity() {
super.onDestroy()
}
fun showUpdateCover() {
runOnUiThread {
if (isFinishing || isDestroyed || updateCoverDialog != null) return@runOnUiThread
val dialog = Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
dialog.setCancelable(false)
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(0xFF0E0E11.toInt())
}
dialog.setContentView(root)
dialog.show()
updateCoverDialog = dialog
}
}
fun hideUpdateCover() {
runOnUiThread {
updateCoverDialog?.dismiss()
updateCoverDialog = null
}
}
fun showUpdatePrompt(title: String, message: String, updateTitle: String, skipTitle: String, storeUrl: String) {
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
val dialog = updateCoverDialog ?: Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen).also {
it.setCancelable(false)
it.show()
updateCoverDialog = it
}
val density = resources.displayMetrics.density
fun dp(value: Int) = (value * density).toInt()
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(0xFF0E0E11.toInt())
setPadding(dp(32), dp(32), dp(32), dp(32))
}
val titleView = TextView(this).apply {
text = title
textSize = 22f
setTextColor(0xFFFFFFFF.toInt())
gravity = Gravity.CENTER
typeface = Typeface.create(typeface, Typeface.BOLD)
}
val messageView = TextView(this).apply {
text = message
textSize = 16f
setTextColor(0xFFC7C8CB.toInt())
gravity = Gravity.CENTER
setPadding(0, dp(16), 0, dp(28))
}
val updateButton = Button(this).apply {
text = updateTitle
isAllCaps = false
textSize = 17f
setTextColor(0xFF0E0E11.toInt())
stateListAnimator = null
background = GradientDrawable().apply {
cornerRadius = dp(12).toFloat()
setColor(0xFFFBB26A.toInt())
}
setOnClickListener {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)))
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "open store failed: ${e.message}")
}
hideUpdateCover()
}
}
val skipButton = Button(this).apply {
text = skipTitle
isAllCaps = false
textSize = 17f
setTextColor(0xFFD7D8DB.toInt())
stateListAnimator = null
background = GradientDrawable().apply {
cornerRadius = dp(12).toFloat()
setColor(0x00000000)
setStroke(dp(1), 0xFF2C2D30.toInt())
}
setOnClickListener { hideUpdateCover() }
}
val updateParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
).apply { topMargin = dp(8) }
val skipParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
).apply { topMargin = dp(12) }
root.addView(titleView)
root.addView(messageView)
root.addView(updateButton, updateParams)
root.addView(skipButton, skipParams)
dialog.setContentView(root)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")
@@ -763,7 +883,13 @@ class AmneziaActivity : QtActivity() {
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
mainScope.launch {
val intent = if (!isOnTv()) {
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
.firstOrNull { pkg ->
try { packageManager.getPackageInfo(pkg, 0); true }
catch (_: PackageManager.NameNotFoundException) { false }
}
val intent = if (!isOnTv() && systemPickerPackage != null) {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
@@ -789,6 +915,7 @@ class AmneziaActivity : QtActivity() {
else -> type = "*/*"
}
}
`package` = systemPickerPackage
}
} else {
Intent(this@AmneziaActivity, TvFilePicker::class.java)
@@ -800,8 +927,11 @@ class AmneziaActivity : QtActivity() {
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog()
}
val uri = it?.data?.apply {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
val uri = it?.data?.let { u ->
if (u.scheme == "content") {
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
}
u
}?.toString() ?: ""
Log.v(TAG, "Open file: $uri")
if (uri.isNotEmpty()) {
@@ -841,7 +971,12 @@ class AmneziaActivity : QtActivity() {
Log.v(TAG, "Get fd for $fileName")
return blockingCall(Dispatchers.IO) {
try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
val uri = Uri.parse(fileName)
pfd = if (uri.scheme == "file") {
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
} else {
contentResolver.openFileDescriptor(uri, "r")
}
pfd?.fd ?: -1
} catch (e: Exception) {
Log.e(TAG, "Failed to get fd: $e")
@@ -1061,12 +1196,10 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y")
blockingCall {
findQtWindow(window.decorView)?.let {
Log.v(TAG, "Send touch to $it")
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
}
findQtWindow(window.decorView)?.let {
Log.v(TAG, "Send touch to $it")
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
}
}

View File

@@ -1,30 +1,36 @@
package org.amnezia.vpn
import android.Manifest
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import org.amnezia.vpn.util.Log
import java.io.File
private const val TAG = "TvFilePicker"
private const val READ_STORAGE_REQUEST_CODE = 1001
class TvFilePicker : ComponentActivity() {
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
// SAF launcher for Android 10+ where File API is blocked by scoped storage
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
if (activitiesToResolveIntent.all {
if (activities.all {
val name = it.activityInfo.packageName
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
}) {
@@ -32,38 +38,140 @@ class TvFilePicker : ComponentActivity() {
}
return intent
}
}) {
}) { uri ->
setResult(RESULT_OK, Intent().apply {
data = it
data = uri
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
})
finish()
}
private val directoryStack = ArrayDeque<File>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.v(TAG, "onCreate")
getFile()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
launchSaf()
} else {
checkPermissionAndBrowse()
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.v(TAG, "onNewIntent")
getFile()
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
navigateBack()
}
private fun getFile() {
private fun launchSaf() {
try {
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch(arrayOf("*/*"))
safLauncher.launch(arrayOf("*/*"))
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found")
Log.w(TAG, "No SAF activity found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
finish()
} catch (e: Exception) {
Log.e(TAG, "Failed to get file: $e")
Log.e(TAG, "SAF launch failed: $e")
setResult(RESULT_CANCELED)
finish()
}
}
private fun checkPermissionAndBrowse() {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_REQUEST_CODE)
} else {
showRootDirectory()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == READ_STORAGE_REQUEST_CODE &&
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
showRootDirectory()
} else {
setResult(RESULT_CANCELED)
finish()
}
}
private fun showRootDirectory() {
@Suppress("DEPRECATION")
val primaryExternal = Environment.getExternalStorageDirectory()
val storageDir = File("/storage")
// Pre-seed stack with /storage so Back from primary storage goes there (USB drives etc.)
if (storageDir.exists() && storageDir.canonicalPath != primaryExternal.canonicalPath) {
directoryStack.addLast(storageDir)
}
showDirectory(primaryExternal)
}
private fun navigateBack() {
if (directoryStack.size > 1) {
directoryStack.removeLast()
val parent = directoryStack.removeLast()
showDirectory(parent)
} else {
setResult(RESULT_CANCELED)
finish()
}
}
private fun showDirectory(dir: File) {
directoryStack.addLast(dir)
Log.v(TAG, "Showing directory: ${dir.absolutePath}")
val entries = try {
dir.listFiles()
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
?: emptyList()
} catch (e: Exception) {
Log.e(TAG, "Failed to list directory: $e")
emptyList()
}
val names = entries.map { if (it.isDirectory) "[${it.name}]" else it.name }.toTypedArray()
val builder = AlertDialog.Builder(this)
.setTitle(dir.absolutePath)
if (entries.isEmpty()) {
builder.setMessage("No files available")
} else {
builder.setItems(names) { dialog, which ->
dialog.dismiss()
val selected = entries[which]
if (selected.isDirectory) {
showDirectory(selected)
} else {
Log.v(TAG, "Selected file: ${selected.absolutePath}")
setResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(selected)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
})
finish()
}
}
}
if (directoryStack.size > 1) {
builder.setNegativeButton("↑ Back") { dialog, _ ->
dialog.dismiss()
navigateBack()
}
} else {
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
setResult(RESULT_CANCELED)
finish()
}
}
builder.setOnCancelListener {
setResult(RESULT_CANCELED)
finish()
}
builder.show()
}
}

View File

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

View File

@@ -10,7 +10,7 @@ private const val WIREGUARD_DEFAULT_MTU = 1280
open class WireguardConfig protected constructor(
protocolConfigBuilder: ProtocolConfig.Builder,
val endpoint: InetEndpoint,
val persistentKeepalive: Int,
val persistentKeepalive: String?,
val publicKeyHex: String,
val preSharedKeyHex: String?,
val privateKeyHex: String,
@@ -31,6 +31,15 @@ open class WireguardConfig protected constructor(
var i3: String?,
var i4: String?,
var i5: String?,
val headerProtectionKeyHex: String?,
val contentPaddingAddition: String?,
val rekeyAfterTime: String?,
val rekeyTimeout: String?,
val rejectAfterTime: String?,
val keepaliveTimeout: String?,
val maxHandshakeAttempts: String?,
val randomTrailers: String?,
val disableCookies: String?,
) : ProtocolConfig(protocolConfigBuilder) {
protected constructor(builder: Builder) : this(
@@ -57,6 +66,15 @@ open class WireguardConfig protected constructor(
builder.i3,
builder.i4,
builder.i5,
builder.headerProtectionKeyHex,
builder.contentPaddingAddition,
builder.rekeyAfterTime,
builder.rekeyTimeout,
builder.rejectAfterTime,
builder.keepaliveTimeout,
builder.maxHandshakeAttempts,
builder.randomTrailers,
builder.disableCookies,
)
fun toWgUserspaceString(): String = with(StringBuilder()) {
@@ -87,6 +105,15 @@ open class WireguardConfig protected constructor(
i4?.let { appendLine("i4=$it") }
i5?.let { appendLine("i5=$it") }
}
headerProtectionKeyHex?.takeIf { it.isNotEmpty() }?.let { appendLine("header_protection_key=$it") }
contentPaddingAddition?.takeIf { it.isNotEmpty() }?.let { appendLine("content_padding_addition=$it") }
rekeyAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_after_time=$it") }
rekeyTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_timeout=$it") }
rejectAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("reject_after_time=$it") }
keepaliveTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("keepalive_timeout=$it") }
maxHandshakeAttempts?.takeIf { it.isNotEmpty() }?.let { appendLine("max_handshake_attempts=$it") }
randomTrailers?.takeIf { it.isNotEmpty() }?.let { appendLine("random_trailers=${it.toUapiBool()}") }
disableCookies?.takeIf { it.isNotEmpty() }?.let { appendLine("disable_cookies=${it.toUapiBool()}") }
}
private fun validateProtocolExtensionParameters() {
@@ -107,7 +134,7 @@ open class WireguardConfig protected constructor(
appendLine("allowed_ip=${route.inetNetwork}")
}
appendLine("endpoint=$endpoint")
if (persistentKeepalive != 0)
if (!persistentKeepalive.isNullOrEmpty() && persistentKeepalive != "0")
appendLine("persistent_keepalive_interval=$persistentKeepalive")
if (preSharedKeyHex != null)
appendLine("preshared_key=$preSharedKeyHex")
@@ -117,7 +144,7 @@ open class WireguardConfig protected constructor(
internal lateinit var endpoint: InetEndpoint
private set
internal var persistentKeepalive: Int = 0
internal var persistentKeepalive: String? = null
private set
internal lateinit var publicKeyHex: String
@@ -149,10 +176,19 @@ open class WireguardConfig protected constructor(
internal var i3: String? = null
internal var i4: String? = null
internal var i5: String? = null
internal var headerProtectionKeyHex: String? = null
internal var contentPaddingAddition: String? = null
internal var rekeyAfterTime: String? = null
internal var rekeyTimeout: String? = null
internal var rejectAfterTime: String? = null
internal var keepaliveTimeout: String? = null
internal var maxHandshakeAttempts: String? = null
internal var randomTrailers: String? = null
internal var disableCookies: String? = null
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
fun setPersistentKeepalive(persistentKeepalive: String) = apply { this.persistentKeepalive = persistentKeepalive }
fun setPublicKeyHex(publicKeyHex: String) = apply { this.publicKeyHex = publicKeyHex }
@@ -178,6 +214,15 @@ open class WireguardConfig protected constructor(
fun setI3(i3: String) = apply { this.i3 = i3 }
fun setI4(i4: String) = apply { this.i4 = i4 }
fun setI5(i5: String) = apply { this.i5 = i5 }
fun setHeaderProtectionKey(headerProtectionKeyHex: String) = apply { this.headerProtectionKeyHex = headerProtectionKeyHex }
fun setContentPaddingAddition(contentPaddingAddition: String) = apply { this.contentPaddingAddition = contentPaddingAddition }
fun setRekeyAfterTime(rekeyAfterTime: String) = apply { this.rekeyAfterTime = rekeyAfterTime }
fun setRekeyTimeout(rekeyTimeout: String) = apply { this.rekeyTimeout = rekeyTimeout }
fun setRejectAfterTime(rejectAfterTime: String) = apply { this.rejectAfterTime = rejectAfterTime }
fun setKeepaliveTimeout(keepaliveTimeout: String) = apply { this.keepaliveTimeout = keepaliveTimeout }
fun setMaxHandshakeAttempts(maxHandshakeAttempts: String) = apply { this.maxHandshakeAttempts = maxHandshakeAttempts }
fun setRandomTrailers(randomTrailers: String) = apply { this.randomTrailers = randomTrailers }
fun setDisableCookies(disableCookies: String) = apply { this.disableCookies = disableCookies }
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
}
@@ -189,3 +234,9 @@ open class WireguardConfig protected constructor(
@OptIn(ExperimentalStdlibApi::class)
internal fun String.base64ToHex(): String = Base64.decode(this, Base64.DEFAULT).toHexString()
/** Converts awg-quick on/off (and 0/1/true/false) to UAPI 1/0 for amneziawg-go ParseBool. */
internal fun String.toUapiBool(): String = when (trim().lowercase()) {
"on", "1", "true", "t", "yes" -> "1"
else -> "0"
}

View File

@@ -2,9 +2,8 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel)
set(LIBS ${LIBS} SortFilterProxyModel)
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
@@ -12,20 +11,24 @@ add_compile_definitions(_WINSOCKAPI_)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_QT6 ON)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain ${CMAKE_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
if(ANDROID)
# Use qtgamepad from amnezia-vpn/qtgamepad repository
# Only if Qt6CorePrivate is available (required by qtgamepad)
find_package(Qt6CorePrivate CONFIG QUIET)
if(Qt6CorePrivate_FOUND)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad ${CMAKE_BINARY_DIR}/3rd/qtgamepad)
# Link both the C++ module and QML plugin
if(TARGET GamepadLegacy)
if(TARGET ${PROJECT})
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
else()
list(APPEND LIBS GamepadLegacy)
endif()
if(TARGET GamepadLegacyQuickPrivate)
if(TARGET ${PROJECT})
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
else()
list(APPEND LIBS GamepadLegacyQuickPrivate)
endif()
message(STATUS "Gamepad support enabled for Android")
else()
@@ -36,7 +39,6 @@ endif()
set(LIBS ${LIBS} qt6keychain)
include_directories(
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
)

View File

@@ -1,21 +0,0 @@
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
include_directories(${QSIMPLECRYPTO_DIR})
set(HEADERS ${HEADERS}
${QSIMPLECRYPTO_DIR}/include/QAead.h
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
${QSIMPLECRYPTO_DIR}/include/QRsa.h
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
${QSIMPLECRYPTO_DIR}/include/QX509.h
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
)
set(SOURCES ${SOURCES}
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
)

View File

@@ -1,6 +1,8 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 28)
if(NOT DEFINED APP_ANDROID_MIN_SDK)
set(APP_ANDROID_MIN_SDK 28)
endif()
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -13,7 +15,6 @@ set_target_properties(${PROJECT} PROPERTIES
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
QT_ANDROID_TARGET_SDK_VERSION 36
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
)
set(QT_ANDROID_MULTI_ABI_FORWARD_VARS "QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL;CMAKE_BUILD_TYPE")
@@ -53,3 +54,27 @@ file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/androi
find_package(openvpn-pt-android REQUIRED)
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
if(APP_ANDROID_MAX_SDK)
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/android-package-source)
file(REMOVE_RECURSE ${APP_ANDROID_PACKAGE_SOURCE_DIR})
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/android/ DESTINATION ${APP_ANDROID_PACKAGE_SOURCE_DIR})
set(manifest_path ${APP_ANDROID_PACKAGE_SOURCE_DIR}/AndroidManifest.xml)
set(manifest_anchor "android:installLocation=\"auto\">")
file(READ ${manifest_path} manifest_contents)
string(REPLACE
"${manifest_anchor}"
"${manifest_anchor}\n\n <uses-sdk android:maxSdkVersion=\"${APP_ANDROID_MAX_SDK}\" />"
patched_contents "${manifest_contents}")
if(patched_contents STREQUAL manifest_contents)
message(FATAL_ERROR
"Failed to set maxSdkVersion=${APP_ANDROID_MAX_SDK}: anchor '${manifest_anchor}' "
"not found in ${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml")
endif()
file(WRITE ${manifest_path} "${patched_contents}")
endif()
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${APP_ANDROID_PACKAGE_SOURCE_DIR})

View File

@@ -0,0 +1,3 @@
if(NOT CLIENT_ANDROID_PACKAGE)
set(CLIENT_ANDROID_PACKAGE "org.amnezia.vpn" CACHE STRING "Android package name for Play Store version lookup")
endif()

View File

@@ -0,0 +1,22 @@
if(NOT CLIENT_SWIFT_MODULE_NAME)
set(CLIENT_SWIFT_MODULE_NAME "${CLIENT_APPLICATION_NAME}" CACHE STRING "Swift module name for Objective-C interop header")
endif()
if(NOT CLIENT_SWIFT_OBJC_HEADER_NAME)
set(CLIENT_SWIFT_OBJC_HEADER_NAME "${CLIENT_SWIFT_MODULE_NAME}-Swift.h" CACHE STRING "Generated Swift Objective-C header name")
endif()
if(NOT CLIENT_NETWORK_EXTENSION_NAME)
set(CLIENT_NETWORK_EXTENSION_NAME "${CLIENT_APPLICATION_NAME}NetworkExtension" CACHE STRING "Display name for Apple network extension targets")
endif()
if(NOT BUILD_OSX_APP_IDENTIFIER)
set(BUILD_OSX_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "OSX Application identifier")
endif()
if(NOT BUILD_IOS_APP_IDENTIFIER)
set(BUILD_IOS_APP_IDENTIFIER org.amnezia.AmneziaVPN CACHE STRING "iOS Application identifier")
endif()
if(NOT BUILD_IOS_GROUP_IDENTIFIER)
set(BUILD_IOS_GROUP_IDENTIFIER group.org.amnezia.AmneziaVPN CACHE STRING "iOS Group identifier")
endif()
if(NOT BUILD_VPN_DEVELOPMENT_TEAM)
set(BUILD_VPN_DEVELOPMENT_TEAM X7UJ388FXK CACHE STRING "Amnezia VPN Development Team")
endif()

View File

@@ -0,0 +1,60 @@
if(NOT CLIENT_TARGET_NAME)
set(CLIENT_TARGET_NAME "AmneziaVPN" CACHE STRING "Client executable target name")
endif()
if(NOT CLIENT_APPLICATION_NAME)
set(CLIENT_APPLICATION_NAME "AmneziaVPN" CACHE STRING "Application display and executable name")
endif()
if(NOT CLIENT_SERVICE_NAME)
set(CLIENT_SERVICE_NAME "AmneziaVPN-service" CACHE STRING "Service executable name")
endif()
if(NOT CLIENT_ORGANIZATION_NAME)
set(CLIENT_ORGANIZATION_NAME "AmneziaVPN.ORG" CACHE STRING "QSettings organization name")
endif()
if(NOT CLIENT_APP_INSTANCE_NAME)
set(CLIENT_APP_INSTANCE_NAME "AmneziaVPNInstance" CACHE STRING "Single-instance local server name")
endif()
if(NOT CLIENT_KEYCHAIN_NAME)
set(CLIENT_KEYCHAIN_NAME "${CLIENT_APPLICATION_NAME}-Keychain" CACHE STRING "QtKeychain service name used for encrypted settings keys")
endif()
# -- QML entry points and UI resources
if(NOT CLIENT_QML_ENTRYPOINT)
set(CLIENT_QML_ENTRYPOINT "qrc:/ui/qml/main2.qml" CACHE STRING "Main QML entrypoint URL")
endif()
if(NOT CLIENT_QML_IMPORT_PATH)
set(CLIENT_QML_IMPORT_PATH "qrc:/ui/qml/Modules/" CACHE STRING "Additional QQmlEngine import path")
endif()
if(NOT CLIENT_QML_PAGES_PREFIX)
set(CLIENT_QML_PAGES_PREFIX "qrc:/ui/qml/Pages2/" CACHE STRING "PageController QML pages prefix")
endif()
if(NOT CLIENT_UI_FONT_RESOURCE)
set(CLIENT_UI_FONT_RESOURCE ":/fonts/pt-root-ui_vf.ttf" CACHE STRING "UI font resource path")
endif()
if(NOT CLIENT_QML_QRC_FILE)
set(CLIENT_QML_QRC_FILE "${_CLIENT_SRC_DIR}/ui/qml/qml.qrc" CACHE FILEPATH "QML resources manifest")
endif()
if(NOT CLIENT_EXTRA_QRC)
set(CLIENT_EXTRA_QRC "" CACHE STRING "Optional semicolon-separated extra QRC files")
endif()
# -- translations
if(NOT CLIENT_TS_PREFIX)
set(CLIENT_TS_PREFIX "amneziavpn" CACHE STRING "Translation filename prefix")
endif()
if(NOT CLIENT_TS_FILES)
set(CLIENT_TS_FILES "" CACHE STRING "Optional semicolon-separated absolute TS file paths")
endif()
# -- brand-provided headers
if(NOT CLIENT_BRANDING_INCLUDE_DIR)
set(CLIENT_BRANDING_INCLUDE_DIR "" CACHE PATH "Optional include root for non-generated branding headers (e.g. pageEnum.h)")
endif()
if(CLIENT_BRANDING_INCLUDE_DIR AND EXISTS "${CLIENT_BRANDING_INCLUDE_DIR}/ui/utils/pageEnum.h")
set(CLIENT_PAGE_ENUM_HEADER "${CLIENT_BRANDING_INCLUDE_DIR}/ui/utils/pageEnum.h")
else()
set(CLIENT_PAGE_ENUM_HEADER "${_CLIENT_SRC_DIR}/ui/utils/pageEnum.h")
endif()
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
endif()

View File

@@ -0,0 +1,45 @@
if(NOT CLIENT_IOS_NE_TARGET_NAME)
set(CLIENT_IOS_NE_TARGET_NAME "networkextension" CACHE STRING "iOS network extension build target name")
endif()
if(NOT CLIENT_IOS_NE_ENTITLEMENTS_FILE)
set(CLIENT_IOS_NE_ENTITLEMENTS_FILE "AmneziaVPNNetworkExtension.entitlements" CACHE STRING "iOS network extension entitlements filename")
endif()
if(NOT CLIENT_IOS_LAUNCHSCREEN_FILE)
set(CLIENT_IOS_LAUNCHSCREEN_FILE "AmneziaVPNLaunchScreen.storyboard" CACHE STRING "iOS launch screen storyboard filename")
endif()
get_filename_component(CLIENT_IOS_LAUNCHSCREEN_NAME "${CLIENT_IOS_LAUNCHSCREEN_FILE}" NAME_WE)
if(NOT CLIENT_IOS_APP_ENTITLEMENTS_PATH)
set(CLIENT_IOS_APP_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/ios/app/main.entitlements" CACHE FILEPATH "iOS app entitlements file path")
endif()
if(NOT CLIENT_IOS_NE_ENTITLEMENTS_PATH)
set(CLIENT_IOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/ios/networkextension/${CLIENT_IOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "iOS network extension entitlements file path")
endif()
if(NOT CLIENT_IOS_LAUNCHSCREEN_PATH)
set(CLIENT_IOS_LAUNCHSCREEN_PATH "${_CLIENT_SRC_DIR}/ios/app/${CLIENT_IOS_LAUNCHSCREEN_FILE}" CACHE FILEPATH "iOS launch screen storyboard path")
endif()
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
endif()
if(NOT CLIENT_IOS_BUNDLE_ID)
set(CLIENT_IOS_BUNDLE_ID "org.amnezia.AmneziaVPN" CACHE STRING "iOS bundle id for App Store version lookup")
endif()
if(NOT CLIENT_IOS_STORE_URL_FALLBACK)
set(CLIENT_IOS_STORE_URL_FALLBACK "itms-apps://itunes.apple.com/app/id1600529900" CACHE STRING "Fallback App Store URL when lookup omits trackViewUrl")
endif()
# -- signing
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for release")
endif()
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for debug")
endif()
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for release")
endif()
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for debug")
endif()

View File

@@ -0,0 +1,37 @@
if(NOT CLIENT_MACOS_NE_TARGET_NAME)
set(CLIENT_MACOS_NE_TARGET_NAME "${CLIENT_NETWORK_EXTENSION_NAME}" CACHE STRING "macOS network extension build target name")
endif()
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_FILE)
set(CLIENT_MACOS_NE_ENTITLEMENTS_FILE "AmneziaVPNNetworkExtension.entitlements" CACHE STRING "macOS network extension entitlements filename")
endif()
if(NOT CLIENT_MACOS_APP_ENTITLEMENTS_PATH)
set(CLIENT_MACOS_APP_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/app/app.entitlements" CACHE FILEPATH "macOS app entitlements file path")
endif()
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_PATH)
set(CLIENT_MACOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/networkextension/${CLIENT_MACOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "macOS network extension entitlements file path")
endif()
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
endif()
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
endif()
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
set(CLIENT_MACOS_EXTRA_PLIST_ENTRIES "" CACHE STRING "Extra macOS Info.plist entries appended verbatim (e.g. appearance pinning)")
endif()
# -- signing
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for release")
endif()
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for debug")
endif()
if(NOT CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.amneziaVPN.NE" CACHE STRING "macOS network extension provisioning profile specifier for release")
endif()
if(NOT CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.amneziaVPN.NE" CACHE STRING "macOS network extension provisioning profile specifier for debug")
endif()

View File

@@ -0,0 +1,9 @@
get_filename_component(_CLIENT_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
include(${CMAKE_CURRENT_LIST_DIR}/branding/common.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/apple.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/ios.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/macos.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/android.cmake)
unset(_CLIENT_SRC_DIR)

View File

@@ -24,6 +24,17 @@ set(IOS_TARGET_COMPILED_TYPES
EXECUTABLE
)
## Simulator architecture: honor CMAKE_OSX_ARCHITECTURES when set, otherwise
## match the host — arm64 Macs run arm64 simulators (Xcode 26 has no Rosetta
## simulator destinations), Intel Macs need x86_64.
if(CMAKE_OSX_ARCHITECTURES)
string(REPLACE ";" " " IOS_SIMULATOR_ARCHS "${CMAKE_OSX_ARCHITECTURES}")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
set(IOS_SIMULATOR_ARCHS "arm64")
else()
set(IOS_SIMULATOR_ARCHS "x86_64")
endif()
## Inspect all the targets, and add extra properties if necessary.
while(IOS_TARGETS)
list(POP_FRONT IOS_TARGETS TARGET_NAME)
@@ -38,6 +49,6 @@ while(IOS_TARGETS)
message("Patching architectures for ${TARGET_NAME}")
set_target_properties(${TARGET_NAME} PROPERTIES
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "${IOS_SIMULATOR_ARCHS}"
)
endwhile()

View File

@@ -31,6 +31,7 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -42,6 +43,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
@@ -49,43 +51,47 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
)
# The context menu helper uses ARC-only constructs (weak references); the
# rest of the Objective-C++ sources build with manual reference counting.
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
)
target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CLIENT_IOS_APP_ENTITLEMENTS_PATH}"
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_ATTRIBUTE_PRODUCT_NAME "${CLIENT_APPLICATION_NAME}"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
XCODE_EMBED_APP_EXTENSIONS networkextension
XCODE_EMBED_APP_EXTENSIONS ${CLIENT_IOS_NE_TARGET_NAME}
)
if(DEFINED DEPLOY)
if(DEPLOY)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "dev ios.org.amnezia.AmneziaVPN"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER}"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "${CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG}"
)
else()
set_target_properties(${PROJECT} PROPERTIES
@@ -97,11 +103,11 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "AmneziaVPN-Swift.h"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "${CLIENT_SWIFT_OBJC_HEADER_NAME}"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTEROP_MODE "objcxx"
)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${BUILD_VPN_DEVELOPMENT_TEAM}"
)
target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_compile_options(${PROJECT} PRIVATE
@@ -116,22 +122,28 @@ target_sources(${PROJECT} PRIVATE
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
set_source_files_properties(
${CLIENT_IOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CLIENT_IOS_LAUNCHSCREEN_PATH}
${CLIENT_IOS_MEDIA_ASSETS_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
${CLIENT_IOS_LAUNCHSCREEN_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
add_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} networkextension)
add_dependencies(${PROJECT} ${CLIENT_IOS_NE_TARGET_NAME})

View File

@@ -34,8 +34,8 @@ set(SOURCES ${SOURCES}
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
set(MACOSX_BUNDLE_ICON_FILE app.icns)
set(ICON_FILE ${CLIENT_MACOS_APP_ICNS_PATH})
get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" NAME)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})

View File

@@ -5,6 +5,7 @@ set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
enable_language(OBJC)
enable_language(OBJCXX)
enable_language(Swift)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Widgets)
@@ -33,7 +34,6 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -44,19 +44,12 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
)
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
set(MACOSX_BUNDLE_ICON_FILE app.icns)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})
target_include_directories(${PROJECT} PRIVATE
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
${Qt6Widgets_PRIVATE_INCLUDE_DIRS}
@@ -67,28 +60,25 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/macos/app/app.entitlements"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CLIENT_MACOS_APP_ENTITLEMENTS_PATH}"
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_ATTRIBUTE_PRODUCT_NAME "${CLIENT_APPLICATION_NAME}"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY "NO"
XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY "YES"
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks"
XCODE_EMBED_APP_EXTENSIONS AmneziaVPNNetworkExtension
XCODE_EMBED_APP_EXTENSIONS ${CLIENT_MACOS_NE_TARGET_NAME}
)
if(DEPLOY)
@@ -96,8 +86,8 @@ if(DEPLOY)
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "dev macos.org.amnezia.AmneziaVPN"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER}"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "${CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG}"
)
else()
set_target_properties(${PROJECT} PROPERTIES
@@ -109,11 +99,11 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "AmneziaVPN-Swift.h"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "${CLIENT_SWIFT_OBJC_HEADER_NAME}"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTEROP_MODE "objcxx"
)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${BUILD_VPN_DEVELOPMENT_TEAM}"
)
target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_compile_options(${PROJECT} PRIVATE
@@ -127,14 +117,20 @@ target_sources(${PROJECT} PRIVATE
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
set_source_files_properties(
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
@@ -143,7 +139,7 @@ set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
)
add_subdirectory(macos/networkextension)
add_dependencies(${PROJECT} AmneziaVPNNetworkExtension)
add_dependencies(${PROJECT} ${CLIENT_MACOS_NE_TARGET_NAME})
get_target_property(QtCore_location Qt6::Core LOCATION)
message("QtCore_location")
@@ -152,5 +148,5 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:${PROJECT}> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
)

View File

@@ -51,17 +51,23 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.h
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
${CLIENT_ROOT_DIR}/ui/utils/pages.h
${CLIENT_PAGE_ENUM_HEADER}
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.h
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.h
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.h
${CMAKE_CURRENT_BINARY_DIR}/version.h
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.h
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.h
${CLIENT_ROOT_DIR}/core/utils/payloadSender.h
${CLIENT_ROOT_DIR}/core/utils/serialization/serialization.h
${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h
${CLIENT_ROOT_DIR}/../common/logger/logger.h
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
${CLIENT_ROOT_DIR}/core/utils/utilities.h
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
${CLIENT_ROOT_DIR}/core/utils/constants.h
@@ -127,9 +133,11 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.cpp
${CLIENT_ROOT_DIR}/ui/utils/qAutoStart.cpp
${CLIENT_ROOT_DIR}/ui/utils/mtProxyPublicHostInput.cpp
${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.cpp
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.cpp
${CLIENT_ROOT_DIR}/core/utils/networkUtilities.cpp
${CLIENT_ROOT_DIR}/core/utils/payloadSender.cpp
${CLIENT_ROOT_DIR}/core/utils/serialization/outbound.cpp
${CLIENT_ROOT_DIR}/core/utils/serialization/inbound.cpp
${CLIENT_ROOT_DIR}/core/utils/serialization/ss.cpp
@@ -139,8 +147,10 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess.cpp
${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,16 @@ public:
bool appendNewClient,
QString *outClientId = nullptr);
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings);
private:
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
const QString &path, QString &out) const;
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);
@@ -51,9 +60,6 @@ private:
QString &outPublicKey,
QString &outShortId) const;
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const;
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const;
};

View File

@@ -2,6 +2,8 @@
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include <QtConcurrent/QtConcurrent>
@@ -78,17 +80,15 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
QJsonObject payload;
payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first());
if (services.contains(apiDefs::key::userCountryCode)) {
payload.insert(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode));
}
if (services.contains(apiDefs::key::serviceType)) {
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType));
}
// both country codes and service types are arrays here, one entry per gateway stack the user has
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::locale, apiUtils::getAppLanguageCode(m_appSettingsRepository))
.addField(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode))
.addField(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType))
.build();
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {

View File

@@ -1,7 +1,6 @@
#include "servicesCatalogController.h"
#include <QJsonDocument>
#include <QSysInfo>
#include <QJsonArray>
#include <QEventLoop>
#include <QDebug>
@@ -11,6 +10,7 @@
#include <limits>
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
@@ -212,11 +212,7 @@ ServicesCatalogController::ServicesCatalogController(SecureAppSettingsRepository
ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &servicesData)
{
QJsonObject apiPayload;
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
apiPayload[apiDefs::key::appLanguage] = m_appSettingsRepository->getAppLanguage().name().split("_").first();
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository).build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
@@ -242,7 +238,7 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
ErrorCode ServicesCatalogController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
{
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
return gatewayController.post(endpoint, apiPayload, responseBody);
}

View File

@@ -8,7 +8,6 @@
#include <QJsonObject>
#include <QPromise>
#include <QSet>
#include <QSysInfo>
#include <QUuid>
#include <QVariantMap>
@@ -21,6 +20,7 @@
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
@@ -32,7 +32,7 @@
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h>
#include "core/utils/swiftBridge.h"
#endif
using namespace amnezia;
@@ -56,6 +56,38 @@ QString getSubscriptionStatusForRenewal(const ApiConfig &apiConfig)
return QStringLiteral("active");
}
QString normalizeCaptchaSolution(const QString &captchaSolution)
{
QString normalizedSolution;
normalizedSolution.reserve(captchaSolution.size());
for (const QChar &ch : captchaSolution) {
const ushort u = ch.unicode();
if (u >= '0' && u <= '9') {
normalizedSolution += ch;
} else if (u >= 0xFF10 && u <= 0xFF19) {
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
}
}
return normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
}
bool fillCaptchaInfoFromResponse(const QByteArray &responseBody, SubscriptionController::CaptchaInfo &captchaInfo)
{
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (!jsonDoc.isObject()) {
return false;
}
const QJsonObject jsonObj = jsonDoc.object();
if (!jsonObj.contains(QStringLiteral("captcha_id")) || !jsonObj.contains(QStringLiteral("captcha_image"))) {
return false;
}
captchaInfo.captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
captchaInfo.captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
captchaInfo.hint = jsonObj.value(QStringLiteral("hint")).toString();
captchaInfo.isRequired = true;
return true;
}
}
@@ -65,39 +97,6 @@ SubscriptionController::SubscriptionController(SecureServersRepository* serversR
{
}
QJsonObject SubscriptionController::GatewayRequestData::toJsonObject() const
{
QJsonObject obj;
if (!osVersion.isEmpty()) {
obj[apiDefs::key::osVersion] = osVersion;
}
if (!appVersion.isEmpty()) {
obj[apiDefs::key::appVersion] = appVersion;
}
if (!appLanguage.isEmpty()) {
obj[apiDefs::key::appLanguage] = appLanguage;
}
if (!installationUuid.isEmpty()) {
obj[apiDefs::key::uuid] = installationUuid;
}
if (!userCountryCode.isEmpty()) {
obj[apiDefs::key::userCountryCode] = userCountryCode;
}
if (!serverCountryCode.isEmpty()) {
obj[apiDefs::key::serverCountryCode] = serverCountryCode;
}
if (!serviceType.isEmpty()) {
obj[apiDefs::key::serviceType] = serviceType;
}
if (!serviceProtocol.isEmpty()) {
obj[apiDefs::key::serviceProtocol] = serviceProtocol;
}
if (!authData.isEmpty()) {
obj[apiDefs::key::authData] = authData;
}
return obj;
}
SubscriptionController::ProtocolData SubscriptionController::generateProtocolData(const QString &protocol)
{
ProtocolData protocolData;
@@ -112,13 +111,15 @@ SubscriptionController::ProtocolData SubscriptionController::generateProtocolDat
return protocolData;
}
void SubscriptionController::appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload)
QString SubscriptionController::publicKeyForProtocol(const QString &protocol, const ProtocolData &protocolData)
{
if (protocol == configKey::awg) {
apiPayload[apiDefs::key::publicKey] = protocolData.wireGuardClientPubKey;
} else if (protocol == configKey::vless) {
apiPayload[apiDefs::key::publicKey] = protocolData.xrayUuid;
return protocolData.wireGuardClientPubKey;
}
if (protocol == configKey::vless) {
return protocolData.xrayUuid;
}
return {};
}
ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
@@ -156,23 +157,14 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
// TODO looks like this block can be removed after v1 configs EOL
serverProtocolConfig[configKey::junkPacketCount] = clientProtocolConfig.value(configKey::junkPacketCount);
serverProtocolConfig[configKey::junkPacketMinSize] = clientProtocolConfig.value(configKey::junkPacketMinSize);
serverProtocolConfig[configKey::junkPacketMaxSize] = clientProtocolConfig.value(configKey::junkPacketMaxSize);
serverProtocolConfig[configKey::initPacketJunkSize] = clientProtocolConfig.value(configKey::initPacketJunkSize);
serverProtocolConfig[configKey::responsePacketJunkSize] = clientProtocolConfig.value(configKey::responsePacketJunkSize);
serverProtocolConfig[configKey::initPacketMagicHeader] = clientProtocolConfig.value(configKey::initPacketMagicHeader);
serverProtocolConfig[configKey::responsePacketMagicHeader] = clientProtocolConfig.value(configKey::responsePacketMagicHeader);
serverProtocolConfig[configKey::underloadPacketMagicHeader] = clientProtocolConfig.value(configKey::underloadPacketMagicHeader);
serverProtocolConfig[configKey::transportPacketMagicHeader] = clientProtocolConfig.value(configKey::transportPacketMagicHeader);
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
serverProtocolConfig[configKey::cookieReplyPacketJunkSize] = clientProtocolConfig.value(configKey::cookieReplyPacketJunkSize);
serverProtocolConfig[configKey::transportPacketJunkSize] = clientProtocolConfig.value(configKey::transportPacketJunkSize);
serverProtocolConfig[configKey::specialJunk1] = clientProtocolConfig.value(configKey::specialJunk1);
serverProtocolConfig[configKey::specialJunk2] = clientProtocolConfig.value(configKey::specialJunk2);
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = clientProtocolConfig.value(key);
if (value.isString() && !value.toString().isEmpty()) {
serverProtocolConfig[key] = value;
}
}
//
@@ -198,9 +190,6 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) {
QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object();
if (responseObj.contains(apiDefs::key::supportedProtocols)) {
apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray());
}
if (responseObj.contains(apiDefs::key::serviceInfo)) {
apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject());
}
@@ -212,7 +201,7 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson
ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase)
{
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase), m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
return gatewayController.post(endpoint, apiPayload, responseBody);
}
@@ -220,31 +209,18 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
const QString &serviceProtocol, const ProtocolData &protocolData,
CaptchaInfo &captchaInfo)
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
captchaInfo.hint = jsonObj.value("hint").toString();
captchaInfo.isRequired = true;
}
fillCaptchaInfoFromResponse(responseBody, captchaInfo);
return errorCode;
}
@@ -278,20 +254,15 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::ApiConfigEmptyError;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload.insert(apiDefs::key::email, trimmedEmail);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::email, trimmedEmail)
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
@@ -332,19 +303,13 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
const QString &transactionId, bool isTestPurchase,
int *duplicateServerIndex)
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload[apiDefs::key::transactionId] = transactionId;
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::transactionId, transactionId)
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
@@ -399,8 +364,6 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject);
ApiV2ServerConfig* apiV2 = &apiV2ServerConfig;
apiV2->apiConfig.vpnKey = normalizedKey;
apiV2->apiConfig.isTestPurchase = isTestPurchase;
apiV2->apiConfig.isInAppPurchase = true;
apiV2->apiConfig.subscriptionExpiredByServer = false;
apiV2->crc = crc;
@@ -410,7 +373,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent)
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
@@ -418,29 +382,43 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
}
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString serviceProtocol = apiV2->serviceProtocol();
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
newCountryCode,
apiV2->serviceType(),
serviceProtocol,
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
if (!newCountryCode.isEmpty()) {
const auto availableCountries = apiV2->apiConfig.availableCountries;
for (const auto &country : availableCountries) {
const auto countryObject = country.toObject();
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
continue;
}
if (isConnectEvent) {
apiPayload[apiDefs::key::isConnectEvent] = true;
const auto availableProtocols = countryObject.value(apiDefs::key::availableProtocols).toArray();
if (!availableProtocols.isEmpty() && !availableProtocols.contains(serviceProtocol)) {
serviceProtocol = availableProtocols.first().toString();
}
break;
}
}
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, newCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::isConnectEvent, isConnectEvent ? QJsonValue(true) : QJsonValue())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfoOut) {
if (fillCaptchaInfoFromResponse(responseBody, *captchaInfoOut) && usedProtocolDataOut) {
*usedProtocolDataOut = protocolData;
}
}
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
ApiV2ServerConfig expiredApiV2 = *apiV2;
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
@@ -450,29 +428,38 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
return errorCode;
}
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
}
ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
const ProtocolData &protocolData, const QByteArray &responseBody)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
QJsonObject serverConfigJson;
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
ErrorCode errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig newApiV2Config = ApiV2ServerConfig::fromJson(serverConfigJson);
ApiV2ServerConfig* newApiV2 = &newApiV2Config;
newApiV2->apiConfig.vpnKey = apiV2->apiConfig.vpnKey;
newApiV2->apiConfig.isTestPurchase = apiV2->apiConfig.isTestPurchase;
newApiV2->apiConfig.isInAppPurchase = apiV2->apiConfig.isInAppPurchase;
newApiV2->apiConfig.subscriptionExpiredByServer = false;
newApiV2->authData = apiV2->authData;
newApiV2->crc = apiV2->crc;
if (apiV2->nameOverriddenByUser) {
newApiV2->name = apiV2->name;
newApiV2->displayName = apiV2->displayName;
@@ -484,6 +471,50 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode,
bool isConnectEvent, const ProtocolData &protocolData,
const QString &captchaId, const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString serviceProtocol = apiV2->serviceProtocol();
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, newCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::captchaId, captchaId)
.addField(apiDefs::key::captchaSolution, normalizeCaptchaSolution(captchaSolution))
.addField(apiDefs::key::isConnectEvent, isConnectEvent ? QJsonValue(true) : QJsonValue())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
if (retryCaptchaOut
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
}
if (errorCode == ErrorCode::ApiSubscriptionExpiredError && !apiV2->apiConfig.isInAppPurchase) {
ApiV2ServerConfig expiredApiV2 = *apiV2;
expiredApiV2.apiConfig.subscriptionExpiredByServer = true;
m_serversRepository->editServer(serverId, expiredApiV2.toJson(),
serverConfigUtils::configTypeFromJson(expiredApiV2.toJson()));
}
return errorCode;
}
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
}
ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
@@ -495,18 +526,12 @@ ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
return ErrorCode::NoError;
}
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
apiV2->apiConfig.serverCountryCode,
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, apiV2->apiConfig.serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QByteArray responseBody;
@@ -532,18 +557,13 @@ ErrorCode SubscriptionController::deactivateExternalDevice(const QString &server
return ErrorCode::NoError;
}
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
uuid,
apiV2->apiConfig.userCountryCode,
serverCountryCode,
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::installationUuid, uuid)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QByteArray responseBody;
@@ -571,19 +591,14 @@ ErrorCode SubscriptionController::exportNativeConfig(const QString &serverId, co
QString protocol = configKey::awg;
ProtocolData protocolData = generateProtocolData(protocol);
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
serverCountryCode,
apiV2->serviceType(),
protocol,
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(protocol, protocolData, apiPayload);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, protocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(protocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase);
@@ -606,18 +621,13 @@ ErrorCode SubscriptionController::revokeNativeConfig(const QString &serverId, co
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString protocol = configKey::awg;
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
serverCountryCode,
apiV2->serviceType(),
protocol,
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, protocol)
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase);
@@ -648,19 +658,20 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers)
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
{
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
return ErrorCode::NoError;
}
if (!hasInstalledContainers) {
return updateServiceFromGateway(serverId, "", true);
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
}
if (isApiKeyExpired(serverId)) {
qDebug() << "attempt to update api config by expires_at event";
return updateServiceFromGateway(serverId, "", true);
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
}
return ErrorCode::NoError;
@@ -681,7 +692,7 @@ void SubscriptionController::removeApiConfig(const QString &serverId)
.arg(hostName)
.arg("");
AmneziaVPN::removeVPNC(vpncName.toStdString());
SWIFT_BRIDGE_NAMESPACE::removeVPNC(vpncName.toStdString());
#endif
apiV2->dns1.clear();
@@ -752,6 +763,36 @@ bool SubscriptionController::isVlessProtocol(const QString &serverId) const
return apiV2.has_value() && apiV2->serviceProtocol() == "vless";
}
QString SubscriptionController::currentProtocol(const QString &serverId) const
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
return apiV2.has_value() ? apiV2->serviceProtocol() : QString();
}
QStringList SubscriptionController::availableProtocols(const QString &serverId) const
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return {};
}
const auto currentCountryCode = apiV2->apiConfig.serverCountryCode;
const auto availableCountries = apiV2->apiConfig.availableCountries;
QStringList protocols;
for (const auto &country : availableCountries) {
const auto countryObject = country.toObject();
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != currentCountryCode) {
continue;
}
for (const auto &protocol : countryObject.value(apiDefs::key::availableProtocols).toArray()) {
protocols.push_back(protocol.toString());
}
break;
}
return protocols;
}
ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &productId,
int *duplicateServerIndex)
@@ -893,20 +934,13 @@ ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonO
}
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
"",
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::cliVersion, QString(APP_VERSION))
.addField(apiDefs::key::subscriptionStatus, getSubscriptionStatusForRenewal(apiV2->apiConfig))
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/account_info"), apiPayload, responseBody, isTestPurchase);
@@ -914,7 +948,8 @@ ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonO
return errorCode;
}
accountInfo = QJsonDocument::fromJson(responseBody).object();
accountInfo = QJsonDocument::fromJson(responseBody).object();
return ErrorCode::NoError;
}
@@ -931,25 +966,19 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
}
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
"",
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::cliVersion, QString(APP_VERSION))
.addField(apiDefs::key::subscriptionStatus, getSubscriptionStatusForRenewal(apiV2->apiConfig))
.build();
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
@@ -979,31 +1008,14 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut)
{
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject()};
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload["captcha_id"] = captchaId;
QString normalizedSolution;
normalizedSolution.reserve(captchaSolution.size());
for (const QChar &ch : captchaSolution) {
const ushort u = ch.unicode();
if (u >= '0' && u <= '9') {
normalizedSolution += ch;
} else if (u >= 0xFF10 && u <= 0xFF19) {
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
}
}
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::captchaId, captchaId)
.addField(apiDefs::key::captchaSolution, normalizeCaptchaSolution(captchaSolution))
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
@@ -1011,16 +1023,7 @@ ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &use
if (retryCaptchaOut
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
const QJsonObject jsonObj = jsonDoc.object();
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
retryCaptchaOut->isRequired = true;
}
}
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
}
return errorCode;
}

View File

@@ -27,21 +27,6 @@ public:
QString xrayUuid;
};
struct GatewayRequestData
{
QString osVersion;
QString appVersion;
QString appLanguage;
QString installationUuid;
QString userCountryCode;
QString serverCountryCode;
QString serviceType;
QString serviceProtocol;
QJsonObject authData;
QJsonObject toJsonObject() const;
};
struct CaptchaInfo {
QString captchaId;
QString captchaImageBase64;
@@ -53,7 +38,8 @@ public:
SecureAppSettingsRepository* appSettingsRepository);
ProtocolData generateProtocolData(const QString &protocol);
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
static QString publicKeyForProtocol(const QString &protocol, const ProtocolData &protocolData);
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
@@ -66,7 +52,12 @@ public:
const QString &transactionId, bool isTestPurchase,
int *duplicateServerIndex = nullptr);
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent);
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
ErrorCode resolveUpdateServiceCaptcha(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
const ProtocolData &protocolData, const QString &captchaId,
const QString &captchaSolution, CaptchaInfo *retryCaptchaOut = nullptr);
ErrorCode deactivateDevice(const QString &serverId);
@@ -78,7 +69,8 @@ public:
ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey);
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers);
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
void removeApiConfig(const QString &serverId);
@@ -86,6 +78,8 @@ public:
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
bool isVlessProtocol(const QString &serverId) const;
QString currentProtocol(const QString &serverId) const;
QStringList availableProtocols(const QString &serverId) const;
ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo);
QFuture<QPair<ErrorCode, QString>> getRenewalLink(const QString &serverId);
@@ -115,8 +109,10 @@ private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool isApiKeyExpired(const QString &serverId) const;
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
ErrorCode extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
const ProtocolData &protocolData, QJsonObject &serverConfigJson);
ErrorCode applyUpdatedServiceConfig(const QString &serverId, const QString &serviceProtocol,
const ProtocolData &protocolData, const QByteArray &responseBody);
void updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
const QString &serviceProtocol, const QString &userCountryCode,
const QByteArray &apiResponseBody);

View File

@@ -6,6 +6,7 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/payloadSender.h"
#include "core/utils/utilities.h"
#include "core/utils/serverConfigUtils.h"
#include "version.h"
@@ -30,7 +31,6 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::setConnectionStateRequested, m_vpnConnection, &VpnConnection::setConnectionState, Qt::QueuedConnection);
connect(this, &ConnectionController::killSwitchModeChangedRequested, m_vpnConnection, &VpnConnection::onKillSwitchModeChanged, Qt::QueuedConnection);
#ifdef Q_OS_ANDROID
connect(this, &ConnectionController::restoreConnectionRequested, m_vpnConnection, &VpnConnection::restoreConnection, Qt::QueuedConnection);
@@ -44,19 +44,99 @@ bool ConnectionController::isConnected() const
void ConnectionController::setConnectionState(Vpn::ConnectionState state)
{
if (m_vpnConnection) {
emit setConnectionStateRequested(state);
emit connectionStateChanged(state);
}
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
{
const auto kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
return ErrorCode::LegacyApiV1NotSupportedError;
case serverConfigUtils::ConfigType::Invalid:
default:
return ErrorCode::InternalError;
}
}
ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) const
{
if (serverId.isEmpty()) {
return ErrorCode::InternalError;
}
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
DockerContainer container = DockerContainer::None;
const ErrorCode errorCode = defaultContainerForServer(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (container == DockerContainer::None) {
if (serverConfigUtils::isApiV2Subscription(kind)) {
return ErrorCode::NoError;
}
return ErrorCode::NoInstalledContainersError;
}
if (ContainerUtils::isUnsupportedContainer(container)) {
return ErrorCode::LegacyContainerNotSupportedError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
return ErrorCode::NoError;
}
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container)
{
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
ContainerConfig containerConfigModel;
QPair<QString, QString> dns;
QString hostName;
@@ -120,10 +200,6 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
return ErrorCode::InternalError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
containerConfigModel, container);
@@ -140,6 +216,11 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
return errorCode;
}
const auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (apiV2.has_value() && !apiV2->sendPayload.isEmpty()) {
PayloadSender::sendAll(apiV2->sendPayload);
}
emit openConnectionRequested(serverId, container, vpnConfiguration);
return ErrorCode::NoError;
}

View File

@@ -34,6 +34,8 @@ public:
QJsonObject& vpnConfiguration,
DockerContainer& container);
ErrorCode isConnectionSupported(const QString &serverId) const;
ErrorCode openConnection(const QString &serverId);
void closeConnection();
@@ -65,7 +67,6 @@ signals:
void connectionStateChanged(Vpn::ConnectionState state);
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
void closeConnectionRequested();
void setConnectionStateRequested(Vpn::ConnectionState state);
void killSwitchModeChangedRequested(bool enabled);
#ifdef Q_OS_ANDROID
@@ -73,6 +74,8 @@ signals:
#endif
private:
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
VpnConnection* m_vpnConnection;

View File

@@ -10,6 +10,7 @@
#include "core/controllers/coreSignalHandlers.h"
#include "logger.h"
#include "secureQSettings.h"
#include "core/utils/appUiConfig.h"
#if defined(Q_OS_ANDROID)
#include "core/utils/installedAppsImageProvider.h"
@@ -18,11 +19,12 @@
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h>
#include "core/utils/swiftBridge.h"
#endif
CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
QQmlApplicationEngine *engine, QObject *parent)
QQmlApplicationEngine *engine, QObject *parent,
bool skipPlatformControllerInit)
: QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine)
{
initRepositories();
@@ -31,8 +33,10 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initControllers();
initSignalHandlers();
initAndroidController();
initAppleController();
if (!skipPlatformControllerInit) {
initAndroidController();
initAppleController();
}
initLogging();
m_translator = new QTranslator(this);
@@ -191,7 +195,7 @@ void CoreController::initControllers()
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
setQmlContextProperty("LanguageUiController", m_languageUiController);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, m_languageUiController, this);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this);
@@ -255,7 +259,7 @@ void CoreController::initAppleController()
{
#ifdef Q_OS_IOS
IosController::Instance()->initialize();
QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_appSettingsRepository->isScreenshotsEnabled()); });
QTimer::singleShot(0, this, [this]() { SWIFT_BRIDGE_NAMESPACE::toggleScreenshots(m_appSettingsRepository->isScreenshotsEnabled()); });
#endif
}
@@ -282,6 +286,10 @@ void CoreController::initSignalHandlers()
if (m_serversUiController->hasServersFromGatewayApi()) {
m_apiNewsUiController->fetchNews(false);
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
m_updateController->checkForUpdates();
#endif
}
void CoreController::updateTranslator(const QLocale &locale)
@@ -291,15 +299,15 @@ void CoreController::updateTranslator(const QLocale &locale)
}
QStringList availableTranslations;
QDirIterator it(":/translations", QStringList("amneziavpn_*.qm"), QDir::Files);
QDirIterator it(":/translations", QStringList(APP_TS_PREFIX "_*.qm"), QDir::Files);
while (it.hasNext()) {
availableTranslations << it.next();
}
// This code allow to load translation for the language only, without country code
const QString lang = locale.name().split("_").first();
const QString translationFilePrefix = QString(":/translations/amneziavpn_") + lang;
QString strFileName = QString(":/translations/amneziavpn_%1.qm").arg(locale.name());
const QString translationFilePrefix = QString(":/translations/" APP_TS_PREFIX "_") + lang;
QString strFileName = QString(":/translations/" APP_TS_PREFIX "_%1.qm").arg(locale.name());
for (const QString &translation : availableTranslations) {
if (translation.contains(translationFilePrefix)) {
strFileName = translation;
@@ -310,7 +318,7 @@ void CoreController::updateTranslator(const QLocale &locale)
if (m_translator->load(strFileName)) {
QCoreApplication::installTranslator(m_translator);
} else {
if (m_translator->load(QString(":/translations/amneziavpn_en.qm"))) {
if (m_translator->load(QString(":/translations/" APP_TS_PREFIX "_en.qm"))) {
QCoreApplication::installTranslator(m_translator);
}
}

View File

@@ -90,7 +90,8 @@ class CoreController : public QObject
public:
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
QQmlApplicationEngine *engine, QObject *parent = nullptr);
QQmlApplicationEngine *engine, QObject *parent = nullptr,
bool skipPlatformControllerInit = false);
PageController* pageController() const;
void setQmlRoot();
@@ -114,7 +115,7 @@ protected:
AppSplitTunnelingModel* appSplitTunnelingModelProtected() const { return m_appSplitTunnelingModel; }
IpSplitTunnelingModel* ipSplitTunnelingModelProtected() const { return m_ipSplitTunnelingModel; }
LanguageModel* languageModelProtected() const { return m_languageModel; }
ConnectionUiController* connectionUiControllerProtected() const { return m_connectionUiController; }
InstallUiController* installUiControllerProtected() const { return m_installUiController; }
ImportController* importCoreControllerProtected() const { return m_importCoreController; }
ExportController* exportControllerProtected() const { return m_exportController; }

View File

@@ -1,6 +1,7 @@
#include "coreSignalHandlers.h"
#include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/errorCodes.h"
@@ -33,7 +34,6 @@
#include "core/controllers/connectionController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h"
@@ -48,7 +48,7 @@
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h>
#include "core/utils/swiftBridge.h"
#endif
CoreSignalHandlers::CoreSignalHandlers(CoreController* coreController, QObject* parent)
@@ -145,7 +145,9 @@ void CoreSignalHandlers::initExportControllerHandler()
});
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](const QString &serverId, int row, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, row, container);
QtConcurrent::run([this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
});
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
@@ -156,15 +158,17 @@ void CoreSignalHandlers::initExportControllerHandler()
void CoreSignalHandlers::initImportControllerHandler()
{
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
if (!m_coreController->m_connectionController->isConnected()) {
int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
if (m_coreController->m_connectionUiController->isConnected()) {
return;
}
const int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
});
}
@@ -176,17 +180,14 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
if (processedServerId.isEmpty()) {
return;
}
QJsonArray availableCountries;
QString serverCountryCode;
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
if (apiV2.has_value()) {
availableCountries = apiV2->apiConfig.availableCountries;
serverCountryCode = apiV2->apiConfig.serverCountryCode;
if (!apiV2.has_value()) {
return;
}
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode);
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
});
}
@@ -205,12 +206,12 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
});
}, Qt::DirectConnection);
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
});
}, Qt::DirectConnection);
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
&InstallController::clearCachedProfile);
@@ -237,13 +238,16 @@ void CoreSignalHandlers::initLanguageHandler()
connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->changeLanguage(m_coreController->m_languageUiController->getSystemLanguageEnum());
});
connect(m_coreController->m_settingsUiController, &SettingsUiController::appLanguageChanged, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->onAppLanguageChanged(m_coreController->m_settingsController->getAppLanguage());
});
}
void CoreSignalHandlers::initAutoConnectHandler()
{
if (m_coreController->m_settingsUiController->isAutoConnectEnabled()
&& !m_coreController->m_serversController->getDefaultServerId().isEmpty()) {
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); });
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->toggleConnection(); });
}
}
@@ -284,6 +288,8 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
connect(m_coreController->m_usersController, &UsersController::clientRenamed,
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
m_coreController->m_exportController, &ExportController::revokeFinished);
}
void CoreSignalHandlers::initSitesModelUpdateHandler()
@@ -348,6 +354,9 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
{
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
connect(m_coreController->m_connectionUiController, &ConnectionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()
@@ -404,7 +413,7 @@ void CoreSignalHandlers::initIosImportHandler()
void CoreSignalHandlers::initIosSettingsHandler()
{
#ifdef Q_OS_IOS
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); });
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, [](bool enabled) { SWIFT_BRIDGE_NAMESPACE::toggleScreenshots(enabled); });
#endif
}
@@ -431,9 +440,6 @@ void CoreSignalHandlers::initNotificationHandler()
void CoreSignalHandlers::initUpdateFoundHandler()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished, m_coreController->m_updateUiController,
&UpdateUiController::checkForUpdates);
connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
const QString version = m_coreController->m_updateUiController->getVersion();
const QString updateId = version.isEmpty() ? QStringLiteral("update") : QStringLiteral("update-%1").arg(version);

View File

@@ -12,14 +12,14 @@
#include <QPromise>
#include <QUrl>
#include "QBlockCipher.h"
#include "QRsa.h"
#include <openssl/rsa.h>
#include "amneziaApplication.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/utilities.h"
#include "cryptoUtils.h"
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
@@ -45,15 +45,75 @@ namespace
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
{
QStringList shuffled = proxyUrls;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
return shuffled;
}
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
{
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
}
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
{
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
if (decryptedPayload.isEmpty()) {
return false;
}
} else {
decryptedPayload = encryptedPayload;
}
return true;
}
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
{
if (cachedProxyUrlsEncrypted.isEmpty()) {
return {};
}
QByteArray cachedProxyUrlsDecrypted;
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
qCritical() << "error decrypting cached proxy urls payload";
return {};
}
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent)
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_appSettingsRepository(appSettingsRepository)
{
}
@@ -87,40 +147,29 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
}
#endif
QSimpleCrypto::QBlockCipher blockCipher;
encRequestData.key = blockCipher.generatePrivateSalt(32);
encRequestData.iv = blockCipher.generatePrivateSalt(32);
encRequestData.salt = blockCipher.generatePrivateSalt(8);
encRequestData.key = CryptoUtils::generateRandomBytes(32);
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
QJsonObject keyPayload;
keyPayload[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[apiDefs::key::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload;
try {
QSimpleCrypto::QRsa rsa;
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
if (publicKey == nullptr) {
qCritical() << "error loading public key from environment variables";
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
EVP_PKEY *publicKey = nullptr;
try {
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QSimpleCrypto::QRsa rsa;
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
} catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables";
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
"", encRequestData.salt);
} catch (...) {
Utils::logException();
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
qCritical() << "error when encrypting the request body";
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
@@ -142,11 +191,11 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
try {
QSimpleCrypto::QBlockCipher blockCipher;
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
if (!decrypted.isEmpty()) {
result.decryptedBody = decrypted;
result.isDecryptionSuccessful = true;
} catch (...) {
} else {
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
}
@@ -265,7 +314,6 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
}
if (!decryptionResult.isDecryptionSuccessful) {
Utils::logException();
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
@@ -308,8 +356,9 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
@@ -355,8 +404,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
@@ -372,10 +419,12 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (proxyStorageUrls.empty()) {
qDebug() << "empty storage endpoint list";
return {};
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
@@ -390,26 +439,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
EVP_PKEY *privateKey = nullptr;
QByteArray responseBody;
try {
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();
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue;
}
@@ -420,6 +451,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
return endpoints;
} else {
auto replyError = reply->error();
@@ -431,7 +464,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
reply->deleteLater();
}
}
return {};
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
@@ -573,10 +606,12 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
}
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete)
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
{
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({});
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
return;
}
@@ -589,33 +624,17 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
connect(reply, &QNetworkReply::finished, this,
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
return;
}
@@ -623,13 +642,9 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
onComplete(shuffledProxyUrls(endpoints));
return;
}
@@ -638,7 +653,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
});
}

View File

@@ -16,13 +16,16 @@
#include "platforms/ios/ios_controller.h"
#endif
class SecureAppSettingsRepository;
class GatewayController : public QObject
{
Q_OBJECT
public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
@@ -55,7 +58,7 @@ private:
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete);
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
@@ -65,6 +68,7 @@ private:
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
inline static QString m_proxyUrl;
};

View File

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

View File

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

View File

@@ -14,6 +14,7 @@
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocolConfig.h"
#include "version.h"
using namespace amnezia;
@@ -220,7 +221,10 @@ ExportController::ExportResult ExportController::generateWireGuardConfig(const Q
result.config.append(line + "\n");
}
result.qrCodes << generateSingleQrCode(result.config.toUtf8());
const QString qr = qrCodeUtils::generatePlainQrCodeImage(result.config.toUtf8());
if (!qr.isEmpty()) {
result.qrCodes << qr;
}
return result;
}
@@ -251,7 +255,10 @@ ExportController::ExportResult ExportController::generateAwgConfig(const QString
result.config.append(line + "\n");
}
result.qrCodes << generateSingleQrCode(result.config.toUtf8());
const QString qr = qrCodeUtils::generatePlainQrCodeImage(result.config.toUtf8());
if (!qr.isEmpty()) {
result.qrCodes << qr;
}
return result;
}
@@ -337,7 +344,7 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
}
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, APPLICATION_NAME);
return result;
}
@@ -369,9 +376,3 @@ QList<QString> ExportController::generateQrCodesFromConfig(const QByteArray &dat
{
return qrCodeUtils::generateQrCodeImageSeries(data);
}
QString ExportController::generateSingleQrCode(const QByteArray &data)
{
auto qr = qrCodeUtils::generateQrCode(data);
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
}

View File

@@ -48,6 +48,7 @@ signals:
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(const QString &serverId, DockerContainer container);
void revokeClientRequested(const QString &serverId, int row, DockerContainer container);
void revokeFinished(ErrorCode errorCode);
void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container);
public slots:
@@ -68,7 +69,6 @@ private:
QString generateVpnUrl(const QByteArray &compressedConfig);
QList<QString> generateQrCodesFromConfig(const QByteArray &data);
QString generateSingleQrCode(const QByteArray &data);
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;

View File

@@ -486,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = configKey::amneziaOpenvpn;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp("dhcp-option DNS (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)");
QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(data);
@@ -511,9 +511,10 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) {
continue;
} else {
QStringList parts = trimmedLine.split(" = ");
if (parts.count() == 2) {
configMap[parts.at(0).trimmed()] = parts.at(1).trimmed();
const qsizetype separatorIndex = trimmedLine.indexOf('=');
if (separatorIndex > 0) {
configMap[trimmedLine.left(separatorIndex).trimmed()] =
trimmedLine.mid(separatorIndex + 1).trimmed();
}
}
}
@@ -566,53 +567,26 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
lastConfig[configKey::persistentKeepAlive] = configMap.value(protocols::wireguard::PersistentKeepalive);
}
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(
configMap.value(protocols::wireguard::AllowedIPs).split(", "));
const QStringList allowedIps = configMap.value(protocols::wireguard::AllowedIPs).split(
QRegularExpression("\\s*,\\s*"), Qt::SkipEmptyParts);
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(allowedIps);
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
QString protocolName = configKey::wireguard;
QString protocolVersion;
ConfigTypes detectedType = ConfigTypes::WireGuard;
const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize,
configKey::junkPacketMaxSize, configKey::initPacketJunkSize,
configKey::responsePacketJunkSize, configKey::initPacketMagicHeader,
configKey::responsePacketMagicHeader, configKey::underloadPacketMagicHeader,
configKey::transportPacketMagicHeader };
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
configKey::transportPacketJunkSize,
configKey::specialJunk1, configKey::specialJunk2, configKey::specialJunk3,
configKey::specialJunk4, configKey::specialJunk5
};
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAllRequiredFields) {
for (const QString &field : requiredJunkFields) {
lastConfig[field] = configMap.value(field);
}
for (const QString &field : optionalJunkFields) {
if (!configMap.value(field).isEmpty()) {
lastConfig[field] = configMap.value(field);
bool hasAwgKeys = std::any_of(awgProtocolKeys.begin(), awgProtocolKeys.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAwgKeys) {
for (const QString &key : awgProtocolKeys) {
if (!configMap.value(key).isEmpty()) {
lastConfig[key] = configMap.value(key);
}
}
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
!configMap.value(configKey::specialJunk2).isEmpty() ||
!configMap.value(configKey::specialJunk3).isEmpty() ||
!configMap.value(configKey::specialJunk4).isEmpty() ||
!configMap.value(configKey::specialJunk5).isEmpty();
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
protocolVersion = "2";
} else if (hasSpecialJunk && !hasCookieReplyPacketJunkSize && !hasTransportPacketJunkSize) {
protocolVersion = "1.5";
}
protocolName = configKey::awg;
detectedType = ConfigTypes::Awg;
}
@@ -630,9 +604,6 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
wireguardConfig[configKey::isThirdPartyConfig] = true;
wireguardConfig[configKey::port] = port;
wireguardConfig[configKey::transportProto] = protocols::openvpn::defaultTransportProto;
if (protocolName == configKey::awg && !protocolVersion.isEmpty()) {
wireguardConfig[configKey::protocolVersion] = protocolVersion;
}
QJsonObject containers;
QString containerName = (protocolName == configKey::awg) ? configKey::amneziaAwg : configKey::amneziaWireguard;
@@ -645,7 +616,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = containerName;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp(
"DNS = "
@@ -666,11 +637,24 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
{
QJsonParseError parserErr;
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
if (parserErr.error != QJsonParseError::NoError || !jsonConf.isObject()) {
qDebug() << "Xray config JSON parse failed:" << parserErr.errorString();
return QJsonObject();
}
const QJsonObject parsedConfig = jsonConf.object();
if (!parsedConfig.value(protocols::xray::inbounds).isArray()
|| !parsedConfig.value(protocols::xray::outbounds).isArray()) {
qDebug() << "Xray config is missing inbounds or outbounds";
return QJsonObject();
}
const QString serializedConfig = QString::fromUtf8(jsonConf.toJson());
QJsonObject xrayVpnConfig;
xrayVpnConfig[configKey::config] = jsonConf.toJson().constData();
xrayVpnConfig[configKey::config] = serializedConfig;
QJsonObject lastConfig;
lastConfig[configKey::lastConfig] = jsonConf.toJson().constData();
lastConfig[configKey::lastConfig] = serializedConfig;
lastConfig[configKey::isThirdPartyConfig] = true;
QJsonObject containers;
@@ -699,7 +683,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
? configKey::amneziaSsxray
: configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}

View File

@@ -2,6 +2,7 @@
#include "core/models/protocolConfig.h"
#include <QCoreApplication>
#include <QDebug>
#include <QEventLoop>
#include <QFutureWatcher>
@@ -10,6 +11,7 @@
#include <QtConcurrent>
#include "core/configurators/configuratorBase.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -20,7 +22,6 @@
#include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/installers/telemtInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
@@ -72,6 +73,16 @@ namespace
}
return false;
}
QString buildRemoveContainerScript(const amnezia::ScriptVars &vars, bool removeDataVolume)
{
QString script = SshSession::replaceVars(amnezia::scriptData(SharedScriptType::remove_container), vars);
if (removeDataVolume) {
script += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
script = SshSession::replaceVars(script, vars);
}
return script;
}
}
InstallController::InstallController(SecureServersRepository *serversRepository,
@@ -93,7 +104,7 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
bool isUpdate)
{
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
SshSession sshSession(this);
SshSession sshSession;
ErrorCode e = ErrorCode::NoError;
e = isUserInSudo(credentials, sshSession);
@@ -120,14 +131,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
amnezia::ScriptVars removeContainerVars =
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
if (!isUpdate) {
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
}
sshSession.runScript(credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
removeContainerVars));
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start";
@@ -146,14 +153,23 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e;
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
}
setupServerFirewall(credentials, sshSession);
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
return startupContainerWorker(credentials, container, config, sshSession);
}
ErrorCode InstallController::updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
{
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
@@ -162,11 +178,11 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
}
if (container == DockerContainer::MtProxy) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession(this);
SshSession sshSession;
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::Telemt) {
ServerCredentials credentials = adminConfig->credentials();
SshSession sshSession(this);
SshSession sshSession;
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
adminConfig->updateContainerConfig(container, newConfig);
@@ -182,43 +198,31 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
bool xrayServerSettingsChanged = false;
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
xrayServerSettingsChanged =
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
}
}
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
errorCode = setupContainer(credentials, container, newConfig, true);
} else {
// Reinstall pulls the latest container image, so the server runs the latest protocol version
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
if (auto* awgConfig = newConfig.getAwgProtocolConfig()) {
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
}
}
} else if (container != DockerContainer::Xray && container != DockerContainer::SSXray) {
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
if (errorCode == ErrorCode::NoError) {
errorCode = startupContainerWorker(credentials, container, newConfig, sshSession);
}
}
const bool skipXrayInboundSync =
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
qDebug() << "InstallController::updateContainer applying Xray server inbound sync, reinstall="
<< reinstallRequired;
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
if (errorCode != ErrorCode::NoError) {
qDebug() << "InstallController::updateContainer Xray inbound sync failed, error="
<< static_cast<int>(errorCode);
if (errorCode == ErrorCode::NoError
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt)) {
const QString containerName = ContainerUtils::containerToString(container);
errorCode = sshSession.runScript(credentials, "sudo docker restart " + containerName);
}
}
@@ -228,7 +232,9 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
clearCachedProfile(serverId, container);
if (reinstallRequired) {
clearCachedProfile(serverId, container);
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
}
@@ -236,6 +242,41 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
return errorCode;
}
ErrorCode InstallController::updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig)
{
switch (m_serversRepository->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
auto config = m_serversRepository->selfHostedAdminConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
auto config = m_serversRepository->selfHostedUserConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedUser);
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
auto config = m_serversRepository->nativeConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::Native);
return ErrorCode::NoError;
}
default:
return ErrorCode::InternalError;
}
}
void InstallController::clearCachedProfile(const QString &serverId, DockerContainer container)
{
if (ContainerUtils::containerService(container) == ServiceType::Other) {
@@ -358,7 +399,7 @@ void InstallController::addEmptyServer(const ServerCredentials &credentials)
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
serverConfig.description = m_serversRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None;
@@ -373,6 +414,11 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
}
if (ContainerUtils::containerService(container) != ServiceType::Other) {
if ((container == DockerContainer::Xray || container == DockerContainer::SSXray)
&& containerConfig.protocolConfig.hasClientConfig()) {
return ErrorCode::NoError;
}
Proto protocol = ContainerUtils::defaultProtocol(container);
DnsSettings dnsSettings = {
@@ -460,6 +506,12 @@ ErrorCode InstallController::buildContainerWorker(const ServerCredentials &crede
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
return ErrorCode::DockerPullRateLimit;
if (stdOut.contains("returned a non-zero code")
|| stdOut.contains("failed to solve")
|| stdOut.contains("Unable to find image")
|| stdOut.contains("Couldn't connect to server"))
return ErrorCode::ServerDockerFailedError;
return error;
}
@@ -484,6 +536,8 @@ ErrorCode InstallController::runContainerWorker(const ServerCredentials &credent
return ErrorCode::ServerPortAlreadyAllocatedError;
if (stdOut.contains("invalid publish"))
return ErrorCode::ServerDockerFailedError;
if (stdOut.contains("Unable to find image") || stdOut.contains("No such image"))
return ErrorCode::ServerDockerFailedError;
return e;
}
@@ -673,13 +727,7 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
? QString(protocols::xray::defaultPort)
: oldXrayConfig->serverConfig.port;
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
? QString(protocols::xray::defaultPort)
: newXrayConfig->serverConfig.port;
if (oldPort != newPort) {
if (!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig)) {
return true;
}
}
@@ -696,18 +744,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
if (oldPort != newPort) {
return true;
}
const QString oldTransport = oldMt->transportMode.isEmpty() ? QString(
protocols::mtProxy::transportModeStandard)
: oldMt->transportMode;
const QString newTransport = newMt->transportMode.isEmpty() ? QString(
protocols::mtProxy::transportModeStandard)
: newMt->transportMode;
if (oldTransport != newTransport) {
return true;
}
if (oldMt->tlsDomain != newMt->tlsDomain) {
return true;
}
}
}
@@ -722,39 +758,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
if (oldPort != newPort) {
return true;
}
const QString oldTransport = oldT->transportMode.isEmpty()
? QString(protocols::telemt::transportModeStandard)
: oldT->transportMode;
const QString newTransport = newT->transportMode.isEmpty()
? QString(protocols::telemt::transportModeStandard)
: newT->transportMode;
if (oldTransport != newTransport) {
return true;
}
if (oldT->tlsDomain != newT->tlsDomain) {
return true;
}
if (oldT->maskEnabled != newT->maskEnabled) {
return true;
}
if (oldT->tlsEmulation != newT->tlsEmulation) {
return true;
}
if (oldT->useMiddleProxy != newT->useMiddleProxy) {
return true;
}
if (oldT->tag != newT->tag) {
return true;
}
const QString oldUser = oldT->userName.isEmpty()
? QString::fromUtf8(protocols::telemt::defaultUserName)
: oldT->userName;
const QString newUser = newT->userName.isEmpty()
? QString::fromUtf8(protocols::telemt::defaultUserName)
: newT->userName;
if (oldUser != newUser) {
return true;
}
}
}
@@ -794,9 +797,23 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
QString conntrackOut;
auto cbConntrack = [&](const QString &data, libssh::Client &) {
conntrackOut += data + "\n";
return ErrorCode::NoError;
};
sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::install_conntrack),
amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())),
cbConntrack, cbConntrack);
qDebug().noquote() << "InstallController::installDockerWorker install_conntrack:" << conntrackOut;
}
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
@@ -809,8 +826,19 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))
if (stdOut.contains("Container runtime is not supported"))
return ErrorCode::ServerContainerRuntimeNotSupported;
QRegularExpression notFoundRegex(
R"(^.*(?:sudo:|docker:).*not found.*$)",
QRegularExpression::MultilineOption);
if (notFoundRegex.match(stdOut).hasMatch()) {
return ErrorCode::ServerDockerFailedError;
}
if (stdOut.contains("Container runtime service not running"))
return ErrorCode::ContainerRuntimeServiceNotRunning;
return error;
}
@@ -847,7 +875,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired;
@@ -929,7 +957,7 @@ ErrorCode InstallController::rebootServer(const QString &serverId)
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
QString script = QString("sudo reboot");
@@ -957,7 +985,7 @@ ErrorCode InstallController::removeAllContainers(const QString &serverId)
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers));
if (errorCode == ErrorCode::NoError) {
@@ -979,13 +1007,12 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
amnezia::ScriptVars removeContainerVars =
SshSession sshSession;
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
ErrorCode errorCode = sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), removeContainerVars));
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
ErrorCode errorCode =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
if (errorCode == ErrorCode::NoError) {
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
@@ -1089,7 +1116,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
@@ -1132,7 +1159,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
ErrorCode InstallController::installServer(const ServerCredentials &credentials, DockerContainer container, int port,
TransportProto transportProto, bool &wasContainerInstalled)
{
SshSession sshSession(this);
SshSession sshSession;
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
if (errorCode) {
@@ -1170,7 +1197,7 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials,
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
serverConfig.description = m_serversRepository->nextAvailableServerName();
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
serverConfig.containers.insert(iterator.key(), iterator.value());
@@ -1201,7 +1228,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
@@ -1240,28 +1267,26 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
return ErrorCode::NoError;
}
ErrorCode InstallController::checkSshConnection(const ServerCredentials &credentials, QString &output,
ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback)
{
SshSession sshSession(this);
SshSession sshSession;
ErrorCode errorCode = ErrorCode::NoError;
ServerCredentials processedCredentials = credentials;
if (processedCredentials.secretData.contains("BEGIN") && processedCredentials.secretData.contains("PRIVATE KEY")) {
if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
if (!passphraseCallback) {
return ErrorCode::SshPrivateKeyError;
}
QString decryptedPrivateKey;
errorCode = sshSession.getDecryptedPrivateKey(processedCredentials, decryptedPrivateKey, passphraseCallback);
errorCode = sshSession.getDecryptedPrivateKey(credentials, decryptedPrivateKey, passphraseCallback);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
processedCredentials.secretData = decryptedPrivateKey;
credentials.secretData = decryptedPrivateKey;
}
output = sshSession.checkSshConnection(processedCredentials, errorCode);
output = sshSession.checkSshConnection(credentials, errorCode);
return errorCode;
}
@@ -1465,7 +1490,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = containerAndPortMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) {
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
continue;
}
@@ -1490,7 +1515,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = torOrDnsRegMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) {
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
continue;
}
@@ -1526,7 +1551,7 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve
return ErrorCode::InternalError;
}
const QString containerName = ContainerUtils::containerToString(container);
SshSession sshSession(this);
SshSession sshSession;
const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName)
: QStringLiteral("sudo docker stop %1").arg(containerName);
const ErrorCode runError = sshSession.runScript(credentials, script);
@@ -1566,7 +1591,7 @@ ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId,
stdOut += data;
return ErrorCode::NoError;
};
SshSession sshSession(this);
SshSession sshSession;
const QString script = QStringLiteral(
"sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'")
.arg(containerName);
@@ -1600,7 +1625,7 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out);
}
@@ -1623,7 +1648,7 @@ QString InstallController::fetchDockerContainerSecret(const QString &serverId, D
stdOut += data;
return ErrorCode::NoError;
};
SshSession sshSession(this);
SshSession sshSession;
const QString path = QStringLiteral("/data/secret");
const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path);
const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut);

View File

@@ -34,7 +34,12 @@ public:
~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
ErrorCode updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
ErrorCode rebootServer(const QString &serverId);
ErrorCode removeAllContainers(const QString &serverId);
@@ -64,7 +69,8 @@ public:
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig);
ErrorCode checkSshConnection(const ServerCredentials &credentials, QString &output, std::function<QString()> passphraseCallback = nullptr);
ErrorCode checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback = nullptr);
bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);

View File

@@ -698,7 +698,7 @@ ErrorCode UsersController::revokeXray(const int row,
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = sshSession->runScript(
credentials,
credentials,
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
@@ -758,14 +758,17 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
ContainerConfig containerCfg = adminConfig->containerConfig(container);
QString containerClientId = containerCfg.protocolConfig.clientId();
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
if (isAdminMatch) {
emit adminConfigRevoked(serverId, container);
}
emit clientRevoked(index);
emit clientsUpdated(m_clientsTable);
}
emit clientsUpdated(m_clientsTable);
emit revokeFinished(errorCode);
return errorCode;
}

View File

@@ -37,6 +37,7 @@ signals:
void clientAdded(const QJsonObject &client);
void clientRenamed(int row, const QString &newName);
void clientRevoked(int row);
void revokeFinished(ErrorCode errorCode);
void adminConfigRevoked(const QString &serverId, DockerContainer container);
public slots:

View File

@@ -8,7 +8,7 @@
#include "core/models/serverDescription.h"
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include <AmneziaVPN-Swift.h>
#include "core/utils/swiftBridge.h"
#endif

View File

@@ -351,18 +351,8 @@ void SettingsController::setAppLanguage(const QLocale &locale)
m_appSettingsRepository->setAppLanguage(locale);
}
bool SettingsController::isPremV1MigrationReminderActive() const
{
return m_appSettingsRepository->isPremV1MigrationReminderActive();
}
void SettingsController::disablePremV1MigrationReminder()
{
m_appSettingsRepository->disablePremV1MigrationReminder();
}
QString SettingsController::nextAvailableServerName() const
{
return m_appSettingsRepository->nextAvailableServerName();
return m_serversRepository->nextAvailableServerName();
}

View File

@@ -69,9 +69,6 @@ public:
void enableDevMode();
bool isPremV1MigrationReminderActive() const;
void disablePremV1MigrationReminder();
QString nextAvailableServerName() const;
bool isDevModeEnabled() const;

View File

@@ -5,15 +5,14 @@
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSysInfo>
#include <QTimer>
#include "amneziaApplication.h"
#include "logger.h"
#include "version.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/errorStrings.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
namespace
@@ -96,12 +95,12 @@ void UpdateController::fetchGatewayUrl()
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
7000,
m_appSettingsRepository->isStrictKillSwitchEnabled());
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
QJsonObject apiPayload;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::cliVersion, QString(APP_VERSION))
.build();
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
@@ -109,7 +108,7 @@ void UpdateController::fetchGatewayUrl()
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) {
logger.error() << errorString(err);
logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
finishUpdateCheck();
return;
}
@@ -250,17 +249,9 @@ void UpdateController::runInstaller()
runLinuxInstaller(kInstallerLocalPath);
#endif
} else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << QString::fromUtf8(reply->readAll());
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
<< reply->errorString();
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
reply->deleteLater();
});

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
@@ -20,6 +21,8 @@ namespace {
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxySecretPath("/data/secret");
constexpr QLatin1String kMtProxyMetaPath("/data/mtproxy-meta");
constexpr QLatin1String kMtProxyStartScriptPath("/opt/amnezia/start.sh");
}
MtProxyInstaller::MtProxyInstaller(QObject *parent)
@@ -53,14 +56,98 @@ ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container
}
}
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
const auto addExtra = [&](const QString &s) {
if (hex32.match(s).hasMatch() && !mt->additionalSecrets.contains(s)) {
mt->additionalSecrets.append(s);
}
};
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
mt->secret = sec;
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
mt->secret = sec;
}
bool metaRestored = false;
ErrorCode metaErr = ErrorCode::NoError;
const QByteArray metaRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyMetaPath), metaErr);
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
QString mode, domain, workersMode, workers, natInternal, natExternal;
bool natEnabled = false;
const QList<QByteArray> lines = metaRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq);
const QString val = line.mid(eq + 1).trimmed();
if (key == QLatin1String("mode")) mode = val;
else if (key == QLatin1String("domain")) domain = val;
else if (key == QLatin1String("tag")) { if (mt->tag.isEmpty()) mt->tag = val; }
else if (key == QLatin1String("additional")) {
for (const QString &s : val.split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
else if (key == QLatin1String("workers_mode")) workersMode = val;
else if (key == QLatin1String("workers")) workers = val;
else if (key == QLatin1String("nat_enabled")) natEnabled = (val == QLatin1String("1"));
else if (key == QLatin1String("nat_internal")) natInternal = val;
else if (key == QLatin1String("nat_external")) natExternal = val;
else if (key == QLatin1String("public_host")) { if (mt->publicHost.isEmpty()) mt->publicHost = val; }
}
if (!mode.isEmpty()) {
mt->transportMode = mode;
if (!domain.isEmpty()) mt->tlsDomain = domain;
if (!workersMode.isEmpty()) mt->workersMode = workersMode;
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual) && !workers.isEmpty()) {
mt->workers = workers;
}
if (natEnabled) {
mt->natEnabled = true;
mt->natInternalIp = natInternal;
mt->natExternalIp = natExternal;
}
metaRestored = true;
}
}
if (!metaRestored) {
ErrorCode startErr = ErrorCode::NoError;
const QByteArray startRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyStartScriptPath), startErr);
if (startErr == ErrorCode::NoError && !startRaw.trimmed().isEmpty()) {
const QString start = QString::fromUtf8(startRaw);
static const QRegularExpression modeRe(QStringLiteral("\\[ \"(standard|faketls)\" = \"faketls\" \\]"));
const QRegularExpressionMatch m = modeRe.match(start);
if (m.hasMatch()) {
mt->transportMode = m.captured(1);
if (m.captured(1) == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) {
static const QRegularExpression domRe(QStringLiteral("--domain ([A-Za-z0-9.\\-]+)"));
const QRegularExpressionMatch dm = domRe.match(start);
if (dm.hasMatch()) mt->tlsDomain = dm.captured(1);
}
}
static const QRegularExpression tagRe(QStringLiteral("-P ([0-9a-fA-F]{32})"));
const QRegularExpressionMatch tm = tagRe.match(start);
if (tm.hasMatch() && mt->tag.isEmpty()) mt->tag = tm.captured(1);
static const QRegularExpression addRe(QStringLiteral("echo \"([0-9a-fA-F,]+)\" \\| tr ',' ' '"));
const QRegularExpressionMatch am = addRe.match(start);
if (am.hasMatch()) {
for (const QString &s : am.captured(1).split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
static const QRegularExpression natRe(QStringLiteral("NAT_VALUE=\"([0-9.]+):([0-9.]+)\""));
const QRegularExpressionMatch nm = natRe.match(start);
if (nm.hasMatch()) {
mt->natEnabled = true;
mt->natInternalIp = nm.captured(1);
mt->natExternalIp = nm.captured(2);
}
}
}
@@ -71,48 +158,62 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out)
{
out = {};
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
return ErrorCode::InternalError;
}
const QString containerName = ContainerUtils::containerToString(container);
const QString script =
QStringLiteral(
"PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); "
"TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); "
"CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); "
"CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); "
"echo \"PORT_OK=${PORT_OK}\"; "
"echo \"TG_OK=${TG_OK}\"; "
"echo \"CLIENTS=${CLIENTS:-0}\"; "
"echo \"CONF_TIME=${CONF_TIME}\"; "
"echo \"STATS=http://localhost:2398/stats\";")
.arg(containerName)
.arg(listenPort);
out = { };
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
const QString containerName = ContainerUtils::containerToString(container);
const bool isTelemt = container == DockerContainer::Telemt;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
if (line.startsWith(QLatin1String("PORT_OK="))) {
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("TG_OK="))) {
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
out.clientsConnected = line.mid(8).trimmed().toInt();
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
out.lastConfigRefresh = line.mid(10).trimmed();
} else if (line.startsWith(QLatin1String("STATS="))) {
out.statsEndpoint = line.mid(6).trimmed();
const QString sportFilter = QString::number(listenPort);
const QString peersCmd = QStringLiteral("sudo conntrack -L -p tcp --dport ") + sportFilter
+ QStringLiteral(" 2>/dev/null | grep ESTABLISHED | awk '{for(i=1;i<=NF;i++) if($i ~ /^src=/){print "
"substr($i,5); break}}'");
const QString publicFilter = QStringLiteral(" | grep -vE "
"'^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3["
"01])\\.|::1$|fe80:|f[cd][0-9a-f][0-9a-f]:)'");
const QString clientsCmd =
QStringLiteral("CLIENTS=$(") + peersCmd + publicFilter + QStringLiteral(" | sort -u | grep -c .); ");
const QString confFile =
isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf");
const QString statsUrl = QString();
const QString script = QStringLiteral("CN=") + containerName + QStringLiteral("; ")
+ QStringLiteral("PORT_OK=$(sudo ss -tlnp 2>/dev/null | grep -q :") + QString::number(listenPort)
+ QStringLiteral(" && echo yes || echo no); ")
+ QStringLiteral("TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "
"https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || "
"echo no); ")
+ clientsCmd + QStringLiteral("CONF_TIME=$(sudo docker exec \"$CN\" sh -c 'stat -c \"%y\" ") + confFile
+ QStringLiteral(" 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); ")
+ QStringLiteral("echo \"PORT_OK=${PORT_OK}\"; ") + QStringLiteral("echo \"TG_OK=${TG_OK}\"; ")
+ QStringLiteral("echo \"CLIENTS=${CLIENTS:-0}\"; ") + QStringLiteral("echo \"CONF_TIME=${CONF_TIME}\"; ")
+ QStringLiteral("echo \"STATS=") + statsUrl + QStringLiteral("\";");
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data;
return ErrorCode::NoError;
};
const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) {
if (line.startsWith(QLatin1String("PORT_OK="))) {
out.portReachable = line.mid(8).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("TG_OK="))) {
out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes");
} else if (line.startsWith(QLatin1String("CLIENTS="))) {
out.clientsConnected = line.mid(8).trimmed().toInt();
} else if (line.startsWith(QLatin1String("CONF_TIME="))) {
out.lastConfigRefresh = line.mid(10).trimmed();
} else if (line.startsWith(QLatin1String("STATS="))) {
out.statsEndpoint = line.mid(6).trimmed();
}
}
return ErrorCode::NoError;
}
return ErrorCode::NoError;
return ErrorCode::InternalError;
}
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,

View File

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

View File

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

View File

@@ -144,10 +144,6 @@ QJsonObject ApiConfig::toJson() const
obj[apiDefs::key::availableCountries] = availableCountries;
}
if (!supportedProtocols.isEmpty()) {
obj[apiDefs::key::supportedProtocols] = supportedProtocols;
}
QJsonObject serviceInfoObj = serviceInfo.toJson();
if (!serviceInfoObj.isEmpty()) {
obj[apiDefs::key::serviceInfo] = serviceInfoObj;
@@ -198,7 +194,6 @@ ApiConfig ApiConfig::fromJson(const QJsonObject& json)
config.issuedConfigs = json.value(apiDefs::key::issuedConfigs).toInt(0);
config.availableCountries = json.value(apiDefs::key::availableCountries).toArray();
config.supportedProtocols = json.value(apiDefs::key::supportedProtocols).toArray();
QJsonObject serviceInfoObj = json.value(apiDefs::key::serviceInfo).toObject();
if (!serviceInfoObj.isEmpty()) {

View File

@@ -34,7 +34,6 @@ struct ApiConfig
int maxDeviceCount;
int issuedConfigs;
QJsonArray availableCountries;
QJsonArray supportedProtocols;
struct ServiceInfo {
bool isAdVisible = false;

View File

@@ -121,7 +121,11 @@ QJsonObject ApiV2ServerConfig::toJson() const
if (!dns2.isEmpty()) {
obj[configKey::dns2] = dns2;
}
if (!sendPayload.isEmpty()) {
obj[configKey::sendPayload] = sendPayload;
}
if (crc > 0) {
obj[configKey::crc] = crc;
}
@@ -165,6 +169,7 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
config.sendPayload = json.value(configKey::sendPayload).toArray();
config.crc = json.value(configKey::crc).toInt(0);

View File

@@ -1,6 +1,7 @@
#ifndef APIV2SERVERCONFIG_H
#define APIV2SERVERCONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QMap>
#include <QPair>
@@ -28,6 +29,7 @@ struct ApiV2ServerConfig {
DockerContainer defaultContainer;
QString dns1;
QString dns2;
QJsonArray sendPayload;
QString name;
bool nameOverriddenByUser = false;

View File

@@ -2,6 +2,10 @@
#include <QJsonDocument>
#include <QJsonArray>
#include <QObject>
#include <QSet>
#include <algorithm>
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
@@ -13,6 +17,54 @@ using namespace ProtocolUtils;
namespace amnezia
{
namespace
{
template <typename T>
bool hasAwg3Markers(const T &config)
{
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
const QStringList awg3Params = { config.headerProtectionKey, config.contentPaddingAddition,
config.rekeyAfterTime, config.rekeyTimeout,
config.rejectAfterTime, config.keepaliveTimeout,
config.maxHandshakeAttempts };
if (std::any_of(awg3Params.begin(), awg3Params.end(), hasValue)) {
return true;
}
return AwgProtocolConfig::isToggleEnabled(config.randomTrailers)
|| AwgProtocolConfig::isToggleEnabled(config.disableCookies);
}
template <typename T>
QString awgVersionOf(const T &config)
{
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
if (hasAwg3Markers(config)) {
return protocols::awg::awgV3;
}
const QStringList junkSizes = { config.cookieReplyPacketJunkSize, config.transportPacketJunkSize };
const QStringList magicHeaders = { config.initPacketMagicHeader, config.responsePacketMagicHeader,
config.underloadPacketMagicHeader, config.transportPacketMagicHeader };
bool hasJunkSizes = std::any_of(junkSizes.begin(), junkSizes.end(), hasValue);
bool hasHeaderRanges = std::any_of(magicHeaders.begin(), magicHeaders.end(),
[](const QString &header) { return header.contains('-'); });
if (hasJunkSizes || hasHeaderRanges) {
return protocols::awg::awgV2;
}
const QStringList specialJunk = { config.specialJunk1, config.specialJunk2, config.specialJunk3,
config.specialJunk4, config.specialJunk5 };
if (std::any_of(specialJunk.begin(), specialJunk.end(), hasValue)) {
return protocols::awg::awgV1_5;
}
return QString();
}
} // namespace
QJsonObject AwgServerConfig::toJson() const
{
QJsonObject obj;
@@ -74,6 +126,34 @@ QJsonObject AwgServerConfig::toJson() const
obj[configKey::specialJunk4] = specialJunk4;
obj[configKey::specialJunk5] = specialJunk5;
if (!headerProtectionKey.isEmpty()) {
obj[configKey::headerProtectionKey] = headerProtectionKey;
}
if (!contentPaddingAddition.isEmpty()) {
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
}
if (!rekeyAfterTime.isEmpty()) {
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
}
if (!rekeyTimeout.isEmpty()) {
obj[configKey::rekeyTimeout] = rekeyTimeout;
}
if (!rejectAfterTime.isEmpty()) {
obj[configKey::rejectAfterTime] = rejectAfterTime;
}
if (!keepaliveTimeout.isEmpty()) {
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
}
if (!maxHandshakeAttempts.isEmpty()) {
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
}
if (!randomTrailers.isEmpty()) {
obj[configKey::randomTrailers] = randomTrailers;
}
if (!disableCookies.isEmpty()) {
obj[configKey::disableCookies] = disableCookies;
}
if (isThirdPartyConfig) {
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
}
@@ -109,7 +189,17 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
config.randomTrailers = json.value(configKey::randomTrailers).toString();
config.disableCookies = json.value(configKey::disableCookies).toString();
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
return config;
@@ -196,16 +286,50 @@ QJsonObject AwgClientConfig::toJson() const
obj[configKey::transportPacketMagicHeader] = transportPacketMagicHeader;
}
obj[configKey::specialJunk1] = specialJunk1;
obj[configKey::specialJunk2] = specialJunk2;
obj[configKey::specialJunk3] = specialJunk3;
obj[configKey::specialJunk4] = specialJunk4;
obj[configKey::specialJunk5] = specialJunk5;
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
if (!specialJunk1.isEmpty()) {
obj[configKey::specialJunk1] = specialJunk1;
}
if (!specialJunk2.isEmpty()) {
obj[configKey::specialJunk2] = specialJunk2;
}
if (!specialJunk3.isEmpty()) {
obj[configKey::specialJunk3] = specialJunk3;
}
if (!specialJunk4.isEmpty()) {
obj[configKey::specialJunk4] = specialJunk4;
}
if (!specialJunk5.isEmpty()) {
obj[configKey::specialJunk5] = specialJunk5;
}
if (!headerProtectionKey.isEmpty()) {
obj[configKey::headerProtectionKey] = headerProtectionKey;
}
if (!contentPaddingAddition.isEmpty()) {
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
}
if (!rekeyAfterTime.isEmpty()) {
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
}
if (!rekeyTimeout.isEmpty()) {
obj[configKey::rekeyTimeout] = rekeyTimeout;
}
if (!rejectAfterTime.isEmpty()) {
obj[configKey::rejectAfterTime] = rejectAfterTime;
}
if (!keepaliveTimeout.isEmpty()) {
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
}
if (!maxHandshakeAttempts.isEmpty()) {
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
}
if (!randomTrailers.isEmpty()) {
obj[configKey::randomTrailers] = randomTrailers;
}
if (!disableCookies.isEmpty()) {
obj[configKey::disableCookies] = disableCookies;
}
return obj;
}
@@ -248,21 +372,28 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
config.randomTrailers = json.value(configKey::randomTrailers).toString();
config.disableCookies = json.value(configKey::disableCookies).toString();
return config;
}
QJsonObject AwgProtocolConfig::toJson() const
{
QJsonObject obj = serverConfig.toJson();
if (clientConfig.has_value()) {
QJsonObject clientJson = clientConfig->toJson();
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
}
return obj;
}
@@ -283,6 +414,31 @@ AwgProtocolConfig AwgProtocolConfig::fromJson(const QJsonObject& json)
return config;
}
QString AwgProtocolConfig::serverProtocolVersion() const
{
return awgVersionOf(serverConfig);
}
QString AwgProtocolConfig::clientProtocolVersion() const
{
return clientConfig.has_value() ? awgVersionOf(clientConfig.value()) : QString();
}
bool AwgProtocolConfig::isToggleEnabled(const QString &value)
{
const QString trimmedValue = value.trimmed();
return !trimmedValue.isEmpty()
&& trimmedValue.compare(QLatin1String(protocols::awg::awgBoolOff), Qt::CaseInsensitive) != 0;
}
QString AwgProtocolConfig::protocolVersionString(const QString &version)
{
if (version == protocols::awg::awgV3) return QObject::tr(" (version 3.1)");
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}
bool AwgProtocolConfig::hasClientConfig() const
{
return clientConfig.has_value();
@@ -310,24 +466,37 @@ bool AwgServerConfig::hasEqualServerSettings(const AwgServerConfig& other) const
transportPacketMagicHeader != other.transportPacketMagicHeader ||
specialJunk1 != other.specialJunk1 || specialJunk2 != other.specialJunk2 ||
specialJunk3 != other.specialJunk3 || specialJunk4 != other.specialJunk4 ||
specialJunk5 != other.specialJunk5) {
specialJunk5 != other.specialJunk5 ||
cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize ||
headerProtectionKey != other.headerProtectionKey ||
contentPaddingAddition != other.contentPaddingAddition ||
rekeyAfterTime != other.rekeyAfterTime || rekeyTimeout != other.rekeyTimeout ||
rejectAfterTime != other.rejectAfterTime || keepaliveTimeout != other.keepaliveTimeout ||
maxHandshakeAttempts != other.maxHandshakeAttempts ||
randomTrailers != other.randomTrailers || disableCookies != other.disableCookies) {
return false;
}
bool isV2 = protocolVersion == protocols::awg::awgV2;
if (isV2) {
if (cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize) {
return false;
}
}
return true;
}
bool AwgServerConfig::hasAwg3Params() const
{
return hasAwg3Markers(*this);
}
bool AwgProtocolConfig::isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4)
{
return (h1 == h2) || (h1 == h3) || (h1 == h4) || (h2 == h3) || (h2 == h4) || (h3 == h4);
QSet<QString> uniqueHeaders;
int filledHeaders = 0;
for (const QString &header : { h1, h2, h3, h4 }) {
if (!header.trimmed().isEmpty()) {
++filledHeaders;
uniqueHeaders.insert(header);
}
}
return uniqueHeaders.size() != filledHeaders;
}
bool AwgProtocolConfig::isPacketSizeEqual(int s1, int s2, int s3, int s4)

View File

@@ -39,12 +39,22 @@ struct AwgServerConfig {
QString specialJunk3;
QString specialJunk4;
QString specialJunk5;
QString headerProtectionKey;
QString contentPaddingAddition;
QString rekeyAfterTime;
QString rekeyTimeout;
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
QString randomTrailers;
QString disableCookies;
bool isThirdPartyConfig = false;
QJsonObject toJson() const;
static AwgServerConfig fromJson(const QJsonObject& json);
bool hasEqualServerSettings(const AwgServerConfig& other) const;
bool hasAwg3Params() const;
};
struct AwgClientConfig {
@@ -76,8 +86,15 @@ struct AwgClientConfig {
QString specialJunk3;
QString specialJunk4;
QString specialJunk5;
bool isObfuscationEnabled = false;
QString headerProtectionKey;
QString contentPaddingAddition;
QString rekeyAfterTime;
QString rekeyTimeout;
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
QString randomTrailers;
QString disableCookies;
QJsonObject toJson() const;
static AwgClientConfig fromJson(const QJsonObject& json);
};
@@ -89,6 +106,11 @@ struct AwgProtocolConfig {
QJsonObject toJson() const;
static AwgProtocolConfig fromJson(const QJsonObject& json);
QString serverProtocolVersion() const;
QString clientProtocolVersion() const;
static QString protocolVersionString(const QString &version);
static bool isToggleEnabled(const QString &value);
bool hasClientConfig() const;
void setClientConfig(const AwgClientConfig& config);
void clearClientConfig();

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
c.key = json.value(configKey::xPaddingKey).toString();
c.header = json.value(configKey::xPaddingHeader).toString();
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
@@ -81,7 +81,6 @@ QJsonObject XrayXhttpConfig::toJson() const
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
obj[configKey::xhttpDisableGrpc] = disableGrpc;
obj[configKey::xhttpDisableSse] = disableSse;
@@ -116,7 +115,6 @@ namespace
c.mode = QString();
c.host = QString();
c.path = QString();
c.headersTemplate = QString();
c.uplinkMethod = QString();
c.disableGrpc = false;
c.disableSse = false;
@@ -155,9 +153,6 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
if (json.contains(configKey::xhttpPath)) {
c.path = json.value(configKey::xhttpPath).toString();
}
if (json.contains(configKey::xhttpHeadersTemplate)) {
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
}
if (json.contains(configKey::xhttpUplinkMethod)) {
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
}
@@ -365,6 +360,8 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
{
return port == other.port
&& transportProto == other.transportProto
&& subnetAddress == other.subnetAddress
&& site == other.site
&& security == other.security
&& flow == other.flow
@@ -466,6 +463,17 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
}
}
}
const QJsonArray outbounds = parsed.value(protocols::xray::outbounds).toArray();
if (!outbounds.isEmpty()) {
const QJsonObject settings = outbounds[0].toObject().value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonArray users = vnext[0].toObject().value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
clientCfg.id = users[0].toObject().value(protocols::xray::id).toString();
}
}
}
c.clientConfig = clientCfg;
} else {
c.clientConfig = XrayClientConfig::fromJson(parsed);

View File

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

View File

@@ -29,6 +29,11 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container);
}
void NativeServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;

View File

@@ -27,6 +27,8 @@ struct NativeServerConfig {
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;

View File

@@ -43,6 +43,11 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
return containers.value(container);
}
void SelfHostedUserServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &primaryDns,
const QString &secondaryDns) const
{

View File

@@ -32,6 +32,8 @@ struct SelfHostedUserServerConfig {
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;

View File

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

View File

@@ -39,33 +39,44 @@ QString OpenVpnProtocol::defaultConfigPath()
return p;
}
void OpenVpnProtocol::stop()
void OpenVpnProtocol::cleanupResources()
{
qDebug() << "OpenVpnProtocol::stop()";
setConnectionState(Vpn::ConnectionState::Disconnecting);
// TODO: need refactoring
// sendTermSignal() will even return true while server connected ???
if ((m_connectionState == Vpn::ConnectionState::Preparing) || (m_connectionState == Vpn::ConnectionState::Connecting)
|| (m_connectionState == Vpn::ConnectionState::Connected)
|| (m_connectionState == Vpn::ConnectionState::Reconnecting)) {
if (m_openVpnProcess || openVpnProcessIsRunning()) {
if (!sendTermSignal()) {
killOpenVpnProcess();
}
QThread::msleep(10);
m_managementServer.stop();
}
m_managementServer.stop();
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
if (!reply.waitForFinished(1000) && !reply.returnValue()) {
qWarning() << "OpenVpnProtocol::stop(): Failed to disable killswitch";
qWarning() << "OpenVpnProtocol::cleanupResources(): Failed to disable killswitch";
}
});
#endif
}
setConnectionState(Vpn::ConnectionState::Disconnected);
void OpenVpnProtocol::stop()
{
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|| m_connectionState == Vpn::ConnectionState::Connecting
|| m_connectionState == Vpn::ConnectionState::Connected
|| m_connectionState == Vpn::ConnectionState::Reconnecting;
if (wasActive) {
setConnectionState(Vpn::ConnectionState::Disconnecting);
}
cleanupResources();
if (wasActive || m_connectionState == Vpn::ConnectionState::Disconnecting) {
setConnectionState(Vpn::ConnectionState::Disconnected);
}
}
ErrorCode OpenVpnProtocol::prepare()
@@ -168,7 +179,7 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
OpenVpnProtocol::stop();
cleanupResources();
if (!QFileInfo::exists(configPath())) {
setLastError(ErrorCode::OpenVpnConfigMissing);
@@ -211,6 +222,18 @@ ErrorCode OpenVpnProtocol::start()
}
#endif
#ifdef Q_OS_WIN
// In "all except sites" mode the config uses redirect-gateway !ipv4, so OpenVPN
// never reports net_route_v4_best_gw and m_routeGateway would stay empty
const QString winGateway = NetworkUtilities::getGatewayAndIface().first;
if (!winGateway.isEmpty()) {
m_routeGateway = winGateway;
qDebug() << "Set VPN route gateway" << m_routeGateway;
} else {
qWarning() << "Unable to detect physical default gateway";
}
#endif
uint mgmtPort = selectMgmtPort();
qDebug() << "OpenVpnProtocol::start mgmt port selected:" << mgmtPort;

View File

@@ -29,6 +29,7 @@ protected slots:
void onReadyReadDataFromManagementServer();
private:
void cleanupResources();
QString configPath() const;
bool openVpnProcessIsRunning() const;
bool sendTermSignal();

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
m_connectionState(Vpn::ConnectionState::Unknown),
m_rawConfig(configuration),
m_timeoutTimer(new QTimer(this)),
m_lastError(ErrorCode::NoError),
m_receivedBytes(0),
m_sentBytes(0)
{

View File

@@ -120,34 +120,60 @@ QVariantMap SecureAppSettingsRepository::vpnSites(RouteMode mode) const
return value("Conf/" + routeModeString(mode)).toMap();
}
QStringList SecureAppSettingsRepository::siteIpList(const QVariant &value)
{
// QVariant::toStringList() handles both a QStringList/QVariantList and a single QString
// (a single string is returned as a one-element list), which covers the legacy format.
QStringList result = value.toStringList();
result.removeAll(QString());
result.removeDuplicates();
return result;
}
void SecureAppSettingsRepository::setVpnSites(RouteMode mode, const QVariantMap &sites)
{
setValue("Conf/" + routeModeString(mode), sites);
}
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QStringList &ips)
{
QVariantMap sites = vpnSites(mode);
if (sites.contains(site) && ip.isEmpty())
const bool siteExisted = sites.contains(site);
if (siteExisted && ips.isEmpty())
return false;
sites.insert(site, ip);
QStringList mergedIps = siteIpList(sites.value(site));
bool changed = !siteExisted;
for (const QString &ip : ips) {
if (!ip.isEmpty() && !mergedIps.contains(ip)) {
mergedIps.append(ip);
changed = true;
}
}
if (!changed)
return false;
sites.insert(site, mergedIps);
setVpnSites(mode, sites);
emit sitesChanged(mode);
return true;
}
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QString> &sites)
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites)
{
QVariantMap allSites = vpnSites(mode);
for (auto i = sites.constBegin(); i != sites.constEnd(); ++i) {
const QString &site = i.key();
const QString &ip = i.value();
if (allSites.contains(site) && allSites.value(site) == ip)
continue;
QStringList mergedIps = siteIpList(allSites.value(site));
for (const QString &ip : i.value()) {
if (!ip.isEmpty() && !mergedIps.contains(ip))
mergedIps.append(ip);
}
allSites.insert(site, ip);
allSites.insert(site, mergedIps);
}
setVpnSites(mode, allSites);
@@ -280,6 +306,24 @@ void SecureAppSettingsRepository::toggleDevGatewayEnv(bool enabled)
setValue("Conf/devGatewayEnv", enabled);
}
QByteArray SecureAppSettingsRepository::readGatewayProxyUrls(const QString &cacheKey) const
{
if (cacheKey.isEmpty()) {
return {};
}
return value(QStringLiteral("Conf/proxyUrls/") + cacheKey).toByteArray();
}
void SecureAppSettingsRepository::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
{
if (cacheKey.isEmpty()) {
return;
}
setValue(QStringLiteral("Conf/proxyUrls/") + cacheKey, proxyUrlsEncrypted);
}
bool SecureAppSettingsRepository::isKillSwitchEnabled() const
{
return value("Conf/killSwitchEnabled", true).toBool();
@@ -398,16 +442,6 @@ void SecureAppSettingsRepository::disableHomeAdLabel()
setValue("Conf/homeAdLabelVisible", false);
}
bool SecureAppSettingsRepository::isPremV1MigrationReminderActive() const
{
return value("Conf/premV1MigrationReminderActive", true).toBool();
}
void SecureAppSettingsRepository::disablePremV1MigrationReminder()
{
setValue("Conf/premV1MigrationReminderActive", false);
}
QByteArray SecureAppSettingsRepository::backupAppConfig() const
{
return m_settings->backupAppConfig();
@@ -426,26 +460,6 @@ void SecureAppSettingsRepository::clearSettings()
emit settingsCleared();
}
QString SecureAppSettingsRepository::nextAvailableServerName() const
{
int i = 0;
bool nameExist = false;
do {
i++;
nameExist = false;
QJsonArray servers = QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
for (const QJsonValue &server : servers) {
if (server.toObject().value(configKey::description).toString() == QString("Server") + " " + QString::number(i)) {
nameExist = true;
break;
}
}
} while (nameExist);
return QString("Server") + " " + QString::number(i);
}
void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
{
m_settings->setValue("Conf/installationUuid", uuid);

View File

@@ -38,11 +38,15 @@ public:
RouteMode routeMode() const;
void setRouteMode(RouteMode mode);
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites);
bool addVpnSite(RouteMode mode, const QString &site, const QStringList &ips = {});
void addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites);
void removeVpnSite(RouteMode mode, const QString &site);
void removeAllVpnSites(RouteMode mode);
QVariantMap vpnSites(RouteMode mode) const;
// Normalizes a stored vpn site value into a list of IPs.
// Supports both the legacy format (a single IP string) and the current one (a list of IPs).
static QStringList siteIpList(const QVariant &value);
bool isSitesSplitTunnelingEnabled() const;
void setSitesSplitTunnelingEnabled(bool enabled);
@@ -59,7 +63,9 @@ public:
void setDevGatewayEndpoint();
bool isDevGatewayEnv(bool isTestPurchase = false) const;
void toggleDevGatewayEnv(bool enabled);
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
bool isKillSwitchEnabled() const;
void setKillSwitchEnabled(bool enabled);
bool isStrictKillSwitchEnabled() const;
@@ -84,14 +90,10 @@ public:
bool isHomeAdLabelVisible() const;
void disableHomeAdLabel();
bool isPremV1MigrationReminderActive() const;
void disablePremV1MigrationReminder();
QByteArray backupAppConfig() const;
bool restoreAppConfig(const QByteArray &cfg);
void clearSettings();
QString nextAvailableServerName() const;
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);

View File

@@ -3,6 +3,7 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonValue>
#include <QSet>
#include <QUuid>
#include "core/utils/serverConfigUtils.h"
@@ -32,6 +33,45 @@ QJsonObject embedStorageServerId(const QString &serverId, const QJsonObject &pay
return o;
}
QString storedServerDisplayName(const SecureServersRepository *repository, const QString &serverId)
{
using Kind = serverConfigUtils::ConfigType;
switch (repository->serverKind(serverId)) {
case Kind::SelfHostedAdmin:
if (const auto cfg = repository->selfHostedAdminConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::SelfHostedUser:
if (const auto cfg = repository->selfHostedUserConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::Native:
if (const auto cfg = repository->nativeConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium:
if (const auto cfg = repository->apiV2Config(serverId)) {
return cfg->displayName;
}
break;
case Kind::AmneziaPremiumV1:
case Kind::AmneziaFreeV2:
if (const auto cfg = repository->legacyApiConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::Invalid:
default:
break;
}
return {};
}
} // namespace
SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent)
@@ -153,6 +193,28 @@ void SecureServersRepository::clearServers()
syncToStorage();
}
QString SecureServersRepository::nextAvailableServerName() const
{
QSet<QString> usedNames;
usedNames.reserve(m_orderedServerIds.size());
for (const QString &serverId : m_orderedServerIds) {
const QString displayName = storedServerDisplayName(this, serverId);
if (!displayName.isEmpty()) {
usedNames.insert(displayName);
}
}
int i = 0;
QString candidate;
do {
++i;
candidate = tr("Server") + QLatin1Char(' ') + QString::number(i);
} while (usedNames.contains(candidate));
return candidate;
}
QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind)
{
const QString id = normalizedOrGeneratedServerId(serverId);

View File

@@ -48,6 +48,8 @@ public:
void clearServers();
QString nextAvailableServerName() const;
void invalidateCache();
signals:

View File

@@ -1,5 +1,6 @@
#include "apiUtils.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/configKeys.h"
#include <QDateTime>
@@ -48,6 +49,30 @@ namespace
}
}
QString apiUtils::getAppLanguageCode(const SecureAppSettingsRepository *appSettingsRepository)
{
if (appSettingsRepository == nullptr) {
return {};
}
return appSettingsRepository->getAppLanguage().name().split("_").first();
}
QString apiUtils::getDistributionChannel()
{
#if defined(Q_OS_ANDROID)
return {};
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
return QStringLiteral("appstore");
#else
return QStringLiteral("github");
#endif
}
QString apiUtils::getCountryFlagCode(const QString &serverCountryCode)
{
return serverCountryCode.section('-', 0, 0).toUpper();
}
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
{
if (subscriptionEndDate.isEmpty()) {
@@ -167,44 +192,6 @@ bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
return premiumTypes.contains(serverConfigUtils::configTypeFromJson(serverConfigObject));
}
QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
{
if (serverConfigUtils::configTypeFromJson(serverConfigObject) != serverConfigUtils::ConfigType::AmneziaPremiumV1) {
return {};
}
QList<QPair<QString, QVariant>> orderedFields;
orderedFields.append(qMakePair(configKey::name, serverConfigObject[configKey::name].toString()));
orderedFields.append(qMakePair(configKey::description, serverConfigObject[configKey::description].toString()));
orderedFields.append(qMakePair(configKey::configVersion, serverConfigObject[configKey::configVersion].toDouble()));
orderedFields.append(qMakePair(apiDefs::key::protocol, serverConfigObject[apiDefs::key::protocol].toString()));
orderedFields.append(qMakePair(apiDefs::key::apiEndpoint, serverConfigObject[apiDefs::key::apiEndpoint].toString()));
orderedFields.append(qMakePair(apiDefs::key::apiKey, serverConfigObject[apiDefs::key::apiKey].toString()));
QString vpnKeyStr = "{";
for (int i = 0; i < orderedFields.size(); ++i) {
const auto &pair = orderedFields[i];
if (pair.second.typeId() == QMetaType::Type::QString) {
vpnKeyStr += "\"" + pair.first + "\": \"" + pair.second.toString() + "\"";
} else if (pair.second.typeId() == QMetaType::Type::Double || pair.second.typeId() == QMetaType::Type::Int) {
vpnKeyStr += "\"" + pair.first + "\": " + QString::number(pair.second.toDouble(), 'f', 1);
}
if (i < orderedFields.size() - 1) {
vpnKeyStr += ", ";
}
}
vpnKeyStr += "}";
QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8();
vpnKeyCompressed = qCompress(vpnKeyCompressed, 6);
vpnKeyCompressed = vpnKeyCompressed.mid(4);
QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed;
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
}
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
auto configType = serverConfigUtils::configTypeFromJson(serverConfigObject);

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