Compare commits

..
Author SHA1 Message Date
vkamn 7f9b7db482 chore: bump version (#3350) 2026-10-29 23:20:34 +09:00
NickVs2015 6c0e62753d chore: update amnezia-xray-bindings to 1.5.0 (#3349) 2026-10-29 22:33:38 +09:00
NickVs2015 da8ce1809b fix: revert local proxy port rotation and logic with port processing (#3347)
* Revert "fix: rotate local proxy port on restart (#3320)"

This reverts commit a55ef420e8.

* fix: check local proxy port availability via service

* chore: update amnezia-xray-bindings
2026-10-29 22:16:26 +09:00
vkamn 94ea5b00aa chore: update icon (#3340)
* feat: new app icon with .icns fallback for older Xcode

* docs: add AGENTS.md and CLAUDE.md
2026-10-29 01:09:51 +09:00
yp c18f7773e3 test: remove xray settings ui tests (#3338) 2026-10-27 23:22:32 +09:00
vkamn 95501dba35 fix: minor fixes (#3330)
* fix: pass deployment target to awg-apple build

Without it clang took the minimum iOS version from the SDK and generated
A12 code. Its LDAPR instructions crash A10/A11 devices with SIGILL.

* chore: add link to amnezia hub

* fix: notification insted of error after onScanServerFinished

* fix: no gap in server tabs for read-only servers

* fix: return Premium and Free features text field

* fix: build site and mirror URLs with the path before UTM tags

* fix: show split tunneling toggle on mobile

* chore: bump version
2026-10-27 19:14:17 +09:00
yp 4cacfb41ab fix: xray settings QA fixes and tests (#3333) 2026-10-27 18:57:05 +09:00
yp 8a51177a44 fix: show locked xray settings page for servers without write access (#3286)
* fix: show locked xray settings page for servers without write access

* reset file
2026-10-27 18:56:47 +09:00
makkaor 3121775a6f fix: preserve imported MTU (#3113) 2026-10-26 11:17:42 +09:00
Yakimov Grigory 3977447d4a feat: add Indonesian (id_ID) localization (#3325) 2026-10-26 00:44:16 +09:00
yp bf95867a7c fix: restore favorite locations (#3309)
* fix: restore favorite locations

* fix per subscription favorites
2026-10-26 00:33:54 +09:00
NickVs2015 a55ef420e8 fix: rotate local proxy port on restart (#3320) 2026-10-26 00:33:21 +09:00
vkamn 3b5a93889f feat: new design system (#3299)
* fix: use text button for local proxy learn more

* feat: pin page header and collapse large title on scroll

* fix: enable page scrolling only when content overflows

* refactor: drop qml focus hacks and simplify list selectors

- remove unused objectNames, dead functions and focus timers
- handle back/escape in FocusController instead of PageStart keys
- declare static menus as items instead of list<QtObject> models
- make radio/segment selection binding-driven, reuseItems opt-in
- fix full access share default server preselection

* refactor: convert config models to Q_PROPERTY and merge MtProto pages
- config models: QObject with properties instead of single-row list models
- protocol/service pages: Flickable layout, write-through edits, no save focus hack
- MtProxy/Telemt settings share PageServiceMtProtoSettingsBase
- question drawer requested via Dialogs singleton

* chore: bump version

* refactor: create rarely shown controls on demand

- focus ring and selection glow create MultiEffect only while visible
- text fields create the context menu, action buttons and attached button on demand
- setup page loads the details sheet content when it opens

* feat: macOS and iOS 26 app icon from Icon Composer

- add images/AppIcon.icon with colour and mono marks
- build the app icon from it on macOS, macOS NE and iOS, require Xcode 26
- drop app.icns and the AppIcon asset catalogs

* feat: update design system changes
- header: centered 18 semibold title in a 56 px row with 48 px buttons, no large title
- hide the back arrow on stack root pages
- subtitle as the first content block, 16 under the header and 24 to the next block
- main toggles as the first content row, desktop only
- logs: share and a more menu instead of three header icons
- countries: 48 px gear, search first under the header, chips to list 24
- sheets: 32 px from title to the first block
- tab bar: 44 px buttons centered with 18 px gaps
- connection key field: Insert while empty, clear button after paste
- error toast: default text color, accent error code link
- 24 px between the last block and sticky buttons

* chore: minor ui fixes

* fix: restore trailing buttons in country rows

* chore: bump version

* chore: build macOS and iOS on macos-26 runners for the Icon Composer app icon

* fix: load only one Go runtime in the UI process
libAmneziaVPN linked libwg-go and libovpn3 (which pulls in the Go-based
libck-ovpn-plugin) without using any of their symbols. With libagw that
made three Go runtimes in the UI process. They conflict over signal
handlers, and the app crashed at startup with SIGSEGV and no tombstone.
Stop linking awg-android and openvpn-pt-android into the app library.
Package their .so files through QT_ANDROID_EXTRA_LIBS instead; the VPN
service processes still load them from Kotlin.

* chore: sync with figma and minor ui fixes

* chore: bump version

* fix: disable Go async preemption on Apple platforms

Build libagw with -X=runtime.godebugDefault=asyncpreemptoff=1 on macOS
and iOS to work around the Go runtime crash on macOS
(golang/go#75887).
2026-10-26 00:32:49 +09:00
294 changed files with 37944 additions and 25760 deletions

No files matched your search

+47 -3
View File
@@ -324,7 +324,7 @@ jobs:
# ------------------------------------------------------
Build-iOS:
runs-on: macos-latest
runs-on: macos-26
needs: Bake-Prebuilts-iOS
if: ${{ always() }}
@@ -472,7 +472,7 @@ jobs:
# ------------------------------------------------------
Build-MacOS:
runs-on: macos-15
runs-on: macos-26
needs: Bake-Prebuilts-MacOS
if: ${{ always() }}
@@ -1049,11 +1049,16 @@ jobs:
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- XrayServerInboundTest
- XrayGeneratorMatrixTest
- XrayConfigModelTest
- XrayCoreAcceptsTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
@@ -1132,6 +1137,17 @@ jobs:
-name "test_*" \
-exec chmod +x {} \;
- name: Download Xray core
if: matrix.filter == 'XrayCoreAcceptsTest'
shell: bash
run: |
XRAY_RELEASE=$(sed -n 's/^ARG XRAY_RELEASE="\(.*\)"/\1/p' client/server_scripts/xray/Dockerfile)
curl -fsSL -o "$RUNNER_TEMP/xray.zip" \
"https://github.com/XTLS/Xray-core/releases/download/${XRAY_RELEASE}/Xray-linux-64.zip"
unzip -q -o "$RUNNER_TEMP/xray.zip" xray -d "$RUNNER_TEMP/xray"
chmod +x "$RUNNER_TEMP/xray/xray"
echo "XRAY_BIN=$RUNNER_TEMP/xray/xray" >> "$GITHUB_ENV"
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
@@ -1208,6 +1224,16 @@ jobs:
-name "test_*" \
-exec chmod +x {} \;
- name: Download Xray core
shell: bash
run: |
XRAY_RELEASE=$(sed -n 's/^ARG XRAY_RELEASE="\(.*\)"/\1/p' client/server_scripts/xray/Dockerfile)
curl -fsSL -o "$RUNNER_TEMP/xray.zip" \
"https://github.com/XTLS/Xray-core/releases/download/${XRAY_RELEASE}/Xray-linux-64.zip"
unzip -q -o "$RUNNER_TEMP/xray.zip" xray -d "$RUNNER_TEMP/xray"
chmod +x "$RUNNER_TEMP/xray/xray"
echo "XRAY_BIN=$RUNNER_TEMP/xray/xray" >> "$GITHUB_ENV"
- name: Run tests
shell: bash
run: |
@@ -1217,7 +1243,8 @@ jobs:
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest)$"
--timeout 3600 \
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest|XrayVpsMatrixTest)$"
# ------------------------------------------------------
@@ -1239,11 +1266,16 @@ jobs:
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- XrayServerInboundTest
- XrayGeneratorMatrixTest
- XrayConfigModelTest
- XrayCoreAcceptsTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
@@ -1312,6 +1344,17 @@ jobs:
with:
arch: x64
- name: Download Xray core
if: matrix.filter == 'XrayCoreAcceptsTest'
shell: pwsh
run: |
$release = (Select-String -Path client/server_scripts/xray/Dockerfile -Pattern '^ARG XRAY_RELEASE="(.*)"').Matches[0].Groups[1].Value
$zip = Join-Path $env:RUNNER_TEMP "xray.zip"
$dir = Join-Path $env:RUNNER_TEMP "xray"
Invoke-WebRequest -Uri "https://github.com/XTLS/Xray-core/releases/download/$release/Xray-windows-64.zip" -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $dir -Force
"XRAY_BIN=$(Join-Path $dir 'xray.exe')" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Run tests
env:
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
@@ -1351,6 +1394,7 @@ jobs:
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
+219
View File
@@ -0,0 +1,219 @@
# AGENTS.md
Operating guide for this repository. Read it before changing anything.
## Project Overview
AmneziaVPN is a cross-platform VPN client built with C++ and Qt Quick (QML) for
Windows, macOS, Linux, Android and iOS. It has three kinds of servers, each
with its own config model (`serverConfigUtils::ConfigType`):
- **Self-hosted.** The admin connects the app to their own server over SSH; the
app installs protocols as Docker containers (AmneziaWG, WireGuard, OpenVPN,
XRay, IPsec and others) and generates client configs (`SelfHostedAdmin`).
Access is shared with other users as `vpn://` keys, which import without
server management rights (`SelfHostedUser`).
- **Amnezia Premium and Free.** Configs come from the Amnezia gateway API. The
gateway passes the backend's config through; the client does not invent
server-side values.
- **Native protocol configs.** Third-party configs imported as they are:
WireGuard and AmneziaWG `.conf`, OpenVPN `.ovpn`, XRay and Shadowsocks links
(`Native`). The app only connects with them; there is no server to manage.
A change to shared code (protocol configs, import, connection) must keep
working for all three.
The desktop builds run a privileged service next to the GUI; the GUI talks to
it over Qt Remote Objects. Android, iOS and the macOS App Store build
(`MACOS_NE`) run the tunnel in the platform's VPN extension instead.
## Repository Layout
- `client/core/` — business logic with no UI: controllers, configurators
(client config generation per protocol), installers (server-side setup over
SSH), protocol and server config models, repositories (encrypted settings
storage).
- `client/ui/controllers/` — `*UiController` classes exposed to QML. Thin
adapters over `core/`: they translate between QML and core types and hold no
business logic of their own.
- `client/ui/models/` — list and property models for QML.
- `client/ui/qml/` — the interface:
- `Pages2/` — screens, one file per page.
- `Components/` — composite pieces of screens.
- `Controls2/` — design system controls, named `<Name>Type.qml`; text styles
live in `Controls2/TextTypes/`.
- `Modules/Style/AmneziaStyle.qml` — design tokens (colours and the like).
- `Config/` — `GlobalConfig`, `Dialogs` and other singletons.
- `client/platforms/`, `client/android/`, `client/ios/`, `client/macos/` —
platform code: VPN extensions, Kotlin, Swift, Objective-C++.
- `client/daemon/`, `client/mozilla/`, `service/`, `ipc/` — the privileged
service and its IPC.
- `client/server_scripts/` — the shell scripts and Dockerfiles run on the
user's server.
- `client/translations/` — Qt `.ts` files.
- `client/tests/` — Qt Test suites, run through `ctest`.
- `recipes/` — Conan recipes for native dependencies (protocol libraries,
OpenSSL, libssh and others).
- `deploy/` — build and installer scripts. `.github/workflows/` — CI.
- `client/3rd/` — vendored third-party code. Do not edit it.
## Essential Commands
Run from the repository root.
```bash
deploy/build.sh -h # build options
deploy/build.sh # build for the host platform
deploy/build.sh -t android --aab # Android APK and AAB
cmake -S . -B build -DCMAKE_PREFIX_PATH=<Qt>/<platform> -DAMNEZIA_BUILD_TESTS=ON
cmake --build build --parallel
ctest --test-dir build/client/tests --output-on-failure
```
Qt 6.10 or later and Conan are required; native dependencies are built by Conan
from `recipes/` on the first configure.
## Repository Rules
These three come first. Each one has been asked for repeatedly.
### 1. Clean, self-documenting code, no comments
- Names and structure carry the meaning. Code must be readable without help.
- No explanatory comments in code: no `//` notes in QML or C++, no doc blocks.
- The one exception is a hack that looks wrong on purpose: a workaround for a
platform or Qt bug, an ordering that must not be "fixed". Then a short comment
next to it says why.
- Explanations of a change go into the chat summary or the commit message, not
into the code.
- Leave existing comments alone unless the change removes the code they
describe.
### 2. Reusable code (DRY), without abstraction for its own sake
- **QML.** Every reusable component lives in its own file. A piece of a page
goes into its own file in `Components/` even when it is used once, if that
keeps the page short and readable. No inline `component` definitions or
`Component` blocks inside pages. Before writing a new control, look in
`Controls2/` and `Components/` for one that already does the job, and extend
it if it almost does.
- **C++.** Logic shared by several places is extracted once and reused:
duplicated parsing, validation or config building is a bug waiting to diverge.
- **No fanaticism.** Do not create a base class, interface, helper or file
that only renames a single call, has one trivial user, or exists "for the
future". Decompose when it makes the code shorter, clearer or removes
duplication; otherwise keep it direct.
### 3. Do not add what was not asked for
- Implement exactly the request. No extra screens, buttons, settings,
refactors, renames, translations or "while I'm here" fixes.
- Ideas, improvements and problems noticed along the way are reported to the
user as suggestions. Nothing is implemented without the user agreeing first.
- When a request is ambiguous, ask instead of guessing big.
- Keep the diff small. If a fix grows beyond the obvious change, say so and
explain why before making it.
### Other rules
- **Committing is the user's call.** Prepare the change and a commit message;
do not commit or push unless asked. Commit messages use a conventional prefix
with a scope (`fix(ui): …`, `feat(macos): …`, `chore: …`): a one-line
subject plus one or two short body lines saying what was wrong and what
changed. Pull requests target `dev`.
- **Format C++ with `.clang-format`.** Format the lines you touch; do not
reformat files the change does not otherwise touch.
- **Delete unused code** instead of keeping it "for later".
- **Do not edit `client/3rd/`.** Fix vendored code through its upstream or a
Conan recipe.
## UI And Design System
- **Figma is the source of truth.** Screens follow the "Amnezia APP VPN" file;
components follow the "Amnezia DS Library". Where the two disagree, the app
screens win.
- **Build UI from the design system controls** in `Controls2/`
(`ButtonType`, `ListItemType`, `TextFieldType`, `SheetType`,
`NavigationBarType` and the others) and the text types in
`Controls2/TextTypes/`. Do not style raw Qt Quick Controls on a page.
- **No hex colours in QML.** Every colour is a semantic token from
`AmneziaStyle.color`. A colour the tokens do not have is added to
`AmneziaStyle` first, with the name the design system gives it.
- **Icons come from Figma**, exported from the DS Library. Never draw an icon
by hand. Exports include page background rectangles — strip them and keep
the `0 0 24 24` viewBox.
- **Restyling a component never redesigns a screen.** A design system update
changes components; screen layout and behaviour stay unless the user asks.
- **No Qt5Compat.GraphicalEffects.** It overflows the interpreted JS stack on
iOS. Use `QtQuick.Effects` (`MultiEffect`), and create effects only while
they are visible: each instance is expensive to create.
### QML pitfalls
- A vertical `ListView` resets the delegate's `x`. Put insets on the
ListView's `leftMargin`/`rightMargin`, or on a child inside the delegate.
- `lineHeightMode: Text.FixedHeight` puts all the extra leading below the text,
while Figma splits it evenly. The text types already compensate with
`halfLeading` padding; a new text type must do the same.
- QML is loaded from `qml.qrc`, not compiled ahead of time: a QML error shows
up only at runtime. When moving QML between files, compare the `import`
lines — types registered from C++ under their own URIs fail only when the
page is opened.
## Translations
- Every user-visible string goes through `qsTr()` in QML or `tr()` in C++.
- The `.ts` files contain only finished, non-empty translations: no
`vanished`, `obsolete` or `unfinished` entries, and every language holds the
same set of strings. `lrelease` must report `N finished and 0 unfinished` for
every file.
- Refresh `.ts` files only when the task is about translations. `lupdate` run
through `qt6_add_translations` misses QML, so run it over an explicit file
list that skips `client/3rd` (from `client/translations`):
```bash
find .. -path ../3rd -prune -o -path ../3rd-prebuilt -prune -o \
\( -name '*.qml' -o -name '*.cpp' -o -name '*.h' -o -name '*.mm' \) -print \
| sort > /tmp/src.lst
<Qt>/bin/lupdate @/tmp/src.lst -ts amneziavpn_*.ts
```
- `lupdate` output differs between macOS and Windows (keychain strings swap
places), so a large `.ts` diff after a refresh is often only that.
- Technical terms stay in English in every language: protocol parameter names
and their descriptions (`Jc`, `S1`, `H1`…), config keys, transport and
algorithm names. Ordinary UI words are translated.
- Renaming a QML file changes its translation context, so its strings have to
be carried over.
## Which Check When
- **Touched C++** — build the affected targets, and run `ctest` when the change
touches anything the tests cover (servers, imports, configs, settings,
split tunneling, XRay).
- **Touched QML** — build, then open every page you changed. If the app cannot
be launched, at least load the changed files through the QML engine so that
syntax, imports and type errors surface, and say what was not checked.
- **Touched a protocol config, import or connection path** — check self-hosted,
Premium/Free and native configs, and every platform that consumes the field
(desktop service, Android, iOS/macOS NE).
- **Touched CMake, recipes or the app icon** — reconfigure from a clean build
directory as well: branding paths are CMake cache variables and keep stale
values in an existing build directory.
- **Touched translations** — `lrelease` on every `.ts` file, see above.
## Privacy
- Never commit or repeat private keys, `vpn://` keys, server addresses,
passwords or tokens — including ones found in pasted configs, logs or test
output.
- Logs must not contain secrets or full config bodies.
- Local absolute paths (`/Users/...`, `C:\...`) do not belong in repository
files.
## Verification Culture
- Verify your own work before reporting it. "Should work" is not a result;
say exactly what was built, run or opened, and what was not.
- Hypotheses are settled by an experiment, not by reasoning about the code.
- After a wrong diagnosis, remove the code it produced.
+6
View File
@@ -0,0 +1,6 @@
# CLAUDE.md
The operating guide for this repository lives in [AGENTS.md](AGENTS.md), so
every agent runtime reads the same file.
@AGENTS.md
+2 -2
View File
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.1.0.0 CACHE STRING "Client app version")
set(AMNEZIAVPN_VERSION 5.1.0.5 CACHE STRING "Client app version")
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
@@ -34,7 +34,7 @@ set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
# bump by 2 on every release, because we're releasing two versions on the play store
set(APP_ANDROID_VERSION_CODE 2173)
set(APP_ANDROID_VERSION_CODE 2188)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
+5
View File
@@ -104,6 +104,7 @@ else()
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_id_ID.ts
)
endif()
@@ -226,6 +227,10 @@ if(ANDROID)
include(cmake/android.cmake)
endif()
if(APPLE)
include(cmake/appleAppIcon.cmake)
endif()
if(IOS)
include(cmake/ios.cmake)
include(cmake/ios-arch-fixup.cmake)
+12 -4
View File
@@ -49,15 +49,23 @@ set(SOURCES ${SOURCES}
find_package(awg-android REQUIRED)
set(LIBS ${LIBS} amnezia::awg-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
$<TARGET_FILE:amnezia::awg-android>
${AMNEZIA_ANDROID_LIBWG_PATH}
${AMNEZIA_ANDROID_LIBWG_QUICK_PATH}
)
find_package(amnezia-libxray REQUIRED)
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
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})
get_filename_component(OPENVPN_PT_ANDROID_LIB_DIR ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH} DIRECTORY)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH}
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpn3.so
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpnutil.so
${OPENVPN_PT_ANDROID_LIB_DIR}/librsapss.so
)
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
+51
View File
@@ -0,0 +1,51 @@
function(apple_icon_composer_supported OUT_VAR)
set(${OUT_VAR} FALSE PARENT_SCOPE)
execute_process(
COMMAND xcrun actool --version --output-format human-readable-text
OUTPUT_VARIABLE ACTOOL_VERSION_OUTPUT
ERROR_QUIET
)
string(REGEX MATCH "short-bundle-version: ([0-9]+)" ACTOOL_VERSION_MATCH "${ACTOOL_VERSION_OUTPUT}")
if(NOT ACTOOL_VERSION_MATCH OR CMAKE_MATCH_1 LESS 26)
return()
endif()
execute_process(
COMMAND sw_vers -productVersion
OUTPUT_VARIABLE HOST_MACOS_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOST_MACOS_VERSION VERSION_LESS 26)
return()
endif()
set(${OUT_VAR} TRUE PARENT_SCOPE)
endfunction()
function(check_apple_app_icon ICON_PATH)
if(NOT EXISTS "${ICON_PATH}/icon.json")
message(FATAL_ERROR "App icon not found: ${ICON_PATH} (expected an Icon Composer .icon bundle)")
endif()
apple_icon_composer_supported(ICON_COMPOSER_SUPPORTED)
if(NOT ICON_COMPOSER_SUPPORTED)
message(FATAL_ERROR "Xcode 26 and macOS 26 or later are required to build the app icon ${ICON_PATH}")
endif()
endfunction()
function(select_macos_app_icon ICON_PATH ICNS_PATH OUT_USE_ICON_COMPOSER)
apple_icon_composer_supported(ICON_COMPOSER_SUPPORTED)
if(ICON_COMPOSER_SUPPORTED)
check_apple_app_icon(${ICON_PATH})
set(${OUT_USE_ICON_COMPOSER} TRUE PARENT_SCOPE)
return()
endif()
if(NOT EXISTS "${ICNS_PATH}")
message(FATAL_ERROR "Xcode 26 and macOS 26 are required for ${ICON_PATH}, and the fallback icon ${ICNS_PATH} was not found")
endif()
message(STATUS "Icon Composer needs Xcode 26 and macOS 26, using the fallback app icon ${ICNS_PATH}")
set(${OUT_USE_ICON_COMPOSER} FALSE PARENT_SCOPE)
endfunction()
+2 -2
View File
@@ -19,8 +19,8 @@ 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")
if(NOT CLIENT_IOS_APP_ICON_PATH)
set(CLIENT_IOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "iOS app icon (Icon Composer .icon)")
endif()
if(NOT CLIENT_IOS_BUNDLE_ID)
+3 -3
View File
@@ -11,11 +11,11 @@ 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")
if(NOT CLIENT_MACOS_APP_ICON_PATH)
set(CLIENT_MACOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "macOS app icon (Icon Composer .icon)")
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")
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/macos/app/AppIcon.icns" CACHE FILEPATH "macOS app icon (.icns) for builds without Xcode 26 and macOS 26")
endif()
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
+7 -3
View File
@@ -127,14 +127,18 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
check_apple_app_icon(${CLIENT_IOS_APP_ICON_PATH})
set_source_files_properties(
${CLIENT_IOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
${CLIENT_IOS_APP_ICON_PATH}
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
)
target_sources(${PROJECT} PRIVATE
${CLIENT_IOS_LAUNCHSCREEN_PATH}
${CLIENT_IOS_MEDIA_ASSETS_PATH}
${CLIENT_IOS_APP_ICON_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
+36 -4
View File
@@ -34,10 +34,42 @@ set(SOURCES ${SOURCES}
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})
select_macos_app_icon(${CLIENT_MACOS_APP_ICON_PATH} ${CLIENT_MACOS_APP_ICNS_PATH} USE_ICON_COMPOSER)
set(APP_ICON_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/appicon)
set(MACOSX_BUNDLE_ICON_FILE AppIcon)
if(USE_ICON_COMPOSER)
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/Assets.car ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
file(GLOB_RECURSE APP_ICON_INPUTS CONFIGURE_DEPENDS "${CLIENT_MACOS_APP_ICON_PATH}/*")
add_custom_command(
OUTPUT ${APP_ICON_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${APP_ICON_OUTPUT_DIR}
COMMAND xcrun actool --output-format human-readable-text --warnings --errors
--platform macosx
--minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET}
--app-icon AppIcon
--output-partial-info-plist ${APP_ICON_OUTPUT_DIR}/partial.plist
--compile ${APP_ICON_OUTPUT_DIR}
${CLIENT_MACOS_APP_ICON_PATH}
DEPENDS ${APP_ICON_INPUTS}
COMMENT "Compiling macOS app icon"
VERBATIM
)
set_source_files_properties(${APP_ICON_FILES} PROPERTIES GENERATED TRUE)
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND plutil -replace CFBundleIconName -string AppIcon $<TARGET_BUNDLE_CONTENT_DIR:${PROJECT}>/Info.plist
VERBATIM
)
else()
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
configure_file(${CLIENT_MACOS_APP_ICNS_PATH} ${APP_ICON_FILES} COPYONLY)
endif()
set_source_files_properties(${APP_ICON_FILES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${APP_ICON_FILES})
target_compile_options(${PROJECT} PRIVATE
-DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\"
+16 -5
View File
@@ -122,13 +122,24 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
set_source_files_properties(
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
select_macos_app_icon(${CLIENT_MACOS_APP_ICON_PATH} ${CLIENT_MACOS_APP_ICNS_PATH} USE_ICON_COMPOSER)
if(USE_ICON_COMPOSER)
set(APP_ICON_SOURCE ${CLIENT_MACOS_APP_ICON_PATH})
set_source_files_properties(
${APP_ICON_SOURCE}
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
)
else()
set(APP_ICON_SOURCE ${CMAKE_CURRENT_BINARY_DIR}/appicon/AppIcon.icns)
configure_file(${CLIENT_MACOS_APP_ICNS_PATH} ${APP_ICON_SOURCE} COPYONLY)
set_source_files_properties(${APP_ICON_SOURCE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
endif()
target_sources(${PROJECT} PRIVATE
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
${APP_ICON_SOURCE}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
-58
View File
@@ -40,64 +40,6 @@ function(osx_bundle_files TARGET)
endforeach()
endfunction(osx_bundle_files)
## A helper to bundle an asset catalog.
function(osx_bundle_assetcatalog TARGET)
cmake_parse_arguments(XCASSETS
""
"CATALOG;PLATFORM"
"DEVICES"
${ARGN})
if(XCASSETS_PLATFORM)
set(XCASSETS_TARGET_ARGS --platform ${XCASSETS_PLATFORM})
elseif(IOS)
set(XCASSETS_TARGET_ARGS --platform iphoneos)
else()
set(XCASSETS_TARGET_ARGS --platform macosx)
endif()
foreach(DEVNAME ${XCASSETS_DEVICES})
list(APPEND XCASSETS_TARGET_ARGS --target-device ${DEVNAME})
endforeach()
list(APPEND XCASSETS_TARGET_ARGS --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET})
## Compile the asset catalog
set(XCASSETS_GEN_PLIST ${CMAKE_CURRENT_BINARY_DIR}/xcassets_generated_info.plist)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car ${XCASSETS_GEN_PLIST}
MAIN_DEPENDENCY ${XCASSETS_CATALOG}/Contents.json
COMMENT "Building asset catalog"
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets
COMMAND actool --output-format human-readable-text --notices --warnings
${XCASSETS_TARGET_ARGS}
--app-icon AppIcon
--output-partial-info-plist ${XCASSETS_GEN_PLIST}
--development-region en --enable-on-demand-resources NO
--compile ${CMAKE_CURRENT_BINARY_DIR}/xcassets ${XCASSETS_CATALOG}
)
## Patch the asset catalog into the target bundle.
if(NOT IOS)
set(XCASSETS_RESOURCE_DIR "Resources")
endif()
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMENT "Bundling asset catalog"
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/${XCASSETS_RESOURCE_DIR}
COMMAND ${CLIENT_ROOT_DIR}/scripts/macos/merge_plist.py ${XCASSETS_GEN_PLIST} -o $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/Info.plist
)
target_sources(${TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car)
set_source_files_properties(
${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car
${XCASSETS_GEN_PLIST}
PROPERTIES
GENERATED TRUE
HEADER_FILE_ONLY TRUE
)
target_sources(${TARGET} PRIVATE ${XCASSETS_GEN_PLIST})
set_source_files_properties(${XCASSETS_GEN_PLIST} PROPERTIES GENERATED TRUE)
endfunction()
## A helper to code-sign an executable.
function(osx_codesign_target TARGET)
## Xcode should perform automatic code-signing for us.
@@ -24,9 +24,9 @@
namespace {
Logger logger("XrayConfigurator");
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
QString effectiveServerFlow(const amnezia::XrayServerConfig &srv)
{
return amnezia::xrayEffective::clientFlow(srv);
return amnezia::xrayEffective::serverFlow(srv);
}
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
@@ -139,7 +139,7 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
return ErrorCode::NoError;
}
const QString flowValue = effectiveClientFlow(srv);
const QString flowValue = effectiveServerFlow(srv);
QString realityPublicKey;
QString realityShortId;
if (effectiveSecurity(srv) == QLatin1String("reality")) {
@@ -314,7 +314,7 @@ ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &c
return errorCode;
}
const QString flowValue = effectiveClientFlow(srv);
const QString flowValue = effectiveServerFlow(srv);
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode);
+4 -1
View File
@@ -143,6 +143,9 @@ protected:
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
SubscriptionController* subscriptionControllerProtected() const { return m_subscriptionController; }
ApiCountryModel* apiCountryModelProtected() const { return m_apiCountryModel; }
ApiCountryListModel* apiCountryListModelProtected() const { return m_apiCountryListModel; }
ApiCountryListModel* apiConfigsCountryListModelProtected() const { return m_apiConfigsCountryListModel; }
private:
void initRepositories();
@@ -170,7 +173,7 @@ private:
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
ConnectionUiController* m_connectionUiController;
FocusController* m_focusController;
FocusController* m_focusController = nullptr;
PageController* m_pageController;
InstallUiController* m_installUiController;
ImportUiController* m_importController;
+61 -4
View File
@@ -12,6 +12,7 @@
#include "core/utils/serverConfigUtils.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "vpnConnection.h"
#include "ui/controllers/qml/focusController.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/controllers/connectionUiController.h"
#include "ui/controllers/settingsUiController.h"
@@ -83,6 +84,7 @@ void CoreSignalHandlers::initAllHandlers()
initAppSplitTunnelingModelUpdateHandler();
initPrepareConfigHandler();
initUnsupportedConnectDrawerHandler();
initBackNavigationHandler();
initStrictKillSwitchHandler();
initAndroidSettingsHandler();
initAndroidConnectionHandler();
@@ -185,12 +187,21 @@ void CoreSignalHandlers::initCountryListSortModeHandler()
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
const auto reloadSortModes = [repository, models]() {
for (ApiCountryListModel *model : models) {
model->setSortMode(repository->countryListSortMode(model->listId()));
}
};
reloadSortModes();
for (ApiCountryListModel *model : models) {
model->setSortMode(repository->countryListSortMode(model->listId()));
connect(model, &ApiCountryListModel::sortModeChanged, this, [repository, model]() {
repository->setCountryListSortMode(model->listId(), model->sortMode());
});
}
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this, reloadSortModes);
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadSortModes);
}
void CoreSignalHandlers::initCountryListStateHandler()
@@ -200,10 +211,45 @@ void CoreSignalHandlers::initCountryListStateHandler()
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
ApiCountryModel *countryModel = m_coreController->m_apiCountryModel;
SecureServersRepository *serversRepository = m_coreController->m_serversRepository;
const auto reloadFavorites = [repository, models, countryModel]() {
const QStringList codes = repository->favoriteLocations(countryModel->serverId());
for (ApiCountryListModel *model : models) {
model->setFavorites(codes);
}
};
const auto migrateLegacyFavorites = [repository, serversRepository]() {
QStringList apiServerIds;
for (const QString &serverId : serversRepository->orderedServerIds()) {
if (serversRepository->apiV2Config(serverId).has_value()) {
apiServerIds.append(serverId);
}
}
repository->migrateLegacyFavoriteLocations(apiServerIds);
};
migrateLegacyFavorites();
reloadFavorites();
connect(countryModel, &ApiCountryModel::serverIdChanged, this, reloadFavorites);
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this,
[migrateLegacyFavorites, reloadFavorites]() {
migrateLegacyFavorites();
reloadFavorites();
});
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadFavorites);
connect(serversRepository, &SecureServersRepository::serverRemoved, this,
[repository, countryModel](const QString &serverId) {
repository->removeFavoriteLocations(serverId);
if (countryModel->serverId() == serverId) {
countryModel->setServerId(QString());
}
});
for (ApiCountryListModel *model : models) {
model->setFavorites(repository->favoriteLocations());
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model](const QStringList &codes) {
repository->setFavoriteLocations(codes);
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model, countryModel](const QStringList &codes) {
repository->setFavoriteLocations(countryModel->serverId(), codes);
for (ApiCountryListModel *other : models) {
if (other != model) {
other->setFavorites(codes);
@@ -226,6 +272,7 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
return;
}
m_coreController->m_apiCountryModel->setServerId(processedServerId);
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode,
apiV2->apiConfig.userCountryCode);
@@ -412,6 +459,16 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initBackNavigationHandler()
{
if (!m_coreController->m_focusController) {
return;
}
connect(m_coreController->m_focusController, &FocusController::backRequested,
m_coreController->m_pageController, &PageController::escapePressed);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()
{
connect(m_coreController->m_settingsUiController, &SettingsUiController::strictKillSwitchEnabledChanged, m_coreController->m_connectionController,
@@ -36,6 +36,7 @@ private:
void initAppSplitTunnelingModelUpdateHandler();
void initPrepareConfigHandler();
void initUnsupportedConnectDrawerHandler();
void initBackNavigationHandler();
void initStrictKillSwitchHandler();
void initAndroidSettingsHandler();
void initAndroidConnectionHandler();
@@ -88,7 +88,7 @@ bool LocalProxyController::setPort(int port)
bool LocalProxyController::isPortBusy(int port) const
{
return !portUtils::isPortAvailable(port);
return !isPortAvailable(port);
}
bool LocalProxyController::enable(const QString &ownerId, int port)
@@ -101,7 +101,7 @@ bool LocalProxyController::enable(const QString &ownerId, int port)
return false;
}
if (!portUtils::isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
if (!isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
return false;
}
@@ -330,7 +330,8 @@ std::optional<int> LocalProxyController::selectProxyPort() const
port = localProxy::defaultProxyPort;
}
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs)) {
const auto isAvailable = [this](int p) { return isPortAvailable(p); };
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs, isAvailable)) {
return port;
}
@@ -340,10 +341,10 @@ std::optional<int> LocalProxyController::selectProxyPort() const
}
std::optional<int> freePort;
if (port != localProxy::defaultProxyPort && portUtils::isPortAvailable(localProxy::defaultProxyPort)) {
if (port != localProxy::defaultProxyPort && isPortAvailable(localProxy::defaultProxyPort)) {
freePort = localProxy::defaultProxyPort;
} else {
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax);
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax, isAvailable);
}
if (!freePort) {
@@ -491,6 +492,20 @@ bool LocalProxyController::isXrayEngineRunning() const
[]() { return false; });
}
bool LocalProxyController::isPortAvailable(int port) const
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
return IpcClient::withInterface(
[port](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->isTcpPortAvailable(port);
return reply.waitForFinished() && reply.returnValue();
},
[port]() { return portUtils::isPortAvailable(port); });
}
#else
bool LocalProxyController::isSupported() const
@@ -522,4 +537,9 @@ bool LocalProxyController::isXrayEngineRunning() const
return false;
}
bool LocalProxyController::isPortAvailable(int port) const
{
return portUtils::isPortAvailable(port);
}
#endif
@@ -68,6 +68,7 @@ private:
bool startXrayEngine(const QString &config);
bool stopXrayEngine();
bool isXrayEngineRunning() const;
bool isPortAvailable(int port) const;
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
@@ -779,8 +779,11 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
}
QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object();
jsonConfig[configKey::mtu] =
ContainerUtils::isAwgContainer(dockerContainer) ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
if (jsonConfig.value(configKey::mtu).toString().trimmed().isEmpty()) {
jsonConfig[configKey::mtu] = ContainerUtils::isAwgContainer(dockerContainer)
? protocols::awg::defaultMtu
: protocols::wireguard::defaultMtu;
}
containerConfig[configKey::lastConfig] = QString(QJsonDocument(jsonConfig).toJson());
@@ -790,4 +793,3 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
}
}
}
@@ -296,7 +296,7 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
portReinstall = isReinstallContainerRequired(container, oldConfig, newConfig);
reinstallRequired = portReinstall;
}
if (container == DockerContainer::Xray) {
if (container == DockerContainer::Xray && serverSettingsChanged) {
if (credentials.isValid()) {
binaryMismatch = xrayServerBinaryMismatchesExpected(credentials, sshSession);
if (binaryMismatch) {
@@ -518,6 +518,14 @@ namespace xrayEffective
return (rawTransport && secureFlow) ? srv.flow : QString();
}
QString serverFlow(const XrayServerConfig &srv)
{
QString flow = clientFlow(srv);
if (flow.endsWith(QLatin1String("-udp443")))
flow.chop(int(qstrlen("-udp443")));
return flow;
}
QString network(const XrayServerConfig &srv)
{
namespace px = protocols::xray;
@@ -576,7 +584,7 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
}
if (!alpnArray.isEmpty())
tlsSettings[px::alpn] = alpnArray;
if (clientSide)
if (clientSide && transport != QLatin1String(px::transportMkcp))
tlsSettings[px::fingerprint] = fingerprintEff;
if (!clientSide && !isThirdPartyConfig) {
QJsonObject cert;
@@ -597,9 +605,11 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
if (transport == QLatin1String(px::transportXhttp)) {
QJsonObject xo;
xo[px::xhttpHost] = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host;
const QString pathEff = xhttp.path.trimmed().isEmpty()
QString pathEff = xhttp.path.trimmed().isEmpty()
? QString::fromLatin1(px::defaultXhttpPath)
: xhttp.path;
: xhttp.path.trimmed();
if (!pathEff.startsWith(QLatin1Char('/')))
pathEff.prepend(QLatin1Char('/'));
xo[px::xhttpPath] = pathEff;
const QString modeEff = xrayEffective::xhttpModeSent(*this);
xo[px::xhttpMode] = modeEff;
@@ -1186,7 +1196,10 @@ XrayServerJsonStatus XrayServerConfig::fromServerInboundJson(const QJsonObject &
if (inbound.contains(px::settings)) {
const QJsonArray clients = inbound.value(px::settings).toObject().value(px::clients).toArray();
if (!clients.isEmpty()) {
srv.flow = clients[0].toObject().value(px::flow).toString();
const QString inboundFlow = clients[0].toObject().value(px::flow).toString();
if (srv.flow != inboundFlow + QLatin1String("-udp443")) {
srv.flow = inboundFlow;
}
}
}
@@ -1305,7 +1318,7 @@ QJsonObject XrayServerConfig::serverView() const
view[QStringLiteral("port")] = port.isEmpty() ? QString::fromLatin1(px::defaultPort) : port;
view[QStringLiteral("transportProto")] = transportProto;
view[px::flow] = xrayEffective::clientFlow(*this);
view[px::flow] = xrayEffective::serverFlow(*this);
view[px::streamSettings] = serverStreamSettings();
@@ -187,6 +187,7 @@ namespace xrayEffective
QString security(const XrayServerConfig &srv);
QString clientFlow(const XrayServerConfig &srv);
QString serverFlow(const XrayServerConfig &srv);
QString network(const XrayServerConfig &srv);
QString xhttpModeSent(const XrayServerConfig &srv);
}
@@ -385,14 +385,57 @@ void SecureAppSettingsRepository::setCountryListSortMode(const QString &listId,
setValue("Conf/countryListSortMode/" + listId, mode);
}
QStringList SecureAppSettingsRepository::favoriteLocations() const
QStringList SecureAppSettingsRepository::favoriteLocations(const QString &serverId) const
{
return value("Conf/favoriteLocations").toStringList();
QStringList codes = value("Conf/favoriteLocations").toMap().value(serverId).toStringList();
codes.removeAll(QString());
codes.removeDuplicates();
codes.sort();
return codes;
}
void SecureAppSettingsRepository::setFavoriteLocations(const QStringList &codes)
void SecureAppSettingsRepository::setFavoriteLocations(const QString &serverId, const QStringList &codes)
{
setValue("Conf/favoriteLocations", codes);
if (serverId.isEmpty()) {
return;
}
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
if (codes.isEmpty()) {
favorites.remove(serverId);
} else {
favorites.insert(serverId, codes);
}
setValue("Conf/favoriteLocations", favorites);
}
void SecureAppSettingsRepository::removeFavoriteLocations(const QString &serverId)
{
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
if (favorites.remove(serverId) > 0) {
setValue("Conf/favoriteLocations", favorites);
}
}
void SecureAppSettingsRepository::migrateLegacyFavoriteLocations(const QStringList &serverIds)
{
// Before per-subscription favorites the key held one list shared by all servers
const QVariant stored = value("Conf/favoriteLocations");
const int type = stored.typeId();
if (type != QMetaType::QStringList && type != QMetaType::QVariantList && type != QMetaType::QString) {
return;
}
QStringList codes = stored.toStringList();
codes.removeAll(QString());
codes.removeDuplicates();
QVariantMap favorites;
if (!codes.isEmpty()) {
for (const QString &serverId : serverIds) {
favorites.insert(serverId, codes);
}
}
setValue("Conf/favoriteLocations", favorites);
}
bool SecureAppSettingsRepository::isNewsNotifications() const
@@ -80,8 +80,10 @@ public:
int countryListSortMode(const QString &listId) const;
void setCountryListSortMode(const QString &listId, int mode);
QStringList favoriteLocations() const;
void setFavoriteLocations(const QStringList &codes);
QStringList favoriteLocations(const QString &serverId) const;
void setFavoriteLocations(const QString &serverId, const QStringList &codes);
void removeFavoriteLocations(const QString &serverId);
void migrateLegacyFavoriteLocations(const QStringList &serverIds);
bool isNewsNotifications() const;
void setNewsNotifications(bool enabled);
@@ -149,6 +149,9 @@ void SecureServersRepository::loadFromStorage()
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
.array();
QJsonArray persistedArray;
bool hasGeneratedIds = false;
for (int i = 0; i < serversArray.size(); ++i) {
const QJsonObject json = serversArray.at(i).toObject();
const QString candidateId = readStorageServerId(json);
@@ -158,15 +161,25 @@ void SecureServersRepository::loadFromStorage()
qWarning() << "Skipping stored server config with unsupported format version"
<< serverConfigUtils::configFormatVersion(strippedJson);
++m_unsupportedFormatConfigsCount;
persistedArray.append(json);
continue;
}
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
persistedArray.append(json);
continue;
}
m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedJson));
m_orderedServerIds.append(serverId);
persistedArray.append(m_serverJsonById.value(serverId));
hasGeneratedIds = hasGeneratedIds || serverId != candidateId;
}
// Per-server settings are keyed by id, so a generated one must survive restarts.
// Unlike syncToStorage(), this keeps entries that were skipped above
if (hasGeneratedIds) {
setValue(QStringLiteral("Servers/serversList"), QJsonDocument(persistedArray).toJson());
}
updateDefaultServerFromStorage();
@@ -94,8 +94,8 @@ namespace amnezia
constexpr char defaultXPaddingMethod[] = "Repeat-x";
constexpr char defaultXPaddingKey[] = "x_padding";
constexpr char defaultXPaddingHeader[] = "X-Padding";
constexpr char defaultXPaddingBytesMin[] = "1";
constexpr char defaultXPaddingBytesMax[] = "256";
constexpr char defaultXPaddingBytesMin[] = "100";
constexpr char defaultXPaddingBytesMax[] = "1000";
constexpr char defaultMkcpTti[] = "50";
constexpr char defaultMkcpUplinkCapacity[] = "5";
+4 -4
View File
@@ -21,12 +21,12 @@ bool portUtils::isPortAvailable(int port)
return success;
}
bool portUtils::waitForPort(int port, int timeoutMs)
bool portUtils::waitForPort(int port, int timeoutMs, const PortCheck &isAvailable)
{
QElapsedTimer timer;
timer.start();
while (!isPortAvailable(port)) {
while (!isAvailable(port)) {
if (timer.elapsed() >= timeoutMs) {
return false;
}
@@ -36,13 +36,13 @@ bool portUtils::waitForPort(int port, int timeoutMs)
return true;
}
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort)
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort, const PortCheck &isAvailable)
{
startPort = qMax(startPort, localProxy::proxyPortMin);
endPort = qMin(endPort, localProxy::proxyPortMax);
for (int port = startPort; port <= endPort; ++port) {
if (isPortAvailable(port)) {
if (isAvailable(port)) {
return port;
}
}
+5 -2
View File
@@ -1,13 +1,16 @@
#ifndef PORTUTILS_H
#define PORTUTILS_H
#include <functional>
#include <optional>
namespace portUtils
{
using PortCheck = std::function<bool(int)>;
bool isPortAvailable(int port);
bool waitForPort(int port, int timeoutMs);
std::optional<int> findFirstAvailablePort(int startPort, int endPort);
bool waitForPort(int port, int timeoutMs, const PortCheck &isAvailable = isPortAvailable);
std::optional<int> findFirstAvailablePort(int startPort, int endPort, const PortCheck &isAvailable = isPortAvailable);
}
#endif // PORTUTILS_H
@@ -0,0 +1,35 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg x="94.969" y="72.212" width="840" height="809.053" viewBox="0 0 840 809.053" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_5470_4284" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="841" height="810">
<path d="M838.697 289.781C839.128 290.057 839.092 290.7 838.633 290.929C835.038 292.719 831.485 294.6 827.806 296.232C799.555 308.776 772.521 323.383 749.551 344.372C741.699 351.545 734.598 359.689 727.949 368.025C721.643 375.923 719.536 385.597 719.631 395.64C719.651 397.39 719.936 399.14 720.232 401.108C720.434 402.455 721.585 403.47 722.947 403.47H754.934C756.426 403.47 757.641 404.682 757.573 406.173C757.006 418.719 752.651 429.513 745.064 438.533C734.995 450.489 730.891 464.7 726.144 478.966C719.221 499.763 718.756 521.202 720.398 542.478C722.108 564.655 713.899 582.337 700.656 598.83C690.259 611.783 680.19 625.011 669.847 638.019C659.244 651.355 655.291 666.414 656.426 683.344C657.89 705.043 662.979 725.963 667.727 747.088C671.598 764.332 672.871 782.151 675.21 799.723C675.317 800.514 675.272 801.319 675.174 802.406C675.006 804.292 672.993 805.496 671.237 804.786C667.425 803.244 663.961 801.983 661.064 799.914C647.862 790.5 639.736 777.081 633.087 762.665C629.836 755.612 626.686 748.509 623.528 741.499C623.236 740.851 622.7 740.346 622.038 740.086C612.637 736.39 610.069 734.129 607.429 727.667C607.012 726.646 606.042 725.945 604.94 725.931C590.847 725.747 576.823 727.121 563.328 731.798C539.442 740.065 515.514 748.249 491.969 757.391C465.73 767.585 438.573 769.648 411.239 766.355C385.068 763.198 359.197 757.514 333.176 753.1C324.448 751.625 315.61 750.75 306.841 749.425C304.666 749.097 302.504 748.249 300.507 747.279C284.746 739.6 269.178 731.511 253.267 724.159C236.015 716.193 222.69 721.877 216.301 739.559C213.675 746.828 212.115 754.467 209.57 761.764C205.685 772.9 201.622 784.009 197.148 794.913C193.962 802.692 187.887 806.658 178.915 807.795C177.278 808.003 175.934 806.595 176.072 804.951C176.687 797.655 177.055 790.615 178.241 783.722C180.676 769.607 184.781 755.724 186.272 741.527C187.749 727.343 187.024 712.913 186.86 698.607C186.833 696.53 184.918 694.125 183.289 692.458C171.62 680.597 159.95 668.736 147.993 657.176C135.338 644.933 123.039 632.28 114.585 616.812C106.896 602.751 94.8842 600.743 81.5728 603.339C60.1486 607.52 38.9845 612.959 17.7108 617.905C12.3942 619.143 7.11331 620.525 1.82366 621.848C1.1673 622.013 0.484093 621.706 0.169055 621.108C-0.123688 620.551 -0.0299188 619.87 0.401333 619.413C2.93984 616.719 5.31074 614.184 8.16162 611.811C24.2639 598.42 40.7356 585.452 56.6464 571.842C66.1408 563.712 74.9923 554.803 83.7754 545.894C85.5402 544.104 86.6484 540.674 86.4705 538.105C82.0653 474.457 96.8679 414.73 123.327 357.407C134.518 333.152 146.142 309.094 157.878 284.322C158.345 283.335 159.369 282.751 160.46 282.687C170.461 282.106 171.998 271.978 175.957 264.599C178.669 259.543 180.643 254.095 182.891 248.386C183.584 246.625 185.831 246.144 187.204 247.447C188.931 249.086 190.564 250.638 192.338 252.327C193.394 253.332 195.059 253.317 196.13 252.327C199.595 249.128 203.201 246.202 205.753 242.545C215.658 228.389 225.399 214.096 234.415 199.379C238.574 192.588 243.198 188.325 251.16 186.097C277.787 178.649 304.16 170.335 330.424 162.392C331.068 162.197 331.64 161.784 332.011 161.223C332.138 161.03 332.228 160.859 332.246 160.682C334.189 145.022 335.461 143.71 351.303 141.114C368.172 138.354 378.802 129.336 381.647 112.282C384.479 95.2702 385.314 77.9165 386.969 60.6994C388.351 46.2015 389.377 31.6626 391.087 17.2057C392.145 8.24311 394.608 3.97571 400.598 0.355299C401.894 -0.42824 403.557 0.142784 404.243 1.49332C407.282 7.4772 410.711 13.3289 412.744 19.6243C418.257 36.7184 422.635 54.1678 427.998 71.3166C432.608 86.0468 437.465 100.736 443.047 115.125C450.517 134.405 465.798 142.576 485.854 142.918C493.611 143.055 501.368 143.328 509.111 142.986C518.592 142.576 527.403 144.653 534.995 150.092C555.53 164.822 576.257 179.32 596.012 195.047C610.924 206.928 624.546 220.435 639.396 233.918C640.083 234.542 641.043 234.751 641.954 234.576C649.443 233.136 656.045 237.059 662.73 241.666C663.822 242.419 664.193 243.86 663.612 245.053C661.428 249.534 659.268 253.957 656.99 258.64C656.297 260.065 656.98 261.773 658.432 262.408C673.511 269.004 684.03 280.872 695.731 291.217C699.822 294.825 704.213 298.118 708.55 301.438C716.718 307.697 725.76 308.995 735.693 306.494C754.901 301.657 774.095 296.751 793.439 292.556C808.443 289.3 822.647 287.5 838.432 289.683C838.526 289.696 838.617 289.729 838.697 289.781ZM432.82 707.072C432.825 707.068 432.831 707.065 432.837 707.065C448.553 707.065 464.269 707.243 479.985 706.956C484.185 706.874 488.809 706.259 492.489 704.401C512.19 694.466 532.013 684.724 551.07 673.656C570.73 662.246 572.385 656.575 566.557 634.343C561.304 614.325 551.604 596.22 541.343 578.511C529.208 557.55 519.741 536.083 519.139 511.268C518.825 498.246 515.076 485.757 508.879 474.074C503.393 463.73 494.897 458.838 483.446 458.688C451.775 458.278 420.761 462.063 390.595 472.27C312.614 498.683 265.114 562.824 263.801 642.911C263.486 662.232 271.859 675.35 288.755 683.002C300.534 688.345 312.806 692.786 325.173 696.585C360.261 707.365 396.485 708.363 432.804 707.079C432.81 707.079 432.Line truncated
</mask>
<g mask="url(#mask0_5470_4284)">
<path d="M313.469 658.228C312.707 668.769 309.913 679.69 311.564 689.723C315.755 715.122 296.832 713.09 282.736 714.487C267.37 716.011 254.543 731.378 237.526 722.615C238.288 721.599 238.923 720.71 239.685 719.694C259.623 705.979 270.417 681.087 296.832 674.357C301.912 673.214 301.658 657.974 313.469 658.228Z" fill="#FEDAA5"/>
<path d="M-8.20716 410.326C-8.20716 267.947 -8.20716 125.569 -8.32803 -16.8094C-8.32803 -20.9187 -7.48198 -21.7648 -3.37258 -21.7648C281.747 -21.6439 566.987 -21.6439 852.106 -21.7648C856.216 -21.7648 857.062 -20.9187 857.062 -16.8094C856.941 268.31 856.941 553.55 857.062 838.669C857.062 842.779 856.216 843.625 852.106 843.625C566.987 843.504 281.747 843.504 -3.37258 843.625C-7.48198 843.625 -8.32803 842.779 -8.32803 838.669C-8.20716 695.808 -8.20716 553.067 -8.20716 410.326ZM-0.350972 449.607C-0.350972 573.493 -0.471837 697.379 -0.471837 821.144C1.94545 825.858 -6.87765 835.89 4.72534 835.89C258.057 836.131 511.389 836.131 764.721 836.01C766.897 836.01 771.127 832.989 770.885 832.264C766.413 812.563 778.742 813.892 791.674 814.497C805.694 815.222 819.835 814.376 833.977 814.738C839.053 814.859 840.262 813.409 840.262 808.332C840.02 751.043 840.141 693.874 840.141 636.584C840.02 421.929 840.02 207.273 839.899 -7.38192C838.932 -8.22797 838.086 -9.67835 837.119 -9.67835C811.133 -9.67835 785.268 -9.67835 759.282 -9.31575C757.953 -9.31575 755.415 -5.20636 755.415 -3.15166C755.173 69.7296 755.415 142.49 756.382 215.25C756.624 232.655 747.075 239.544 733.418 243.654C728.825 244.862 724.715 245.467 720.122 246.675C702.597 246.675 684.467 241.236 677.095 267.343C672.381 283.66 657.152 296.955 646.637 311.579C641.681 315.205 637.209 319.798 631.77 322.336C606.751 334.423 583.062 315.568 556.351 319.315C526.86 323.424 507.486 331.521 506.519 362.583C506.519 364.033 521.526 393.84 493.742 376.753C492.533 376.028 490.842 376.725 489.271 376.725C469.087 363.309 444.914 377.088 424.246 366.935C423.4 365.364 422.554 363.793 421.587 362.222C421.708 354.124 422.433 346.026 421.708 338.049C420.62 326.808 422.191 320.403 436.212 322.82C444.551 324.27 458.088 327.05 461.11 317.864C472.108 284.627 488.667 254.29 502.929 222.744C511.148 204.494 492.172 186.968 489.875 168.355C487.579 150.467 472.834 135.238 478.514 116.142C486.491 89.7931 505.467 69.9714 520.817 47.974C522.388 46.4028 523.959 44.8315 525.651 43.1394C538.826 23.5594 554.175 6.63836 577.865 -0.371785C581.491 -2.30562 589.468 1.19945 588.259 -6.41501C587.292 -12.9417 580.403 -9.55748 576.535 -9.55748C391.371 -9.43662 206.206 -9.07402 20.9212 -10.1618C-0.713563 -10.2827 -4.09777 -2.78907 -0.592699 14.7363C-0.592699 154.818 -0.592699 295.021 -0.592699 435.103C-0.471835 439.938 -0.350972 444.772 -0.350972 449.607ZM640.352 95.8363C640.11 95.8363 639.868 95.8363 639.506 95.8363C639.506 64.049 639.506 32.2616 639.506 0.474274C639.506 -3.0308 641.077 -7.50278 634.429 -7.38192C627.782 -7.26105 629.716 -2.54734 629.716 0.715998C629.232 62.7195 628.87 124.723 628.386 186.726C628.386 193.616 631.287 198.088 637.572 200.505C638.055 200.747 640.231 198.692 640.231 197.846C640.352 163.762 640.352 129.799 640.352 95.8363Z" fill="#FCE8EC"/>
<path d="M839.683 -7.14046C839.803 207.515 839.803 422.17 839.924 636.825C839.924 694.115 839.924 751.284 840.045 808.574C840.045 813.529 838.836 815.101 833.76 814.98C819.619 814.617 805.599 815.342 791.458 814.738C778.404 814.134 766.076 812.804 770.669 832.505C770.911 833.351 766.68 836.252 764.505 836.252C511.173 836.373 257.841 836.373 4.50893 836.131C-7.09406 836.131 1.60818 826.099 -0.688248 821.386C0.641262 820.056 1.97077 818.727 3.17942 817.397C16.3536 802.772 29.5279 788.148 42.5812 773.523C89.3558 772.798 136.251 772.556 183.026 770.381C188.465 770.139 189.673 787.664 196.321 772.919C198.617 767.843 202.848 763.733 206.111 759.14C205.386 762.524 202.968 766.876 204.177 769.172C207.803 775.82 210.583 768.326 212.396 766.996C224.12 757.569 234.151 768.205 243.941 770.622C250.347 772.194 254.578 783.313 259.412 790.323C263.642 796.487 267.268 803.135 271.136 809.541C271.378 804.223 271.861 798.905 271.982 793.708C272.103 788.873 271.499 784.038 272.103 779.204C274.037 763.975 270.773 749.713 290.233 738.231C305.824 729.045 329.151 716.959 324.921 689.039C323.108 677.194 324.921 664.866 325.162 652.659C337.249 643.956 349.456 635.133 361.543 626.431C378.584 611.806 398.285 602.379 419.558 595.852C420.162 595.852 420.766 595.852 421.371 595.852C422.337 595.49 423.425 595.006 424.392 594.644C448.323 597.061 445.543 577.36 449.774 563.702C480.594 551.979 515.645 574.58 545.257 551.616C553.475 545.21 564.474 541.826 571.122 534.453C582.241 522.125 593.844 512.818 610.886 509.918C620.313 508.346 635.542 514.511 638.201 502.424C640.981 489.854 636.026 475.713 634.455 462.176C640.256 454.924 645.937 447.793 651.738 440.542C656.573 425.554 666.363 437.036 673.615 436.795C697.908 435.707 701.776 413.468 713.137 399.448C734.047 373.583 746.617 342.762 756.89 311.821C766.68 282.693 757.495 251.872 757.495 221.656C757.616 147.566 757.495 73.597 757.615 -0.492919C757.615 -2.42675 758.582 -4.48145 759.066 -6.41528C760.516 -5.69009 761.967 -5.08577 763.296 -4.36058C762.329 -3.99798 761.241 -3.6354 760.275 -3.27281C760.637 -2.91021 760.879 -2.54762 761.241 -2.06416C761.846 -2.78935 762.571 -3.51453 763.175 -4.36058C788.919 -5.56922 814.301 -6.41528 839.683 -7.14046Z" fill="#F18928" stroke-width="1.34889" stroke="#F18928"/>
<path d="M-0.807763 435.101C-0.807763 295.019 -0.807763 154.816 -0.807763 14.7345C-0.203441 14.7345 0.521746 14.6137 1.12607 14.6137C74.6117 14.6137 148.097 15.3388 221.583 14.1302C241.646 13.7676 255.546 19.3274 268.962 33.952C281.773 47.7305 297.969 58.3666 312.715 70.3322C325.768 81.8143 319.725 98.7353 319.362 110.943C318.395 146.114 318.395 181.89 307.034 216.216C304.254 224.797 304.617 234.587 301.716 243.168C295.673 261.298 306.551 281.482 292.651 299.129C281.532 313.27 273.675 330.07 264.369 345.661C256.15 358.836 244.184 369.955 242.613 386.634C241.767 386.876 240.921 386.997 240.075 387.239C222.187 383.734 200.794 403.193 186.17 378.657C186.17 378.657 185.203 379.02 185.082 379.262C180.731 392.919 166.469 393.403 157.162 395.82C116.068 406.577 73.5239 406.094 31.5839 403.072C4.51027 401.138 -4.0711 409.599 0.884341 434.98C0.400883 435.101 -0.203441 435.101 -0.807763 435.101Z" fill="#80C9C1" stroke-width="1.34889" stroke="#80C9C1"/>
<path d="M264.488 345.543C273.794 329.951 281.65 313.151 292.77 299.01C306.669 281.364 295.792 261.179 301.835 243.05C304.736 234.468 304.252 224.799 307.153 216.097C318.393 181.892 318.393 145.996 319.481 110.824C319.844 98.4958 325.766 81.6956 312.833 70.2135C330.48 78.3114 354.169 68.2796 367.102 92.5734C377.979 112.999 393.934 87.3762 407.833 92.2108C410.371 83.992 417.381 78.3114 417.623 68.0379C417.865 59.94 420.161 42.1729 429.951 44.7111C460.288 52.6881 490.625 42.8981 520.479 48.337C505.129 70.3343 486.032 90.1561 478.176 116.505C472.495 135.48 487.241 150.83 489.537 168.718C491.834 187.331 510.809 204.856 502.591 223.107C488.329 254.653 471.77 285.111 460.772 318.227C457.75 327.413 444.213 324.633 435.874 323.183C421.732 320.766 420.282 327.05 421.37 338.412C422.095 346.389 421.37 354.487 421.249 362.585C404.207 361.739 387.165 361.376 370.244 359.805C365.168 359.321 360.333 355.937 355.378 353.882C357.795 349.048 355.499 341.433 363.717 339.862C364.563 339.741 366.014 336.72 365.651 336.115C360.938 329.589 355.982 323.304 350.906 317.14C350.785 317.019 347.884 318.59 346.796 319.799C324.799 345.18 299.659 361.497 264.488 345.543Z" fill="#95C7DB" stroke-width="1.34889" stroke="#95C7DB"/>
<path d="M520.601 48.3367C490.747 43.0186 460.41 52.6878 430.073 44.7107C420.162 42.1726 417.987 59.9397 417.745 68.0376C417.382 78.3111 410.493 83.9917 407.955 92.2105C394.056 87.3759 378.101 112.999 367.224 92.5731C354.291 68.2793 330.602 78.3111 312.955 70.2132C298.21 58.2476 281.893 47.6115 269.203 33.8329C255.787 19.2083 241.766 13.6486 221.824 14.0112C148.096 15.3407 74.6107 14.6155 1.12514 14.6155C4.9928 7.84708 -7.33537 -6.77753 8.25614 -6.8984C190.641 -7.50272 373.146 -7.38185 555.53 -6.8984C562.903 -6.8984 570.155 -2.4264 577.528 -0.0091138C553.838 6.88016 538.489 23.9221 525.314 43.5021C523.743 45.0733 522.172 46.6446 520.601 48.3367Z" fill="#87ADD4" stroke-width="1.34889" stroke="#87ADD4"/>
<path d="M498.602 502.547C499.81 490.098 505.854 483.934 519.028 482.967C532.806 482 524.104 468.947 527.73 461.937C536.07 445.62 541.75 427.611 540.904 408.515C546.222 406.581 551.903 405.372 556.617 402.471C573.054 392.319 584.778 376.969 605.929 371.168C617.532 367.904 629.377 368.75 640.255 365.003C660.197 358.114 659.231 336.479 668.054 321.613C678.931 312.911 683.887 300.945 690.172 288.859C695.973 277.86 687.15 256.83 709.268 255.742C715.795 255.379 718.212 252.116 719.3 246.556C723.893 245.348 728.585 244.232 733.178 243.023C744.056 296.445 721.476 339.984 682.316 369.838C655.242 390.385 656.088 414.437 651.737 440.302C645.935 447.554 640.255 454.685 634.453 461.937C628.289 473.661 617.291 467.738 608.347 468.826C600.37 469.793 592.392 467.496 585.02 473.661C560.243 494.57 531.477 505.085 498.602 502.547Z" fill="#FEDAA5" stroke-width="1.34889" stroke="#FEDAA5"/>
<path d="M668.424 321.413C659.601 336.279 660.396 357.987 640.454 364.998C629.576 368.744 617.68 367.868 606.077 371.131C584.926 376.933 573.171 392.323 556.733 402.475C552.02 405.376 546.124 406.608 540.926 408.542C523.643 398.027 506.368 387.192 488.963 376.676C490.534 376.676 492.589 375.951 493.677 376.676C520.509 393.235 506.126 363.986 506.126 362.535C507.093 331.473 526.673 323.375 556.043 319.266C582.754 315.519 606.322 334.374 631.462 322.287C636.901 319.628 641.373 315.156 646.329 311.53C653.701 314.794 661.172 318.149 668.424 321.413Z" fill="#FFF1CF" stroke-width="1.34889" stroke-linejoin="bevel" stroke="#FFF1CF"/>
<path d="M449.73 563.404C445.372 577.206 448.278 596.82 424.305 594.399C423.787 592.976 421.607 594.497 421.278 595.731C420.672 595.731 419.762 595.3 419.156 595.3C415.524 526.166 418.493 456.979 407.112 388.329C408.081 377.554 414.861 371.258 424.063 366.778C444.766 377.069 468.981 363.267 489.201 376.585C506.515 387.119 523.481 397.969 540.916 408.503C541.763 427.633 536.42 445.235 528.066 461.701C524.434 468.723 533.151 481.799 519.349 482.768C506.151 483.615 500.098 489.911 498.887 502.382C482.3 522.722 465.954 543.063 449.73 563.404Z" fill="#A29EA1" stroke-width="1.34889" stroke="#A29EA1"/>
<path d="M-0.807763 435.103C-0.203441 435.103 0.400882 434.982 1.0052 434.982C75.216 437.642 149.427 441.147 223.517 432.686C202.003 460.122 180.731 482.966 139.032 479.34C93.95 475.472 48.0215 480.79 2.57644 482.241C1.48866 471.363 0.521747 460.485 -0.566033 449.607C-0.686898 444.773 -0.807763 439.938 -0.807763 435.103Z" fill="#797AAA" stroke-width="1.34889" stroke="#797AAA"/>
<path d="M651.736 440.3C656.088 414.435 655.242 390.383 682.315 369.836C721.475 339.982 744.077 296.471 733.078 243.049C746.736 238.94 755.68 233.017 755.438 215.613C754.471 142.853 755.076 69.9712 755.196 -2.7892C755.196 -4.96476 757.614 -8.95329 759.064 -8.95329C785.05 -9.43674 810.915 -9.43674 836.901 -9.31588C837.868 -9.31588 838.714 -7.86551 839.681 -7.01946C814.299 -6.29427 788.918 -5.44822 763.536 -4.72303C762.086 -5.44822 760.635 -6.05253 759.306 -6.77772C758.822 -4.84389 757.855 -2.7892 757.855 -0.855369C757.735 73.2346 757.855 147.204 757.735 221.294C757.614 251.389 766.92 282.33 757.13 311.458C746.736 342.4 734.287 373.22 713.377 399.085C702.016 413.105 698.148 435.465 673.855 436.432C666.361 436.795 656.571 425.313 651.736 440.3Z" fill="#F8B570"/>
<path d="M424.42 366.647C415.355 371.118 408.194 377.935 407.106 388.692C402.271 391.955 397.316 395.339 392.481 398.603C368.309 397.877 344.257 397.031 320.084 396.306C315.249 395.581 310.414 394.735 305.58 394.01C285.637 382.528 263.761 388.208 242.73 386.516C244.302 369.837 256.267 358.717 264.486 345.543C299.658 361.376 324.676 345.06 346.916 319.678C348.003 318.469 350.904 316.898 351.025 317.019C356.101 323.183 361.057 329.468 365.77 335.995C366.133 336.599 364.803 339.5 363.837 339.742C355.618 341.313 358.035 348.927 355.497 353.762C360.452 355.816 365.287 359.201 370.363 359.684C387.284 361.135 404.806 361.362 421.848 362.208C422.694 363.78 423.574 365.075 424.42 366.647Z" fill="#87ADD4"/>
<path d="M640.578 95.6923C640.578 129.796 640.398 164.837 640.276 198.941C640.276 199.912 638.092 201.854 637.606 201.611C631.416 199.184 628.382 194.814 628.382 187.775C628.868 125.514 629.072 63.1936 629.558 0.932391C629.558 -2.34452 627.727 -7.33012 634.402 -7.45149C640.956 -7.57285 639.703 -3.06418 639.824 0.455463C639.824 32.375 639.728 63.6514 639.728 95.5709C639.971 95.6923 640.214 95.6923 640.578 95.6923Z" fill="#FCE8EC"/>
<path d="M668.056 321.732C660.804 318.468 653.311 314.601 646.059 311.337C656.574 296.713 671.561 283.539 676.275 267.222C683.769 241.236 702.922 246.407 720.327 246.286C719.239 251.846 716.039 255.498 709.513 255.74C687.395 256.828 696.58 277.979 690.779 288.977C684.615 301.185 678.934 313.03 668.056 321.732Z" fill="#F8C9D1" stroke-width="1.34889" stroke="#F8C9D1"/>
<path d="M577.525 -0.0101719C570.153 -2.42746 562.901 -6.89945 555.528 -6.89945C373.144 -7.26205 190.638 -7.50377 8.25367 -6.89945C-7.33785 -6.89945 4.86947 7.72515 1.12267 14.6144C0.518345 14.6144 -0.206842 14.7353 -0.811164 14.7353C-4.4371 -2.79006 -1.05289 -10.2837 20.5818 -10.1628C205.746 -9.07501 391.031 -9.55847 576.196 -9.31674C580.184 -9.31674 586.953 -12.7009 587.92 -6.17426C589.249 1.56107 581.272 -1.944 577.525 -0.0101719Z" fill="#95C7DB" stroke-width="1.34889" stroke="#95C7DB"/>
<path d="M-0.686898 449.607C0.400883 460.485 1.3678 471.363 2.45558 482.24C1.97212 489.613 1.0052 496.986 1.0052 504.359C0.88434 606.368 0.88434 708.378 1.0052 810.388C1.0052 812.684 2.33472 814.98 3.0599 817.277C1.73039 818.606 0.400882 819.936 -0.807763 821.265C-0.807763 697.379 -0.686898 573.493 -0.686898 449.607Z" fill="#ADA0CC"/>
<path d="M520.6 48.339C522.171 46.7677 523.743 45.1965 525.435 43.5044C523.743 45.0756 522.171 46.6469 520.6 48.339Z" fill="#95C7DB"/>
<path d="M3.05605 817.275C2.33087 814.979 1.00135 812.682 1.00135 810.386C0.88049 708.376 0.88049 606.367 1.00135 504.357C1.00135 496.984 1.96827 489.612 2.45173 482.239C48.0176 480.909 93.8253 475.471 138.908 479.338C180.606 482.964 201.878 460.121 223.392 432.684C223.996 432.564 224.722 432.443 225.326 432.201C233.786 429.059 237.775 433.893 245.148 438.244C273.793 454.924 304.976 446.342 332.412 433.047C350.783 424.224 363.353 429.421 369.759 443.079C386.438 479.217 379.186 518.982 383.537 557.054C388.493 599.84 362.628 589.567 340.51 589.687C325.764 589.808 311.14 589.929 296.394 589.567C293.252 589.446 289.626 588.116 287.209 586.061C270.892 573.008 251.674 588.841 234.995 580.623C217.107 571.799 205.141 594.039 188.704 589.204C183.628 587.754 179.881 577.964 175.53 578.084C149.423 578.568 123.074 578.689 100.956 596.818C91.408 604.675 75.333 606.367 74.6078 622.079C73.3992 646.373 73.6409 670.787 73.7618 695.081C73.8826 717.441 74.1244 717.441 86.936 722.034C87.6612 722.276 87.842 724.11 88.2046 725.198C72.9757 741.273 57.8077 757.326 42.5787 773.281C29.4045 788.026 16.2303 802.651 3.05605 817.275Z" fill="#6E5EA6" stroke-width="1.34889" stroke="#6E5EA6"/>
<path d="M88.1444 725.295C87.7818 724.208 87.6609 722.395 86.9357 722.153C74.1241 717.56 73.8824 717.681 73.7615 695.2C73.6406 670.906 73.5198 646.492 74.6076 622.198C75.3327 606.486 91.4077 604.793 100.956 596.937C123.074 578.808 149.423 578.566 175.529 578.203C179.76 578.082 183.627 587.872 188.704 589.323C205.141 594.157 217.107 571.918 234.995 580.741C251.553 588.96 270.891 573.127 287.208 586.18C289.625 588.114 293.251 589.565 296.394 589.685C311.139 590.048 325.764 589.927 340.509 589.806C362.628 589.685 388.372 599.959 383.537 557.173C379.186 519.101 386.438 479.336 369.759 443.198C363.474 429.54 350.783 424.343 332.411 433.166C304.975 446.461 273.792 455.042 245.147 438.363C237.775 434.012 233.786 429.177 225.326 432.32C230.281 417.333 235.222 402.117 240.177 387.13C241.023 386.888 241.895 386.753 242.741 386.511C263.771 388.203 285.629 382.493 305.571 393.976C310.406 394.701 315.274 395.559 320.108 396.284C323.371 401.844 386.47 403.709 392.414 398.626C397.249 395.363 402.287 391.866 407.121 388.603C418.482 457.133 415.687 526.836 419.313 595.85C398.041 602.255 378.219 611.683 361.298 626.428C340.993 625.582 320.204 626.791 300.382 623.286C272.1 618.33 250.345 630.538 228.468 644.437C219.403 650.118 209.492 651.568 199.944 648.788C167.19 639.482 137.336 659.424 139.028 692.541C140.237 714.78 137.336 727.954 110.746 720.461C105.307 718.89 98.6596 721.428 92.6164 722.032C91.166 722.999 89.5948 724.087 88.1444 725.295Z" fill="#797AAA" stroke-width="1.34889" stroke="#797AAA"/>
<path d="M92.7403 721.915C98.7835 721.311 105.431 718.773 110.87 720.344C137.581 727.838 140.361 714.784 139.152 692.424C137.46 659.307 167.193 639.365 200.068 648.671C209.616 651.451 219.527 650.001 228.592 644.32C250.468 630.542 272.224 618.213 300.506 623.169C320.328 626.674 341.117 625.465 361.422 626.311C349.336 635.014 337.128 643.837 325.042 652.539C320.812 655.077 316.702 657.615 312.472 660.033C301.352 659.791 301.473 674.295 296.518 675.503C271.378 681.909 261.105 705.599 242.129 718.652C240.195 715.147 238.865 714.18 234.273 716.718C227.142 720.586 218.923 724.091 210.946 724.453C174.445 726.025 138.064 726.508 101.563 726.871C98.5418 726.992 95.641 723.607 92.7403 721.915Z" fill="#A29EA1" stroke-width="1.34889" stroke="#A29EA1"/>
<path d="M498.602 502.543C531.477 505.202 560.243 494.566 585.141 473.656C592.514 467.492 600.491 469.789 608.468 468.822C617.412 467.734 628.41 473.656 634.575 461.933C636.146 475.469 640.98 489.611 638.321 502.18C635.662 514.388 620.433 508.103 611.006 509.674C593.964 512.575 582.361 521.761 571.242 534.21C564.473 541.703 553.475 545.087 545.377 551.372C515.765 574.457 480.714 551.735 449.894 563.459C465.969 543.154 482.285 522.848 498.602 502.543Z" fill="#F8B570"/>
<path d="M92.7394 721.915C95.7085 723.67 98.6479 726.973 101.549 726.852C138.05 726.369 174.551 725.895 210.931 724.444C218.908 724.082 227.104 720.615 234.235 716.626C238.828 714.088 240.339 715.075 242.273 718.58C241.548 719.547 240.798 720.585 240.073 721.552C228.712 734.001 217.472 746.571 206.11 759.02C202.726 763.613 198.617 767.722 196.32 772.799C189.673 787.544 188.585 770.019 183.025 770.261C136.251 772.557 89.2649 772.636 42.4903 773.362C57.7193 757.287 72.8736 741.239 88.1025 725.285C89.5529 724.076 91.1681 723.003 92.7394 721.915Z" fill="#B59259" stroke-width="1.34889" stroke="#B59259"/>
<path d="M206.107 758.899C217.469 746.45 228.709 733.88 240.07 721.431C256.266 729.771 268.473 715.146 283.098 713.696C296.514 712.366 314.523 714.3 310.534 690.127C308.963 680.458 311.622 670.185 312.347 660.153C316.577 657.615 320.687 655.077 324.917 652.659C324.796 664.746 322.862 677.195 324.675 689.04C328.906 716.959 305.579 729.046 289.987 738.231C270.528 749.714 273.791 763.975 271.858 779.204C271.253 783.918 271.858 788.874 271.737 793.708C271.616 799.026 271.132 804.344 270.891 809.541C267.023 803.136 263.397 796.488 259.167 790.324C254.332 783.193 250.102 772.194 243.696 770.623C233.906 768.206 223.874 757.691 212.15 766.997C210.458 768.327 207.679 775.82 203.932 769.173C202.965 766.755 205.382 762.404 206.107 758.899Z" fill="#F8B570"/>
<path d="M240.193 387.12C235.238 402.107 230.403 417.215 225.448 432.202C224.843 432.323 224.118 432.444 223.514 432.686C149.424 441.267 75.2133 437.641 1.00251 434.982C-3.95294 409.721 4.74931 401.261 31.7021 403.074C73.642 405.975 116.186 406.458 157.28 395.822C166.587 393.405 180.849 392.8 185.2 379.264C185.321 378.901 186.288 378.659 186.288 378.659C200.912 403.074 222.426 383.736 240.193 387.12Z" fill="#A29EA1" stroke-width="1.34889" stroke="#A29EA1"/>
<path d="M392.483 398.6C387.285 403.918 323.469 401.863 320.085 396.304C344.258 397.029 368.431 397.875 392.483 398.6Z" fill="#7D89C1" stroke-width="1.34889" stroke="#7D89C1"/>
<path d="M320.079 396.306C315.245 395.581 310.41 394.735 305.575 394.01C310.41 394.735 315.245 395.581 320.079 396.306Z" fill="#7D89C1"/>
</g>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,5 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg x="94.969" y="72.212" width="840" height="809.053" viewBox="0 0 840 809.053" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M838.697 289.781C839.129 290.058 839.092 290.701 838.633 290.929C835.039 292.719 831.485 294.6 827.806 296.232C799.555 308.776 772.522 323.383 749.552 344.372C741.699 351.546 734.598 359.69 727.95 368.025C721.643 375.923 719.536 385.597 719.632 395.64C719.651 397.39 719.936 399.14 720.232 401.108C720.435 402.455 721.585 403.47 722.947 403.47H754.934C756.426 403.47 757.641 404.683 757.574 406.173C757.007 418.72 752.651 429.514 745.064 438.533C734.995 450.489 730.891 464.7 726.144 478.966C719.221 499.763 718.756 521.203 720.398 542.478C722.108 564.655 713.899 582.337 700.656 598.83C690.259 611.784 680.19 625.011 669.847 638.019C659.245 651.356 655.291 666.414 656.426 683.344C657.89 705.043 662.979 725.963 667.727 747.088C671.598 764.333 672.871 782.151 675.21 799.723C675.317 800.514 675.272 801.319 675.175 802.406C675.006 804.292 672.993 805.496 671.237 804.786C667.425 803.245 663.961 801.983 661.064 799.915C647.862 790.5 639.736 777.082 633.087 762.666C629.837 755.612 626.686 748.51 623.528 741.499C623.236 740.852 622.7 740.346 622.039 740.087C612.637 736.391 610.069 734.129 607.43 727.667C607.013 726.646 606.042 725.945 604.94 725.931C590.847 725.748 576.824 727.122 563.328 731.798C539.442 740.065 515.514 748.25 491.969 757.391C465.73 767.585 438.573 769.648 411.239 766.355C385.068 763.199 359.197 757.514 333.176 753.101C324.448 751.625 315.61 750.75 306.841 749.425C304.666 749.097 302.504 748.25 300.507 747.28C284.746 739.6 269.178 731.511 253.267 724.159C236.015 716.193 222.69 721.877 216.301 739.559C213.675 746.829 212.115 754.467 209.57 761.764C205.685 772.9 201.622 784.009 197.148 794.914C193.962 802.692 187.887 806.658 178.915 807.795C177.278 808.003 175.934 806.595 176.072 804.951C176.687 797.655 177.055 790.615 178.241 783.722C180.676 769.607 184.781 755.724 186.272 741.527C187.749 727.343 187.024 712.914 186.86 698.607C186.833 696.53 184.917 694.125 183.289 692.458C171.62 680.597 159.95 668.737 147.993 657.177C135.338 644.933 123.039 632.28 114.584 616.812C106.896 602.751 94.8841 600.743 81.5727 603.339C60.1486 607.52 38.9844 612.959 17.7107 617.905C12.3941 619.143 7.11322 620.525 1.82357 621.849C1.16721 622.013 0.484001 621.707 0.168963 621.108C-0.12378 620.551 -0.0300104 619.87 0.401242 619.413C2.93975 616.72 5.31065 614.184 8.16154 611.811C24.2638 598.42 40.7355 585.452 56.6463 571.843C66.1408 563.712 74.9923 554.803 83.7753 545.894C85.5401 544.104 86.6483 540.674 86.4704 538.105C82.0652 474.457 96.8679 414.73 123.327 357.408C134.518 333.153 146.142 309.094 157.878 284.322C158.345 283.335 159.369 282.751 160.459 282.687C170.461 282.106 171.998 271.978 175.957 264.599C178.669 259.543 180.643 254.095 182.891 248.386C183.584 246.625 185.831 246.144 187.204 247.447C188.931 249.086 190.564 250.638 192.338 252.327C193.394 253.332 195.059 253.317 196.13 252.327C199.595 249.128 203.201 246.202 205.753 242.545C215.658 228.389 225.399 214.096 234.415 199.379C238.574 192.588 243.198 188.325 251.16 186.097C277.787 178.649 304.16 170.335 330.424 162.392C331.068 162.197 331.64 161.784 332.011 161.223C332.138 161.03 332.228 160.859 332.246 160.682C334.189 145.022 335.461 143.71 351.303 141.114C368.172 138.354 378.802 129.336 381.647 112.282C384.479 95.2703 385.314 77.9165 386.969 60.6994C388.351 46.2015 389.377 31.6626 391.087 17.2057C392.145 8.24311 394.608 3.97571 400.598 0.355299C401.894 -0.42824 403.557 0.142784 404.243 1.49332C407.282 7.4772 410.711 13.3289 412.744 19.6243C418.257 36.7184 422.635 54.1678 427.998 71.3166C432.608 86.0468 437.465 100.736 443.047 115.125C450.517 134.405 465.798 142.576 485.854 142.918C493.611 143.055 501.368 143.328 509.111 142.986C518.592 142.576 527.403 144.653 534.996 150.092C555.53 164.822 576.257 179.32 596.012 195.048C610.924 206.928 624.546 220.435 639.396 233.918C640.083 234.542 641.043 234.751 641.954 234.576C649.443 233.136 656.045 237.059 662.73 241.666C663.822 242.419 664.193 243.86 663.612 245.053C661.429 249.534 659.269 253.957 656.99 258.64C656.297 260.065 656.98 261.773 658.432 262.408C673.511 269.004 684.03 280.872 695.731 291.217C699.822 294.825 704.213 298.118 708.55 301.438C716.718 307.697 725.761 308.995 735.693 306.494C754.901 301.657 774.095 296.751 793.44 292.556C808.443 289.301 822.647 287.5 838.432 289.683C838.526 289.696 838.617 289.73 838.697 289.781ZM432.82 707.072C432.825 707.068 432.831 707.065 432.837 707.065C448.553 707.065 464.269 707.243 479.985 706.956C484.185 706.874 488.809 706.259 492.489 704.401C512.19 694.467 532.013 684.724 551.07 673.656C570.73 662.246 572.385 656.575 566.557 634.344C561.304 614.325 551.604 596.22 541.343 578.511C529.209 557.55 519.741 536.083 519.139 511.268C518.825 498.246 515.076 485.757 508.879 474.074C503.393 463.73 494.897 458.838 483.446 458.688C451.775 458.278 420.761 462.063 390.595 472.27C312.614 498.684 265.114 562.824 263.801 642.911C263.486 662.232 271.859 675.35 288.755 683.002C300.534 688.345 312.806 692.786 325.173 696.585C360.261 707.365 396.485 708.363 432.804 707.079C432.81 707.079 432.81Line truncated
</svg>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

+50
View File
@@ -0,0 +1,50 @@
{
"fill" : {
"linear-gradient" : [
"srgb:0.24706,0.24706,0.27451,1.00000",
"srgb:0.03529,0.03529,0.04314,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.5,
"y" : 1
}
}
},
"groups" : [
{
"layers" : [
{
"image-name-specializations" : [
{
"value" : "01-mark.svg"
},
{
"appearance" : "tinted",
"value" : "02-mark-mono.svg"
}
],
"name" : "01-mark"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@@ -1,290 +0,0 @@
{
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "24x24",
"subtype" : "38mm"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "27.5x27.5",
"subtype" : "42mm"
},
{
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "33x33",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "40x40",
"subtype" : "38mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "44x44",
"subtype" : "40mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "46x46",
"subtype" : "41mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "50x50",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "86x86",
"subtype" : "38mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "98x98",
"subtype" : "42mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "108x108",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "car",
"scale" : "2x",
"size" : "60"
},
{
"idiom" : "car",
"scale" : "3x",
"size" : "60"
},
{
"idiom" : "watch",
"role" : "longLook",
"scale" : "2x",
"size" : "44x44",
"subtype" : "42mm"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,68 +0,0 @@
{
"images" : [
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,68 +0,0 @@
{
"images": [
{
"idiom": "mac",
"size": "16x16",
"scale": "1x",
"filename": "16.png"
},
{
"idiom": "mac",
"size": "16x16",
"scale": "2x",
"filename": "16@2x.png"
},
{
"idiom": "mac",
"size": "32x32",
"scale": "1x",
"filename": "32.png"
},
{
"idiom": "mac",
"size": "32x32",
"scale": "2x",
"filename": "32@2x.png"
},
{
"idiom": "mac",
"size": "128x128",
"scale": "1x",
"filename": "128.png"
},
{
"idiom": "mac",
"size": "128x128",
"scale": "2x",
"filename": "128@2x.png"
},
{
"idiom": "mac",
"size": "256x256",
"scale": "1x",
"filename": "256.png"
},
{
"idiom": "mac",
"size": "256x256",
"scale": "2x",
"filename": "256@2x.png"
},
{
"idiom": "mac",
"size": "512x512",
"scale": "1x",
"filename": "512.png"
},
{
"idiom": "mac",
"size": "512x512",
"scale": "2x",
"filename": "512@2x.png"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
+2
View File
@@ -44,6 +44,8 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIcons</key>
+51
View File
@@ -26,6 +26,7 @@ set(CMAKE_AUTOUIC ON)
qt6_add_resources(TEST_QRC
../server_scripts/serverScripts.qrc
../resources/countries.qrc
)
set(_main_cpp "${CMAKE_CURRENT_SOURCE_DIR}/../main.cpp")
@@ -160,6 +161,42 @@ if(NOT ANDROID)
test_common
)
add_executable(test_xray_server_inbound
testXrayServerInbound.cpp
)
target_link_libraries(test_xray_server_inbound PRIVATE
Qt6::Test
test_common
)
add_executable(test_xray_generator_matrix
testXrayGeneratorMatrix.cpp
)
target_link_libraries(test_xray_generator_matrix PRIVATE
Qt6::Test
test_common
)
add_executable(test_xray_config_model
testXrayConfigModel.cpp
)
target_link_libraries(test_xray_config_model PRIVATE
Qt6::Test
test_common
)
add_executable(test_xray_core_accepts
testXrayCoreAccepts.cpp
)
target_link_libraries(test_xray_core_accepts PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_language_model_and_controller
testUiLanguageModelAndController.cpp
)
@@ -196,6 +233,15 @@ if(NOT ANDROID)
test_common
)
add_executable(test_favorite_locations_restore
testFavoriteLocationsRestore.cpp
)
target_link_libraries(test_favorite_locations_restore PRIVATE
Qt6::Test
test_common
)
add_executable(test_backup_export_import
testBackupExportImport.cpp
)
@@ -432,12 +478,17 @@ if(NOT ANDROID)
add_test(NAME SettingsSignalsTest COMMAND test_settings_signals)
add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller)
add_test(NAME XraySerializationTest COMMAND test_xray_serialization)
add_test(NAME XrayServerInboundTest COMMAND test_xray_server_inbound)
add_test(NAME XrayGeneratorMatrixTest COMMAND test_xray_generator_matrix)
add_test(NAME XrayConfigModelTest COMMAND test_xray_config_model)
add_test(NAME XrayCoreAcceptsTest COMMAND test_xray_core_accepts)
add_test(NAME ConfigFormatVersionTest COMMAND test_config_format_version)
add_test(NAME UiLanguageModelAndControllerTest COMMAND test_ui_language_model_and_controller)
add_test(NAME UiIpModelAndControllerTest COMMAND test_ui_ip_model_and_controller)
add_test(NAME UiAppSTModelAndControllerTest COMMAND test_ui_app_st_model_and_controller)
add_test(NAME UiAllowedDnsModelAndControllerTest COMMAND test_ui_allowed_dns_model_and_controller)
add_test(NAME BackupExportImportTest COMMAND test_backup_export_import)
add_test(NAME FavoriteLocationsRestoreTest COMMAND test_favorite_locations_restore)
add_test(NAME ConnectionTest COMMAND test_connection)
add_test(NAME OpenSSLLibraryTest COMMAND test_openssl_library)
add_test(NAME LogsControllerTest COMMAND test_logs_controller)
@@ -29,4 +29,15 @@ add_test(NAME AdminSelfHostedExportTest COMMAND test_admin_self_hosted_export)
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
add_test(NAME MultipleExportsTest COMMAND test_multiple_exports)
add_executable(test_xray_vps_matrix
testXrayVpsMatrix.cpp
)
target_link_libraries(test_xray_vps_matrix PRIVATE
Qt6::Test
test_common
)
add_test(NAME XrayVpsMatrixTest COMMAND test_xray_vps_matrix)
amnezia_copy_runtime_dlls()
@@ -0,0 +1,462 @@
#include <QApplication>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QHostInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkProxy>
#include <QNetworkReply>
#include <QProcess>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTemporaryDir>
#include <QTest>
#include <QTimer>
#include <QUuid>
#include <functional>
#include <libssh/libssh.h>
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/serversController.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/utils/commonStructs.h"
#include "core/utils/containerEnum.h"
#include "core/utils/errorCodes.h"
#include "core/utils/protocolEnum.h"
#include "secureQSettings.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
using namespace amnezia;
using namespace amnezia::test;
namespace
{
enum class Expect {
Connects,
Refused,
Explore,
};
struct VpsCase
{
QString name;
std::function<void(XrayConfigModel &)> edit;
Expect expect = Expect::Connects;
bool deviceOnly = false;
};
void base(XrayConfigModel &m, const QString &transport, const QString &security, const QString &flow = QString(),
const QString &mode = QStringLiteral("Stream-one"))
{
m.setTransport(transport);
m.setSecurity(security);
m.setFlow(flow);
m.setSni(QStringLiteral("www.googletagmanager.com"));
if (transport == QLatin1String("xhttp")) {
m.setXhttpMode(mode);
m.setXhttpPath(QStringLiteral("/"));
m.setXhttpUplinkDataPlacement(QStringLiteral("Body"));
m.setXhttpUplinkMethod(QStringLiteral("POST"));
m.setXPaddingObfsMode(true);
m.setXmuxEnabled(true);
m.setXmuxMaxConcurrencyMin(QStringLiteral("0"));
m.setXmuxMaxConcurrencyMax(QStringLiteral("0"));
m.setXmuxMaxConnectionsMin(QStringLiteral("0"));
m.setXmuxMaxConnectionsMax(QStringLiteral("0"));
}
}
QList<VpsCase> vpsCases()
{
QList<VpsCase> cases;
const QList<QPair<QString, QString>> xhttpCombos = {
{ "Auto", "none" }, { "Auto", "tls" }, { "Auto", "reality" },
{ "Packet-up", "none" }, { "Packet-up", "tls" },
{ "Stream-up", "none" }, { "Stream-up", "tls" }, { "Stream-up", "reality" },
{ "Stream-one", "none" }, { "Stream-one", "tls" }, { "Stream-one", "reality" },
};
for (const auto &combo : xhttpCombos) {
const QString mode = combo.first;
const QString security = combo.second;
cases.append({ QStringLiteral("xhttp/%1/%2").arg(mode, security),
[mode, security](XrayConfigModel &m) { base(m, "xhttp", security, QString(), mode); } });
}
const QList<QPair<QString, QString>> rawCombos = {
{ "none", "" },
{ "tls", "" }, { "tls", "xtls-rprx-vision" }, { "tls", "xtls-rprx-vision-udp443" },
{ "reality", "" }, { "reality", "xtls-rprx-vision" }, { "reality", "xtls-rprx-vision-udp443" },
};
for (const auto &combo : rawCombos) {
const QString security = combo.first;
const QString flow = combo.second;
cases.append({ QStringLiteral("raw/%1/%2").arg(security, flow.isEmpty() ? QStringLiteral("noflow") : flow),
[security, flow](XrayConfigModel &m) { base(m, "raw", security, flow); } });
}
cases.append({ "mkcp/none", [](XrayConfigModel &m) { base(m, "mkcp", "none"); } });
cases.append({ "mkcp/tls", [](XrayConfigModel &m) { base(m, "mkcp", "tls"); } });
cases.append({ "xhttp/packet-up/tls/cookie+GET", [](XrayConfigModel &m) {
base(m, "xhttp", "tls", QString(), "Packet-up");
m.setXhttpUplinkDataPlacement(QStringLiteral("Cookie"));
m.setXhttpUplinkMethod(QStringLiteral("GET"));
} });
cases.append({ "xhttp/reality/xPadding obfs off", [](XrayConfigModel &m) {
base(m, "xhttp", "reality");
m.setXPaddingObfsMode(false);
} });
cases.append({ "xhttp/reality/xPadding 200-400", [](XrayConfigModel &m) {
base(m, "xhttp", "reality");
m.setXPaddingBytesMin(QStringLiteral("200"));
m.setXPaddingBytesMax(QStringLiteral("400"));
} });
cases.append({ "xhttp/reality/path without slash", [](XrayConfigModel &m) {
base(m, "xhttp", "reality");
m.setXhttpPath(QStringLiteral("amnezia"));
} });
cases.append({ "device-only/xhttp/xmux concurrency 16-32", [](XrayConfigModel &m) {
m.setXmuxEnabled(true);
m.setXmuxMaxConcurrencyMin(QStringLiteral("16"));
m.setXmuxMaxConcurrencyMax(QStringLiteral("32"));
}, Expect::Connects, true });
cases.append({ "device-only/xhttp/fingerprint firefox + PUT", [](XrayConfigModel &m) {
m.setFingerprint(QStringLiteral("firefox"));
m.setXhttpUplinkMethod(QStringLiteral("PUT"));
}, Expect::Connects, true });
cases.append({ "refused/xmux concurrency and connections", [](XrayConfigModel &m) {
base(m, "xhttp", "tls");
m.setXmuxMaxConcurrencyMax(QStringLiteral("1"));
m.setXmuxMaxConnectionsMax(QStringLiteral("1"));
}, Expect::Refused });
cases.append({ "refused/wildcard sni", [](XrayConfigModel &m) {
base(m, "raw", "reality");
m.setSni(QStringLiteral("*.example.com"));
}, Expect::Refused });
cases.append({ "xhttp/packet-up/reality",
[](XrayConfigModel &m) { base(m, "xhttp", "reality", QString(), "Packet-up"); } });
return cases;
}
const QList<VpsCase> &cases()
{
static const QList<VpsCase> all = vpsCases();
return all;
}
int freeLocalPort()
{
QTcpServer server;
server.listen(QHostAddress::LocalHost, 0);
const int port = server.serverPort();
server.close();
return port;
}
}
class TestXrayVpsMatrix : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController = nullptr;
SecureQSettings *m_settings = nullptr;
ServerCredentials m_credentials;
QString m_serverId;
QString m_xray;
QStringList m_expectedIps;
XrayServerConfig m_originalSettings;
QTemporaryDir m_dir;
bool m_ready = false;
ContainerConfig storedContainer() const
{
return m_coreController->m_serversController->getContainerConfig(m_serverId, DockerContainer::Xray);
}
XrayProtocolConfig storedXray() const
{
const ContainerConfig container = storedContainer();
const auto *xray = container.getXrayProtocolConfig();
return xray ? *xray : XrayProtocolConfig {};
}
ErrorCode save(XrayConfigModel &model)
{
const ContainerConfig oldConfig = storedContainer();
ContainerConfig newConfig = oldConfig;
newConfig.protocolConfig = model.getProtocolConfig();
return m_coreController->m_installController->updateServerConfig(m_serverId, DockerContainer::Xray, oldConfig,
newConfig, true);
}
QString egressIpThroughTunnel(QString &error)
{
const XrayProtocolConfig xray = storedXray();
if (!xray.hasClientConfig()) {
error = QStringLiteral("no client config stored after save");
return {};
}
QJsonObject native =
QJsonDocument::fromJson(sanitizeNativeConfig(xray.clientConfig->nativeConfig).toUtf8()).object();
const int socksPort = freeLocalPort();
QJsonArray inbounds = native.value("inbounds").toArray();
QJsonObject socks = inbounds.first().toObject();
socks["listen"] = QStringLiteral("127.0.0.1");
socks["port"] = socksPort;
inbounds[0] = socks;
native["inbounds"] = inbounds;
native["log"] = QJsonObject { { "loglevel", "warning" } };
const QString path = m_dir.filePath(QStringLiteral("client-%1.json").arg(QUuid::createUuid().toString(QUuid::Id128)));
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
error = QStringLiteral("cannot write ") + path;
return {};
}
file.write(QJsonDocument(native).toJson());
file.close();
QProcess client;
client.setProcessChannelMode(QProcess::MergedChannels);
client.start(m_xray, { QStringLiteral("run"), QStringLiteral("-c"), path });
if (!client.waitForStarted(10000)) {
error = QStringLiteral("xray client did not start");
return {};
}
QElapsedTimer listening;
listening.start();
bool up = false;
while (!up && listening.elapsed() < 10000 && client.state() == QProcess::Running) {
QTcpSocket probe;
probe.connectToHost(QHostAddress::LocalHost, socksPort);
up = probe.waitForConnected(300);
if (!up)
QTest::qWait(200);
}
if (!up) {
client.kill();
client.waitForFinished(3000);
error = QStringLiteral("local socks never came up: ") + QString::fromUtf8(client.readAll());
return {};
}
QString ip;
for (int attempt = 0; attempt < 3 && ip.isEmpty(); ++attempt) {
QNetworkAccessManager manager;
manager.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, QStringLiteral("127.0.0.1"), socksPort));
QNetworkRequest request(QUrl(QStringLiteral("https://api.ipify.org")));
request.setTransferTimeout(20000);
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QTimer::singleShot(25000, &loop, &QEventLoop::quit);
loop.exec();
if (reply->isFinished() && reply->error() == QNetworkReply::NoError)
ip = QString::fromUtf8(reply->readAll()).trimmed();
else
error = QStringLiteral("request through the tunnel failed: ") + reply->errorString();
reply->deleteLater();
}
client.kill();
client.waitForFinished(3000);
if (ip.isEmpty())
error += QStringLiteral("\nxray client output: ") + QString::fromUtf8(client.readAll());
return ip;
}
private slots:
void initTestCase()
{
m_credentials.hostName = getEnvValue("SELF_HOSTED_SERVER_HOST_NAME");
m_credentials.userName = getEnvValue("SELF_HOSTED_SERVER_USER_NAME");
m_credentials.secretData = getEnvValue("SELF_HOSTED_SERVER_PASSWORD");
m_credentials.port = getEnvValue("SELF_HOSTED_SERVER_SSH_PORT").toInt();
if (m_credentials.port <= 0)
m_credentials.port = 22;
m_xray = getEnvValue("XRAY_BIN");
for (const char *key : { "SELF_HOSTED_SERVER_HOST_NAME", "SELF_HOSTED_SERVER_USER_NAME",
"SELF_HOSTED_SERVER_PASSWORD", "SELF_HOSTED_SERVER_SSH_PORT", "XRAY_BIN" })
logEnvValueState(QString::fromLatin1(key));
if (m_credentials.hostName.isEmpty() || m_credentials.userName.isEmpty() || m_credentials.secretData.isEmpty())
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
if (m_xray.isEmpty() || !QFileInfo(m_xray).isExecutable())
QSKIP("Set XRAY_BIN to the xray binary used as the client");
QVERIFY(m_dir.isValid());
const QString expected = getEnvValue("XRAY_E2E_EXPECT_IP");
if (!expected.isEmpty()) {
m_expectedIps << expected;
} else {
for (const QHostAddress &a : QHostInfo::fromName(m_credentials.hostName).addresses())
m_expectedIps << a.toString();
}
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
ssh_init();
int port = getEnvValue("XRAY_E2E_PORT").toInt();
if (port <= 0)
port = 8443;
bool wasInstalled = false;
const ErrorCode installError = m_coreController->m_installController->installServer(
m_credentials, DockerContainer::Xray, port, TransportProto::Tcp, wasInstalled);
QVERIFY2(installError == ErrorCode::NoError,
qPrintable(QStringLiteral("installServer(Xray) failed, error %1").arg(static_cast<int>(installError))));
m_serverId = m_coreController->m_serversRepository->serverIdAt(
m_coreController->m_serversRepository->serversCount() - 1);
qInfo().noquote() << "Xray on the VPS:" << (wasInstalled ? "installed by this run" : "already there, reused");
m_originalSettings = storedXray().serverConfig;
m_ready = true;
}
void cleanupTestCase()
{
if (m_ready) {
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, storedXray());
model.applyServerConfig(m_originalSettings);
const ErrorCode restoreError = save(model);
if (restoreError != ErrorCode::NoError)
qWarning().noquote() << "Could not restore the original Xray settings, error"
<< static_cast<int>(restoreError);
else
qInfo().noquote() << "Original Xray settings restored";
}
if (m_coreController) {
ssh_finalize();
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
}
void matrix_data()
{
QTest::addColumn<int>("index");
for (int i = 0; i < cases().size(); ++i)
QTest::newRow(qPrintable(cases().at(i).name)) << i;
}
void matrix()
{
QFETCH(int, index);
const VpsCase &c = cases().at(index);
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, storedXray());
c.edit(model);
const QStringList errors = model.validationErrors();
if (c.expect == Expect::Refused) {
QVERIFY2(!errors.isEmpty(), "validation must refuse this before anything reaches the server");
return;
}
if (c.expect == Expect::Connects)
QVERIFY2(errors.isEmpty(), qPrintable(errors.join("; ")));
if (c.deviceOnly)
QVERIFY2(!model.pendingChangeTouchesServer(QString()), "a device-only edit must not count as a server change");
QElapsedTimer timer;
timer.start();
const ErrorCode saveError = save(model);
const qint64 saveMs = timer.elapsed();
if (c.expect == Expect::Explore) {
QString why;
const QString ip = saveError == ErrorCode::NoError ? egressIpThroughTunnel(why) : QString();
const QString result = QStringLiteral("EXPLORE %1: save error %2 %3")
.arg(c.name)
.arg(static_cast<int>(saveError))
.arg(ip.isEmpty() ? QStringLiteral("-> NO TRAFFIC: ") + why
: QStringLiteral("-> traffic flows, egress ") + ip);
QSKIP(qPrintable(result));
}
QVERIFY2(saveError == ErrorCode::NoError,
qPrintable(QStringLiteral("save failed, error %1").arg(static_cast<int>(saveError))));
qInfo().noquote() << c.name << "saved in" << saveMs << "ms";
QString why;
const QString ip = egressIpThroughTunnel(why);
QVERIFY2(!ip.isEmpty(), qPrintable(why));
QVERIFY2(m_expectedIps.isEmpty() || m_expectedIps.contains(ip),
qPrintable(QStringLiteral("traffic left through %1, expected %2")
.arg(ip, m_expectedIps.join(QStringLiteral(", ")))));
}
void portChangeRecreatesContainer()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, storedXray());
base(model, "raw", "reality", "xtls-rprx-vision");
QCOMPARE(save(model), ErrorCode::NoError);
const QString originalPort = storedXray().serverConfig.port;
const QString newPort = QString::number(originalPort.toInt() == 8443 ? 8444 : 8443);
model.updateModel(DockerContainer::Xray, storedXray());
QVERIFY(model.pendingChangeRequiresReinstall(newPort));
model.setPort(newPort);
QCOMPARE(save(model), ErrorCode::NoError);
QCOMPARE(storedXray().serverConfig.port, newPort);
QString why;
QVERIFY2(!egressIpThroughTunnel(why).isEmpty(), qPrintable(why));
model.updateModel(DockerContainer::Xray, storedXray());
model.setPort(originalPort);
QCOMPARE(save(model), ErrorCode::NoError);
}
void rescanKeepsDeviceSettings()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, storedXray());
base(model, "raw", "reality", "xtls-rprx-vision-udp443");
model.setFingerprint(QStringLiteral("firefox"));
QCOMPARE(save(model), ErrorCode::NoError);
QCOMPARE(m_coreController->m_installController->scanServerForInstalledContainers(m_serverId), ErrorCode::NoError);
const XrayServerConfig after = storedXray().serverConfig;
QCOMPARE(after.flow, QStringLiteral("xtls-rprx-vision-udp443"));
QCOMPARE(after.fingerprint, QStringLiteral("firefox"));
QString why;
QVERIFY2(!egressIpThroughTunnel(why).isEmpty(), qPrintable(why));
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
TestXrayVpsMatrix test;
QStringList args = app.arguments();
if (!args.contains(QStringLiteral("-maxwarnings")))
args << QStringLiteral("-maxwarnings") << QStringLiteral("0");
return QTest::qExec(&test, args);
}
#include "testXrayVpsMatrix.moc"
@@ -0,0 +1,489 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include <QThreadPool>
#include <QUuid>
#include "utils/testCoreController.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
namespace
{
const QString favoritesKey = QStringLiteral("Conf/favoriteLocations");
const QString serverA = QStringLiteral("aaaaaaaa-0000-0000-0000-000000000001");
const QString serverB = QStringLiteral("bbbbbbbb-0000-0000-0000-000000000002");
const QString serverSelfHosted = QStringLiteral("cccccccc-0000-0000-0000-000000000003");
const QStringList locationCodes = { "AT", "BE", "CA", "CH", "CZ", "DE", "DK", "ES", "FI", "FR", "GB",
"HU", "IE", "IT", "JP", "NL", "NO", "PL", "PT", "SE", "US" };
QJsonArray toJsonArray(const QStringList &codes)
{
QJsonArray array;
for (const QString &code : codes) {
array.append(code);
}
return array;
}
QJsonArray countriesJson(const QStringList &codes)
{
QJsonArray countries;
for (const QString &code : codes) {
QJsonObject country;
country[apiDefs::key::serverCountryCode] = code;
country[apiDefs::key::serverCountryName] = QStringLiteral("Country ") + code;
countries.append(country);
}
return countries;
}
QJsonObject apiServerJson(const QString &serverId, const QString &hostName)
{
ApiV2ServerConfig api;
api.hostName = hostName;
api.description = hostName;
api.configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
api.apiConfig.serviceType = QStringLiteral("amnezia-premium");
api.apiConfig.availableCountries = countriesJson(locationCodes);
QJsonObject json = api.toJson();
if (!serverId.isEmpty()) {
json[configKey::storageServerId] = serverId;
}
return json;
}
QJsonObject selfHostedServerJson(const QString &serverId, const QString &hostName)
{
QJsonObject json;
json[configKey::hostName] = hostName;
json[configKey::userName] = QStringLiteral("root");
json[configKey::password] = QStringLiteral("secret");
json[configKey::port] = 22;
json[configKey::description] = hostName;
json[configKey::storageServerId] = serverId;
return json;
}
QByteArray backupWith(const QString &key, const QJsonValue &value)
{
QJsonObject backup;
backup[key] = value;
return QJsonDocument(backup).toJson();
}
QJsonObject perServer(const QString &serverId, const QStringList &codes)
{
QJsonObject favorites;
favorites[serverId] = toJsonArray(codes);
return favorites;
}
bool hasUseCase(const ApiCountryListModel *model, const QString &id)
{
for (const QVariant &entry : model->useCases()) {
if (entry.toMap().value(QStringLiteral("useCaseId")).toString() == id) {
return true;
}
}
return false;
}
} // namespace
class TestFavoriteLocationsRestore : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController = nullptr;
SecureQSettings *m_settings = nullptr;
ApiCountryModel *countryModel() const { return m_coreController->m_apiCountryModel; }
ApiCountryListModel *picker() const { return m_coreController->m_apiCountryListModel; }
ApiCountryListModel *nativeConfigs() const { return m_coreController->m_apiConfigsCountryListModel; }
QStringList storedFavorites(const QString &serverId) const
{
return m_coreController->m_appSettingsRepository->favoriteLocations(serverId);
}
void storeServers(const QJsonArray &servers)
{
m_settings->setValue(QStringLiteral("Servers/serversList"), QJsonDocument(servers).toJson());
m_coreController->m_serversRepository->invalidateCache();
}
void showServer(const QString &serverId, const QStringList &codes = locationCodes)
{
countryModel()->setServerId(serverId);
countryModel()->updateModel(countriesJson(codes), QString(), QString());
}
void restore(const QByteArray &data)
{
QSignalSpy restoredSpy(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished);
m_coreController->m_settingsUiController->restoreAppConfigFromData(data);
QCOMPARE(restoredSpy.count(), 1);
}
void favoriteAll(const QStringList &codes)
{
for (const QString &code : codes) {
QVERIFY(picker()->toggleFavorite(code));
}
}
private slots:
void initTestCase()
{
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
// Restores with API servers start gateway requests that write settings when they finish
QThreadPool::globalInstance()->waitForDone();
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
// Goes through the repository so that settingsCleared reaches the models
m_coreController->m_appSettingsRepository->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
showServer(serverA);
QVERIFY(picker()->favorites().isEmpty());
QVERIFY(nativeConfigs()->favorites().isEmpty());
}
void testFavoritesArePerServer()
{
favoriteAll({ "US", "DE" });
showServer(serverB);
QVERIFY(picker()->favorites().isEmpty());
QVERIFY(nativeConfigs()->favorites().isEmpty());
favoriteAll({ "FR" });
showServer(serverA);
QCOMPARE(picker()->favorites(), QStringList({ "DE", "US" }));
QCOMPARE(nativeConfigs()->favorites(), QStringList({ "DE", "US" }));
QCOMPARE(storedFavorites(serverA), QStringList({ "DE", "US" }));
QCOMPARE(storedFavorites(serverB), QStringList({ "FR" }));
}
void testLimitIsPerServer()
{
favoriteAll(locationCodes.mid(0, picker()->favoritesLimit()));
QVERIFY(!picker()->toggleFavorite(locationCodes.last()));
showServer(serverB);
QVERIFY(picker()->toggleFavorite(locationCodes.last()));
}
void testToggleWithoutServerIsNotStored()
{
showServer(QString());
QVERIFY(picker()->toggleFavorite("US"));
QVERIFY(m_settings->value(favoritesKey).toMap().isEmpty());
showServer(serverA);
QVERIFY(picker()->favorites().isEmpty());
}
void testBackupContainsPerServerFavorites()
{
favoriteAll({ "US", "DE", "NL" });
showServer(serverB);
favoriteAll({ "FR" });
const QJsonObject backup = QJsonDocument::fromJson(m_coreController->m_settingsController->backupAppConfig()).object();
const QVariantMap favorites = backup.value(favoritesKey).toObject().toVariantMap();
QCOMPARE(favorites.value(serverA).toStringList(), QStringList({ "DE", "NL", "US" }));
QCOMPARE(favorites.value(serverB).toStringList(), QStringList({ "FR" }));
}
void testRestoreReloadsBothModels()
{
favoriteAll({ "FR" });
QSignalSpy pickerChangedSpy(picker(), &ApiCountryListModel::favoritesChanged);
QSignalSpy nativeChangedSpy(nativeConfigs(), &ApiCountryListModel::favoritesChanged);
restore(backupWith(favoritesKey, perServer(serverA, { "US", "DE" })));
const QStringList expected = { "DE", "US" };
QCOMPARE(storedFavorites(serverA), expected);
QCOMPARE(picker()->favorites(), expected);
QCOMPARE(nativeConfigs()->favorites(), expected);
QCOMPARE(pickerChangedSpy.count(), 0);
QCOMPARE(nativeChangedSpy.count(), 0);
}
void testRestoreOtherServerOnlyClearsCurrent()
{
favoriteAll({ "FR" });
restore(backupWith(favoritesKey, perServer(serverB, { "US" })));
QVERIFY(storedFavorites(serverA).isEmpty());
QVERIFY(picker()->favorites().isEmpty());
showServer(serverB);
QCOMPARE(picker()->favorites(), QStringList({ "US" }));
}
void testToggleAfterRestoreKeepsRestored()
{
favoriteAll({ "FR" });
restore(backupWith(favoritesKey, perServer(serverA, { "US", "DE" })));
QVERIFY(picker()->toggleFavorite("NL"));
const QStringList expected = { "DE", "NL", "US" };
QCOMPARE(storedFavorites(serverA), expected);
QCOMPARE(picker()->favorites(), expected);
QCOMPARE(nativeConfigs()->favorites(), expected);
QVERIFY(nativeConfigs()->toggleFavorite("US"));
QCOMPARE(storedFavorites(serverA), QStringList({ "DE", "NL" }));
QCOMPARE(picker()->favorites(), QStringList({ "DE", "NL" }));
}
void testRestoreWithoutKeyKeepsCurrent()
{
favoriteAll({ "FR" });
restore(backupWith(QStringLiteral("Conf/autoConnect"), false));
QCOMPARE(storedFavorites(serverA), QStringList({ "FR" }));
QCOMPARE(picker()->favorites(), QStringList({ "FR" }));
QCOMPARE(nativeConfigs()->favorites(), QStringList({ "FR" }));
}
void testRestoreValueShapes_data()
{
QTest::addColumn<QJsonValue>("value");
QTest::addColumn<QStringList>("expected");
QTest::newRow("empty map") << QJsonValue(QJsonObject()) << QStringList();
QTest::newRow("null") << QJsonValue(QJsonValue::Null) << QStringList();
QTest::newRow("number") << QJsonValue(42) << QStringList();
QTest::newRow("bool") << QJsonValue(true) << QStringList();
QTest::newRow("server: empty list") << QJsonValue(perServer(serverA, {})) << QStringList();
QTest::newRow("server: single string") << QJsonValue(QJsonObject { { serverA, "US" } }) << QStringList({ "US" });
QTest::newRow("server: duplicates and empty")
<< QJsonValue(perServer(serverA, { "US", "US", "", "DE" })) << QStringList({ "DE", "US" });
QTest::newRow("server: number") << QJsonValue(QJsonObject { { serverA, 42 } }) << QStringList();
QTest::newRow("server: object") << QJsonValue(QJsonObject { { serverA, QJsonObject() } }) << QStringList();
}
void testRestoreValueShapes()
{
QFETCH(QJsonValue, value);
QFETCH(QStringList, expected);
favoriteAll({ "FR" });
restore(backupWith(favoritesKey, value));
QCOMPARE(storedFavorites(serverA), expected);
QCOMPARE(picker()->favorites(), expected);
QCOMPARE(nativeConfigs()->favorites(), expected);
}
void testRestoreEmptyResetsFavoritesChip()
{
favoriteAll({ "FR" });
picker()->setActiveUseCaseId(QStringLiteral("favorites"));
QCOMPARE(picker()->activeUseCaseId(), QStringLiteral("favorites"));
restore(backupWith(favoritesKey, QJsonObject()));
QVERIFY(!hasUseCase(picker(), QStringLiteral("favorites")));
QCOMPARE(picker()->activeUseCaseId(), QStringLiteral("all"));
}
void testRestoreOverLimit()
{
const QStringList restored = locationCodes.mid(0, picker()->favoritesLimit() + 5);
QVERIFY(restored.size() < locationCodes.size());
restore(backupWith(favoritesKey, perServer(serverA, restored)));
QCOMPARE(picker()->favorites().size(), restored.size());
QSignalSpy limitSpy(picker(), &ApiCountryListModel::favoritesLimitExceeded);
QVERIFY(!picker()->toggleFavorite(locationCodes.last()));
QCOMPARE(limitSpy.count(), 1);
QVERIFY(picker()->toggleFavorite(restored.first()));
QCOMPARE(storedFavorites(serverA).size(), restored.size() - 1);
}
void testRestoreAtLimitOverSmallerSet()
{
// Real-world case: a backup holding exactly the limit restored over a smaller current set
const QStringList atLimit = { "ge", "hk", "id", "il", "in", "jp", "kr", "kz",
"my", "pe", "ph", "sg", "tr", "us-east", "us-west" };
QCOMPARE(atLimit.size(), picker()->favoritesLimit());
showServer(serverA, atLimit + QStringList { "de", "fr" });
for (const QString &code : atLimit.mid(0, 12)) {
QVERIFY(picker()->toggleFavorite(code));
}
picker()->setActiveUseCaseId(QStringLiteral("favorites"));
restore(backupWith(favoritesKey, perServer(serverA, atLimit)));
QStringList expected = atLimit;
expected.sort();
QCOMPARE(storedFavorites(serverA), expected);
QCOMPARE(picker()->favorites(), expected);
QCOMPARE(nativeConfigs()->favorites(), expected);
QCOMPARE(picker()->activeUseCaseId(), QStringLiteral("favorites"));
QSignalSpy limitSpy(picker(), &ApiCountryListModel::favoritesLimitExceeded);
QVERIFY(!picker()->toggleFavorite("de"));
QCOMPARE(limitSpy.count(), 1);
QCOMPARE(storedFavorites(serverA), expected);
}
void testMigrateLegacyList()
{
SecureAppSettingsRepository *repository = m_coreController->m_appSettingsRepository;
m_settings->setValue(favoritesKey, QStringList { "US", "", "DE", "US" });
repository->migrateLegacyFavoriteLocations({ serverA, serverB });
QCOMPARE(storedFavorites(serverA), QStringList({ "DE", "US" }));
QCOMPARE(storedFavorites(serverB), QStringList({ "DE", "US" }));
repository->setFavoriteLocations(serverB, { "FR" });
repository->migrateLegacyFavoriteLocations({ serverA, serverB });
QCOMPARE(storedFavorites(serverB), QStringList({ "FR" }));
m_settings->setValue(favoritesKey, QStringLiteral("US"));
repository->migrateLegacyFavoriteLocations({});
QVERIFY(m_settings->value(favoritesKey).toMap().isEmpty());
QCOMPARE(m_settings->value(favoritesKey).typeId(), int(QMetaType::QVariantMap));
}
void testRestoreLegacyListCopiesToApiServers()
{
storeServers({ apiServerJson(serverA, "a.example"), apiServerJson(serverB, "b.example"),
selfHostedServerJson(serverSelfHosted, "10.0.0.1") });
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 3);
restore(backupWith(favoritesKey, toJsonArray({ "US", "DE" })));
const QVariantMap stored = m_settings->value(favoritesKey).toMap();
QCOMPARE(stored.keys(), QStringList({ serverA, serverB }));
QCOMPARE(storedFavorites(serverA), QStringList({ "DE", "US" }));
QCOMPARE(storedFavorites(serverB), QStringList({ "DE", "US" }));
QCOMPARE(picker()->favorites(), QStringList({ "DE", "US" }));
QVERIFY(picker()->toggleFavorite("NL"));
QCOMPARE(storedFavorites(serverA), QStringList({ "DE", "NL", "US" }));
QCOMPARE(storedFavorites(serverB), QStringList({ "DE", "US" }));
}
void testServerRemovedDropsItsFavorites()
{
storeServers({ apiServerJson(serverA, "a.example"), apiServerJson(serverB, "b.example") });
favoriteAll({ "US" });
showServer(serverB);
favoriteAll({ "FR" });
showServer(serverA);
m_coreController->m_serversRepository->removeServer(serverA);
QVERIFY(storedFavorites(serverA).isEmpty());
QCOMPARE(storedFavorites(serverB), QStringList({ "FR" }));
QVERIFY(countryModel()->serverId().isEmpty());
QVERIFY(picker()->favorites().isEmpty());
}
void testGeneratedServerIdIsPersisted()
{
QJsonObject unsupported = apiServerJson(QString(), "future.example");
unsupported[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion + 1;
storeServers({ apiServerJson(QString(), "no-id.example"), unsupported });
SecureServersRepository *servers = m_coreController->m_serversRepository;
QCOMPARE(servers->serversCount(), 1);
const QString generatedId = servers->serverIdAt(0);
QVERIFY(!generatedId.isEmpty());
servers->invalidateCache();
QCOMPARE(servers->serverIdAt(0), generatedId);
const QJsonArray stored =
QJsonDocument::fromJson(m_settings->value(QStringLiteral("Servers/serversList")).toByteArray()).array();
QCOMPARE(stored.size(), 2);
QCOMPARE(stored.at(0).toObject().value(configKey::storageServerId).toString(), generatedId);
}
void testClearSettingsResetsModels()
{
favoriteAll({ "FR", "DE" });
m_coreController->m_settingsController->clearSettings();
QVERIFY(storedFavorites(serverA).isEmpty());
QVERIFY(picker()->favorites().isEmpty());
QVERIFY(nativeConfigs()->favorites().isEmpty());
QVERIFY(picker()->toggleFavorite("US"));
QCOMPARE(storedFavorites(serverA), QStringList({ "US" }));
}
void testRestoreSortMode()
{
QCOMPARE(picker()->sortMode(), int(ApiCountryListModel::ByRegion));
restore(backupWith(QStringLiteral("Conf/countryListSortMode/picker"), int(ApiCountryListModel::Alphabetical)));
QCOMPARE(picker()->sortMode(), int(ApiCountryListModel::Alphabetical));
QCOMPARE(nativeConfigs()->sortMode(), int(ApiCountryListModel::ByRegion));
m_coreController->m_appSettingsRepository->clearSettings();
QCOMPARE(picker()->sortMode(), int(ApiCountryListModel::ByRegion));
}
void testRoundTrip()
{
favoriteAll({ "US", "DE", "JP" });
showServer(serverB);
favoriteAll({ "FR" });
const QByteArray backup = m_coreController->m_settingsController->backupAppConfig();
m_coreController->m_appSettingsRepository->clearSettings();
QVERIFY(picker()->favorites().isEmpty());
restore(backup);
QCOMPARE(picker()->favorites(), QStringList({ "FR" }));
QCOMPARE(storedFavorites(serverA), QStringList({ "DE", "JP", "US" }));
QCOMPARE(storedFavorites(serverB), QStringList({ "FR" }));
}
};
QTEST_MAIN(TestFavoriteLocationsRestore)
#include "testFavoriteLocationsRestore.moc"
+318
View File
@@ -0,0 +1,318 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QTest>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayMatrix.h"
using namespace amnezia;
using namespace xraytest;
namespace
{
XrayProtocolConfig withNative(const XrayServerConfig &srv, const QString &native)
{
XrayProtocolConfig cfg;
cfg.serverConfig = srv;
XrayClientConfig client;
client.nativeConfig = native;
client.localPort = QStringLiteral("10808");
client.id = clientId();
cfg.setClientConfig(client);
return cfg;
}
XrayProtocolConfig issued(const XrayServerConfig &srv)
{
return withNative(srv, compact(clientJson(srv)));
}
bool hasError(const QStringList &errors, const QString &needle)
{
for (const QString &e : errors) {
if (e.contains(needle))
return true;
}
return false;
}
}
class TestXrayConfigModel : public QObject
{
Q_OBJECT
private slots:
void options();
void isValidSni_data();
void isValidSni();
void validationErrors_data();
void validationErrors();
void deviceOnlyEditStaysLocal_data();
void deviceOnlyEditStaysLocal();
void serverEditTouchesServer_data();
void serverEditTouchesServer();
void portChangeRequiresReinstall();
void switchingToXhttpFillsDefaultsButKeepsParsedMode();
void unchangedSaveKeepsClientConfig();
void serverEditDropsClientConfig();
void deviceOnlyEditRebuildsClientConfig();
void deviceOnlyEditWithBrokenNativeDropsClientConfig();
void unsavedChangesFollowEdits();
};
void TestXrayConfigModel::options()
{
QCOMPARE(XrayConfigModel::xhttpModeOptions(),
QStringList({ "Auto", "Packet-up", "Stream-up", "Stream-one" }));
QCOMPARE(XrayConfigModel::xhttpUplinkDataPlacementOptions(),
QStringList({ "Body", "Auto", "Header", "Cookie" }));
QCOMPARE(XrayConfigModel::xPaddingBytesMinDefault(), QStringLiteral("100"));
QCOMPARE(XrayConfigModel::xPaddingBytesMaxDefault(), QStringLiteral("1000"));
}
void TestXrayConfigModel::isValidSni_data()
{
QTest::addColumn<QString>("sni");
QTest::addColumn<bool>("valid");
QTest::newRow("empty") << "" << true;
QTest::newRow("domain") << "www.example.com" << true;
QTest::newRow("ipv4") << "203.0.113.10" << true;
QTest::newRow("star") << "*" << false;
QTest::newRow("wildcard") << "*.asd.com" << false;
QTest::newRow("star inside") << "a*b.example.com" << false;
QTest::newRow("no tld") << "example" << false;
QTest::newRow("digits") << "12345" << false;
}
void TestXrayConfigModel::isValidSni()
{
QFETCH(QString, sni);
QFETCH(bool, valid);
QCOMPARE(XrayConfigModel::isValidSni(sni), valid);
}
void TestXrayConfigModel::validationErrors_data()
{
QTest::addColumn<QString>("transport");
QTest::addColumn<QString>("security");
QTest::addColumn<QString>("edit");
QTest::addColumn<QString>("expected");
QTest::newRow("valid raw reality") << "raw" << "reality" << "" << "";
QTest::newRow("port 0") << "raw" << "reality" << "port=0" << "Port must be";
QTest::newRow("port 70000") << "raw" << "reality" << "port=70000" << "Port must be";
QTest::newRow("tls sni *") << "raw" << "tls" << "sni=*" << "SNI";
QTest::newRow("reality wildcard sni") << "xhttp" << "reality" << "sni=*.asd.com" << "SNI";
QTest::newRow("none ignores sni") << "raw" << "none" << "sni=*" << "";
QTest::newRow("packet-up + reality") << "xhttp" << "reality" << "mode=Packet-up" << "";
QTest::newRow("packet-up + tls") << "xhttp" << "tls" << "mode=Packet-up" << "";
QTest::newRow("auto + reality") << "xhttp" << "reality" << "mode=Auto" << "";
QTest::newRow("xmux both") << "xhttp" << "tls" << "xmux=both" << "XMux";
QTest::newRow("xmux both but off") << "xhttp" << "tls" << "xmux=both-off" << "";
QTest::newRow("path without slash") << "xhttp" << "tls" << "path=text" << "";
QTest::newRow("bad host") << "xhttp" << "tls" << "host=bad host" << "Host";
}
void TestXrayConfigModel::validationErrors()
{
QFETCH(QString, transport);
QFETCH(QString, security);
QFETCH(QString, edit);
QFETCH(QString, expected);
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig(transport, security, "")));
const QString value = edit.section(QLatin1Char('='), 1);
if (edit.startsWith("port="))
model.setPort(value);
else if (edit.startsWith("sni="))
model.setSni(value);
else if (edit.startsWith("mode="))
model.setXhttpMode(value);
else if (edit.startsWith("path="))
model.setXhttpPath(value);
else if (edit.startsWith("host="))
model.setXhttpHost(value);
else if (edit.startsWith("xmux=")) {
model.setXmuxMaxConcurrencyMin("1");
model.setXmuxMaxConcurrencyMax("1");
model.setXmuxMaxConnectionsMin("1");
model.setXmuxMaxConnectionsMax("1");
model.setXmuxEnabled(value == QLatin1String("both"));
}
const QStringList errors = model.validationErrors();
if (expected.isEmpty())
QVERIFY2(errors.isEmpty(), qPrintable(errors.join("; ")));
else
QVERIFY2(hasError(errors, expected), qPrintable(errors.join("; ")));
}
void TestXrayConfigModel::deviceOnlyEditStaysLocal_data()
{
QTest::addColumn<QString>("edit");
QTest::newRow("fingerprint") << "fingerprint";
QTest::newRow("uplinkHTTPMethod") << "method";
QTest::newRow("uplinkChunkSize") << "chunk";
QTest::newRow("scMinPostsIntervalMs") << "minPosts";
QTest::newRow("xmux") << "xmux";
QTest::newRow("vision -> vision-udp443") << "udp443";
}
void TestXrayConfigModel::deviceOnlyEditStaysLocal()
{
QFETCH(QString, edit);
XrayConfigModel model;
const bool flowCase = edit == QLatin1String("udp443");
model.updateModel(DockerContainer::Xray,
issued(baseConfig(flowCase ? "raw" : "xhttp", "reality", flowCase ? "xtls-rprx-vision" : "")));
if (edit == QLatin1String("fingerprint"))
model.setFingerprint("firefox");
else if (edit == QLatin1String("method"))
model.setXhttpUplinkMethod("PATCH");
else if (edit == QLatin1String("chunk"))
model.setXhttpUplinkChunkSize("4096");
else if (edit == QLatin1String("minPosts"))
model.setXhttpScMinPostsIntervalMsMin("5");
else if (edit == QLatin1String("xmux"))
model.setXmuxMaxConcurrencyMax("64");
else
model.setFlow("xtls-rprx-vision-udp443");
QVERIFY(model.hasUnsavedChanges());
QVERIFY(!model.pendingChangeTouchesServer(QString()));
QVERIFY(!model.pendingChangeRequiresReinstall(QString()));
QCOMPARE(model.saveDescription(QString()),
QStringLiteral("The server will not be changed now. The new settings apply the next time you connect."));
}
void TestXrayConfigModel::serverEditTouchesServer_data()
{
QTest::addColumn<QString>("edit");
QTest::newRow("sni") << "sni";
QTest::newRow("path") << "path";
QTest::newRow("mode") << "mode";
QTest::newRow("transport") << "transport";
QTest::newRow("security") << "security";
QTest::newRow("xPaddingObfsMode") << "obfs";
}
void TestXrayConfigModel::serverEditTouchesServer()
{
QFETCH(QString, edit);
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig("xhttp", "reality", "")));
if (edit == QLatin1String("sni"))
model.setSni("www.microsoft.com");
else if (edit == QLatin1String("path"))
model.setXhttpPath("/other");
else if (edit == QLatin1String("mode"))
model.setXhttpMode("Stream-up");
else if (edit == QLatin1String("transport"))
model.setTransport("raw");
else if (edit == QLatin1String("security"))
model.setSecurity("tls");
else
model.setXPaddingObfsMode(!model.xPaddingObfsMode());
QVERIFY(model.pendingChangeTouchesServer(QString()));
QVERIFY(!model.pendingChangeRequiresReinstall(QString()));
}
void TestXrayConfigModel::portChangeRequiresReinstall()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig("raw", "reality", "")));
QVERIFY(model.pendingChangeRequiresReinstall(QStringLiteral("8443")));
QVERIFY(model.pendingChangeTouchesServer(QStringLiteral("8443")));
}
void TestXrayConfigModel::switchingToXhttpFillsDefaultsButKeepsParsedMode()
{
XrayConfigModel fresh;
XrayServerConfig raw = baseConfig("raw", "reality", "");
raw.xhttp = XrayXhttpConfig {};
fresh.updateModel(DockerContainer::Xray, issued(raw));
fresh.setTransport("xhttp");
QCOMPARE(fresh.xhttpMode(), QStringLiteral("Stream-one"));
QVERIFY(!fresh.xhttpSessionPlacement().isEmpty());
QVERIFY(!fresh.xhttpUplinkMethod().isEmpty());
XrayConfigModel parsed;
XrayServerConfig rawWithPacketUp = baseConfig("raw", "tls", "");
rawWithPacketUp.xhttp.mode = QStringLiteral("Packet-up");
parsed.updateModel(DockerContainer::Xray, issued(rawWithPacketUp));
parsed.setTransport("xhttp");
QCOMPARE(parsed.xhttpMode(), QStringLiteral("Packet-up"));
}
void TestXrayConfigModel::unchangedSaveKeepsClientConfig()
{
XrayConfigModel model;
const XrayProtocolConfig stored = issued(baseConfig("raw", "reality", "xtls-rprx-vision"));
model.updateModel(DockerContainer::Xray, stored);
const XrayProtocolConfig saved = model.getProtocolConfig();
QVERIFY(saved.hasClientConfig());
QCOMPARE(saved.clientConfig->nativeConfig, stored.clientConfig->nativeConfig);
}
void TestXrayConfigModel::serverEditDropsClientConfig()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig("raw", "reality", "")));
model.setSni("www.microsoft.com");
QVERIFY(!model.getProtocolConfig().hasClientConfig());
}
void TestXrayConfigModel::deviceOnlyEditRebuildsClientConfig()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig("xhttp", "reality", "")));
model.setFingerprint("firefox");
model.setXhttpUplinkMethod("PATCH");
const XrayProtocolConfig saved = model.getProtocolConfig();
QVERIFY(saved.hasClientConfig());
const QJsonObject native = QJsonDocument::fromJson(saved.clientConfig->nativeConfig.toUtf8()).object();
const QJsonObject stream = streamOf(vlessOutbound(native));
QCOMPARE(stream.value("realitySettings").toObject().value("fingerprint").toString(), QStringLiteral("firefox"));
QCOMPARE(stream.value("xhttpSettings").toObject().value("uplinkHTTPMethod").toString(), QStringLiteral("PATCH"));
QCOMPARE(stream.value("realitySettings").toObject().value("publicKey").toString(), key32('\x02'));
QCOMPARE(saved.clientConfig->id, clientId());
}
void TestXrayConfigModel::deviceOnlyEditWithBrokenNativeDropsClientConfig()
{
XrayConfigModel model;
const QString broken = QStringLiteral(R"({"outbounds":[{"protocol":"vless","settings":{}}]})");
model.updateModel(DockerContainer::Xray, withNative(baseConfig("xhttp", "reality", ""), broken));
model.setFingerprint("firefox");
QVERIFY2(!model.getProtocolConfig().hasClientConfig(),
"a native config that cannot be rebuilt must be dropped so the server issues a fresh one");
}
void TestXrayConfigModel::unsavedChangesFollowEdits()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig("raw", "reality", "")));
QVERIFY(!model.hasUnsavedChanges());
const QString original = model.fingerprint();
model.setFingerprint("firefox");
QVERIFY(model.hasUnsavedChanges());
model.setFingerprint(original);
QVERIFY(!model.hasUnsavedChanges());
}
QTEST_GUILESS_MAIN(TestXrayConfigModel)
#include "testXrayConfigModel.moc"
+279
View File
@@ -0,0 +1,279 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QProcess>
#include <QTemporaryDir>
#include <QTest>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayMatrix.h"
using namespace amnezia;
using namespace xraytest;
namespace
{
struct CoreCase
{
QString name;
XrayServerConfig srv;
bool allowedByUi = true;
};
QList<CoreCase> optionSweep()
{
QList<CoreCase> out;
for (const QString &security : { QStringLiteral("tls"), QStringLiteral("reality") }) {
for (const QString &fp : XrayConfigModel::fingerprintOptions()) {
XrayServerConfig srv = baseConfig("raw", security, "xtls-rprx-vision");
srv.fingerprint = fp;
out.append({ QStringLiteral("option/raw/%1/fingerprint=%2").arg(security, fp), srv });
}
}
for (const QString &alpn : XrayConfigModel::alpnOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "tls", "");
srv.alpn = alpn;
out.append({ QStringLiteral("option/xhttp/tls/alpn=%1").arg(alpn), srv });
}
for (const QString &method : XrayConfigModel::xhttpUplinkMethodOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "none", "");
srv.xhttp.mode = QStringLiteral("Packet-up");
srv.xhttp.uplinkMethod = method;
out.append({ QStringLiteral("option/xhttp/packet-up/uplinkMethod=%1").arg(method), srv });
}
for (const QString &pl : XrayConfigModel::xhttpSessionPlacementOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "none", "");
srv.xhttp.mode = QStringLiteral("Packet-up");
srv.xhttp.sessionPlacement = pl;
srv.xhttp.seqPlacement = pl;
out.append({ QStringLiteral("option/xhttp/packet-up/session+seqPlacement=%1").arg(pl), srv });
}
for (const QString &pl : XrayConfigModel::xPaddingPlacementOptions()) {
for (const QString &method : XrayConfigModel::xPaddingMethodOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "reality", "");
srv.xhttp.xPadding.obfsMode = true;
srv.xhttp.xPadding.placement = pl;
srv.xhttp.xPadding.method = method;
srv.xhttp.xPadding.bytesMin = QStringLiteral("100");
srv.xhttp.xPadding.bytesMax = QStringLiteral("1000");
out.append({ QStringLiteral("option/xhttp/xPadding/%1/%2").arg(pl, method), srv });
}
}
for (const QString &mode : XrayConfigModel::xhttpModeOptions()) {
for (const QString &pl : XrayConfigModel::xhttpUplinkDataPlacementOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "tls", "");
srv.xhttp.mode = mode;
srv.xhttp.uplinkDataPlacement = pl;
out.append({ QStringLiteral("option/xhttp/tls/mode=%1/dataPlacement=%2").arg(mode, pl), srv });
}
}
return out;
}
const QList<CoreCase> &cases()
{
static const QList<CoreCase> all = [] {
QList<CoreCase> out;
for (const MatrixCase &m : matrix())
out.append({ m.name, m.srv, m.allowedByUi });
out.append(optionSweep());
return out;
}();
return all;
}
QString fileNameFor(const QString &name)
{
QString out = name;
for (QChar &ch : out) {
if (!ch.isLetterOrNumber() && ch != QLatin1Char('-'))
ch = QLatin1Char('_');
}
return out;
}
}
class TestXrayCoreAccepts : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void generatedConfigs_data();
void generatedConfigs();
void coreRejectsWhatWeRelyOn_data();
void coreRejectsWhatWeRelyOn();
private:
struct Run
{
int exitCode = -1;
QString output;
};
Run xrayTest(const QJsonObject &config, const QString &fileName);
void useLocalCertificate(QJsonObject &serverConfig) const;
QString m_xray;
QTemporaryDir m_dir;
QString m_certFile;
QString m_keyFile;
};
void TestXrayCoreAccepts::initTestCase()
{
m_xray = qEnvironmentVariable("XRAY_BIN");
if (m_xray.isEmpty() || !QFileInfo(m_xray).isExecutable())
QSKIP("Set XRAY_BIN to an xray v26.7.28 binary to run the core checks");
QVERIFY(m_dir.isValid());
QProcess version;
version.start(m_xray, { QStringLiteral("version") });
QVERIFY(version.waitForFinished(30000));
const QString firstLine = QString::fromUtf8(version.readAllStandardOutput()).section(QLatin1Char('\n'), 0, 0);
qInfo().noquote() << "xray under test:" << firstLine;
if (!firstLine.contains(QLatin1String("26.7.28")))
qWarning().noquote() << "XRAY_BIN is not v26.7.28, results may differ from the VPS";
const QString prefix = m_dir.filePath(QStringLiteral("tls"));
QProcess cert;
cert.start(m_xray, { QStringLiteral("tls"), QStringLiteral("cert"), QStringLiteral("-domain"),
QStringLiteral("amnezia-xray"), QStringLiteral("-file"), prefix });
QVERIFY(cert.waitForFinished(30000));
m_certFile = prefix + QStringLiteral(".crt");
m_keyFile = prefix + QStringLiteral(".key");
QVERIFY2(QFile::exists(m_certFile) && QFile::exists(m_keyFile),
qPrintable(QString::fromUtf8(cert.readAllStandardError())));
}
TestXrayCoreAccepts::Run TestXrayCoreAccepts::xrayTest(const QJsonObject &config, const QString &fileName)
{
const QString path = m_dir.filePath(fileName + QStringLiteral(".json"));
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
return { -1, QStringLiteral("cannot write ") + path };
file.write(QJsonDocument(config).toJson());
file.close();
QProcess process;
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(m_xray, { QStringLiteral("-test"), QStringLiteral("-format"), QStringLiteral("json"),
QStringLiteral("-config"), path });
if (!process.waitForFinished(30000))
return { -1, QStringLiteral("xray -test timed out") };
Run run;
run.exitCode = process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
run.output = QString::fromUtf8(process.readAll()).trimmed();
return run;
}
void TestXrayCoreAccepts::useLocalCertificate(QJsonObject &serverConfig) const
{
QJsonArray inbounds = serverConfig.value("inbounds").toArray();
QJsonObject inbound = inbounds.first().toObject();
QJsonObject stream = inbound.value("streamSettings").toObject();
QJsonObject tls = stream.value("tlsSettings").toObject();
if (tls.isEmpty())
return;
tls["certificates"] = QJsonArray { QJsonObject { { "certificateFile", m_certFile }, { "keyFile", m_keyFile } } };
stream["tlsSettings"] = tls;
inbound["streamSettings"] = stream;
inbounds[0] = inbound;
serverConfig["inbounds"] = inbounds;
}
void TestXrayCoreAccepts::generatedConfigs_data()
{
QTest::addColumn<int>("index");
for (int i = 0; i < cases().size(); ++i)
QTest::newRow(qPrintable(cases().at(i).name)) << i;
}
void TestXrayCoreAccepts::generatedConfigs()
{
QFETCH(int, index);
const CoreCase &c = cases().at(index);
QJsonObject server = serverJson(c.srv);
useLocalCertificate(server);
const QJsonObject client = clientJson(c.srv);
const QString base = fileNameFor(c.name);
const Run serverRun = xrayTest(server, base + QStringLiteral("_server"));
const Run clientRun = xrayTest(client, base + QStringLiteral("_client"));
if (c.name.endsWith(QLatin1String("xmux-concurrency-and-connections"))) {
QVERIFY2(clientRun.exitCode != 0, qPrintable(clientRun.output));
QVERIFY2(clientRun.output.contains(QLatin1String("maxConnections cannot be specified together")),
qPrintable(clientRun.output));
return;
}
if (!c.allowedByUi)
QSKIP("Blocked by validationErrors()");
QVERIFY2(serverRun.exitCode == 0, qPrintable(QStringLiteral("server.json rejected: ") + serverRun.output
+ QStringLiteral("\n") + compact(server)));
QVERIFY2(clientRun.exitCode == 0, qPrintable(QStringLiteral("client config rejected: ") + clientRun.output
+ QStringLiteral("\n") + compact(client)));
}
void TestXrayCoreAccepts::coreRejectsWhatWeRelyOn_data()
{
QTest::addColumn<QString>("what");
QTest::addColumn<QString>("expected");
QTest::newRow("inbound flow with -udp443") << "udp443" << "doesn't support";
QTest::newRow("cookie uplinkDataPlacement outside packet-up") << "cookie" << "only in packet-up";
QTest::newRow("GET outside packet-up") << "get" << "GET only in packet-up";
QTest::newRow("allowInsecure") << "insecure" << "allowInsecure";
}
void TestXrayCoreAccepts::coreRejectsWhatWeRelyOn()
{
QFETCH(QString, what);
QFETCH(QString, expected);
QJsonObject config;
if (what == QLatin1String("udp443")) {
XrayServerConfig srv = baseConfig("raw", "reality", "xtls-rprx-vision");
XrayServerInboundInputs in = inboundInputs(srv);
in.clients = QJsonArray { XrayServerConfig::makeClientEntry(clientId(), "xtls-rprx-vision-udp443") };
config = srv.toServerInboundJson(in);
} else {
XrayServerConfig srv = baseConfig("xhttp", "tls", "");
srv.xhttp.mode = QStringLiteral("Stream-one");
config = clientJson(srv);
QJsonArray outbounds = config.value("outbounds").toArray();
for (int i = 0; i < outbounds.size(); ++i) {
QJsonObject ob = outbounds.at(i).toObject();
if (ob.value("protocol").toString() != QLatin1String("vless"))
continue;
QJsonObject stream = ob.value("streamSettings").toObject();
if (what == QLatin1String("insecure")) {
QJsonObject tls = stream.value("tlsSettings").toObject();
tls["allowInsecure"] = true;
stream["tlsSettings"] = tls;
} else {
QJsonObject x = stream.value("xhttpSettings").toObject();
if (what == QLatin1String("cookie"))
x["uplinkDataPlacement"] = QStringLiteral("cookie");
else
x["uplinkHTTPMethod"] = QStringLiteral("GET");
stream["xhttpSettings"] = x;
}
ob["streamSettings"] = stream;
outbounds[i] = ob;
}
config["outbounds"] = outbounds;
}
const Run run = xrayTest(config, QStringLiteral("negative_") + what);
QVERIFY2(run.exitCode != 0, qPrintable(QStringLiteral("xray accepted it: ") + compact(config)));
QVERIFY2(run.output.contains(expected), qPrintable(run.output));
}
QTEST_GUILESS_MAIN(TestXrayCoreAccepts)
#include "testXrayCoreAccepts.moc"
+249
View File
@@ -0,0 +1,249 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QTest>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "utils/xrayMatrix.h"
using namespace amnezia;
using namespace xraytest;
namespace
{
const QList<MatrixCase> &cases()
{
static const QList<MatrixCase> all = matrix();
return all;
}
QString str(const QJsonObject &obj, const char *key)
{
return obj.value(QString::fromLatin1(key)).toString();
}
QString serverClientFlow(const QJsonObject &inbound)
{
const QJsonArray clients = inbound.value("settings").toObject().value("clients").toArray();
return clients.isEmpty() ? QString() : str(clients.first().toObject(), "flow");
}
QString outboundUserFlow(const QJsonObject &outbound)
{
const QJsonArray vnext = outbound.value("settings").toObject().value("vnext").toArray();
if (vnext.isEmpty())
return {};
const QJsonArray users = vnext.first().toObject().value("users").toArray();
return users.isEmpty() ? QString() : str(users.first().toObject(), "flow");
}
}
class TestXrayGeneratorMatrix : public QObject
{
Q_OBJECT
private slots:
void invariants_data();
void invariants();
void deviceOnlyChangesKeepServerView_data();
void deviceOnlyChangesKeepServerView();
void serverChangesChangeServerView_data();
void serverChangesChangeServerView();
};
void TestXrayGeneratorMatrix::invariants_data()
{
QTest::addColumn<int>("index");
for (int i = 0; i < cases().size(); ++i)
QTest::newRow(qPrintable(cases().at(i).name)) << i;
}
void TestXrayGeneratorMatrix::invariants()
{
QFETCH(int, index);
const MatrixCase &c = cases().at(index);
const XrayServerConfig &srv = c.srv;
const QJsonObject server = serverJson(srv);
const QJsonObject client = clientJson(srv);
const QJsonObject inbound = firstInbound(server);
const QJsonObject outbound = vlessOutbound(client);
const QJsonObject sIn = streamOf(inbound);
const QJsonObject sOut = streamOf(outbound);
const QString serverText = compact(server);
const QString clientText = compact(client);
QVERIFY2(!outbound.isEmpty(), qPrintable(clientText));
const bool raw = srv.transport == QLatin1String("raw");
const bool xhttp = srv.transport == QLatin1String("xhttp");
const bool mkcp = srv.transport == QLatin1String("mkcp");
const QString network = xhttp ? QStringLiteral("xhttp") : mkcp ? QStringLiteral("kcp") : QStringLiteral("tcp");
QCOMPARE(str(sIn, "network"), network);
QCOMPARE(str(sOut, "network"), network);
QCOMPARE(sIn.contains("xhttpSettings"), xhttp);
QCOMPARE(sOut.contains("xhttpSettings"), xhttp);
QCOMPARE(sIn.contains("kcpSettings"), mkcp);
QCOMPARE(sOut.contains("kcpSettings"), mkcp);
const QString security = (mkcp && srv.security == QLatin1String("reality")) ? QStringLiteral("none") : srv.security;
QCOMPARE(str(sIn, "security"), security);
QCOMPARE(str(sOut, "security"), security);
for (const char *key : { "uplinkHTTPMethod", "uplinkChunkSize", "scMinPostsIntervalMs", "xmux", "fingerprint" })
QVERIFY2(!containsKeyDeep(server, QString::fromLatin1(key)), qPrintable(QString::fromLatin1(key) + ": " + serverText));
QVERIFY2(!containsKeyDeep(server, "allowInsecure") && !containsKeyDeep(client, "allowInsecure"), qPrintable(clientText));
const bool visionAllowed = raw && (security == QLatin1String("tls") || security == QLatin1String("reality"));
QVERIFY2(!serverText.contains("udp443"), qPrintable(serverText));
if (!visionAllowed || srv.flow.isEmpty()) {
QCOMPARE(serverClientFlow(inbound), QString());
QCOMPARE(outboundUserFlow(outbound), QString());
} else {
QCOMPARE(serverClientFlow(inbound), QStringLiteral("xtls-rprx-vision"));
QCOMPARE(outboundUserFlow(outbound), srv.flow);
}
if (security == QLatin1String("reality")) {
const QJsonObject rIn = sIn.value("realitySettings").toObject();
const QJsonObject rOut = sOut.value("realitySettings").toObject();
QCOMPARE(str(rIn, "dest"), srv.site + QStringLiteral(":443"));
QCOMPARE(rIn.value("serverNames").toArray(), QJsonArray { srv.sni });
QCOMPARE(str(rIn, "privateKey"), key32('\x01'));
QCOMPARE(rIn.value("shortIds").toArray(), QJsonArray { shortId() });
QCOMPARE(str(rOut, "serverName"), srv.sni);
QCOMPARE(str(rOut, "publicKey"), key32('\x02'));
QCOMPARE(str(rOut, "shortId"), shortId());
QCOMPARE(str(rOut, "fingerprint"), srv.fingerprint);
QVERIFY(!sIn.contains("tlsSettings") && !sOut.contains("tlsSettings"));
} else if (security == QLatin1String("tls")) {
const QJsonObject tIn = sIn.value("tlsSettings").toObject();
const QJsonObject tOut = sOut.value("tlsSettings").toObject();
QVERIFY2(!tIn.value("certificates").toArray().isEmpty(), qPrintable(serverText));
QCOMPARE(str(tOut, "serverName"), srv.sni);
QCOMPARE(str(tOut, "pinnedPeerCertSha256"), tlsPin());
QCOMPARE(tOut.contains("fingerprint"), !mkcp);
QVERIFY(!sIn.contains("realitySettings") && !sOut.contains("realitySettings"));
} else {
QVERIFY2(!sIn.contains("tlsSettings") && !sIn.contains("realitySettings"), qPrintable(serverText));
QVERIFY2(!sOut.contains("tlsSettings") && !sOut.contains("realitySettings"), qPrintable(clientText));
}
if (!xhttp)
return;
const QJsonObject xIn = sIn.value("xhttpSettings").toObject();
const QJsonObject xOut = sOut.value("xhttpSettings").toObject();
const QString mode = xrayEffective::xhttpMode(srv.xhttp.mode);
QCOMPARE(str(xIn, "mode"), mode);
QCOMPARE(str(xOut, "mode"), mode);
QVERIFY2(str(xIn, "path").startsWith('/') && str(xOut, "path").startsWith('/'), qPrintable(clientText));
QCOMPARE(str(xIn, "path"), str(xOut, "path"));
const bool packetUp = mode == QLatin1String("packet-up");
for (const QJsonObject &x : { xIn, xOut }) {
const QString dataPlacement = str(x, "uplinkDataPlacement");
if (dataPlacement == QLatin1String("header") || dataPlacement == QLatin1String("cookie"))
QVERIFY2(packetUp, qPrintable(compact(x)));
}
if (str(xOut, "uplinkHTTPMethod") == QLatin1String("GET"))
QVERIFY2(packetUp, qPrintable(clientText));
for (const QJsonObject &x : { xIn, xOut }) {
QVERIFY(x.contains("xPaddingObfsMode"));
QCOMPARE(x.value("xPaddingObfsMode").toBool(), srv.xhttp.xPadding.obfsMode);
if (!srv.xhttp.xPadding.obfsMode) {
for (const char *key : { "xPaddingBytes", "xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod" })
QVERIFY2(!x.contains(QString::fromLatin1(key)), qPrintable(compact(x)));
}
}
const XrayXmuxConfig &xmux = srv.xhttp.xmux;
QCOMPARE(xOut.contains("xmux"), xmux.enabled && (xmux.maxConcurrencyMax != "0" || xmux.maxConnectionsMax != "0"));
if (xOut.contains("xmux")) {
const QJsonObject mux = xOut.value("xmux").toObject();
QCOMPARE(mux.contains("maxConcurrency"), xmux.maxConcurrencyMax != "0");
QCOMPARE(mux.contains("maxConnections"), xmux.maxConnectionsMax != "0");
}
}
void TestXrayGeneratorMatrix::deviceOnlyChangesKeepServerView_data()
{
QTest::addColumn<QString>("field");
QTest::newRow("fingerprint") << "fingerprint";
QTest::newRow("uplinkMethod") << "uplinkMethod";
QTest::newRow("uplinkChunkSize") << "uplinkChunkSize";
QTest::newRow("scMinPostsIntervalMs") << "scMinPostsIntervalMs";
QTest::newRow("xmux") << "xmux";
QTest::newRow("vision -> vision-udp443") << "udp443";
}
void TestXrayGeneratorMatrix::deviceOnlyChangesKeepServerView()
{
QFETCH(QString, field);
XrayServerConfig before = baseConfig(field == "udp443" ? "raw" : "xhttp", "reality",
field == "udp443" ? "xtls-rprx-vision" : "");
XrayServerConfig after = before;
if (field == "fingerprint")
after.fingerprint = QStringLiteral("firefox");
else if (field == "uplinkMethod")
after.xhttp.uplinkMethod = QStringLiteral("PATCH");
else if (field == "uplinkChunkSize")
after.xhttp.uplinkChunkSize = QStringLiteral("4096");
else if (field == "scMinPostsIntervalMs")
after.xhttp.scMinPostsIntervalMsMin = QStringLiteral("5");
else if (field == "xmux")
after.xhttp.xmux.maxConcurrencyMax = QStringLiteral("64");
else if (field == "udp443")
after.flow = QStringLiteral("xtls-rprx-vision-udp443");
QVERIFY2(before.toJson() != after.toJson(), "the edit must change the stored settings");
QVERIFY2(before.hasEqualServerSettings(after),
qPrintable(QStringLiteral("a device-only edit must not count as a server change: ")
+ compact(before.serverView()) + QStringLiteral(" -> ") + compact(after.serverView())));
QCOMPARE(compact(serverJson(before)), compact(serverJson(after)));
}
void TestXrayGeneratorMatrix::serverChangesChangeServerView_data()
{
QTest::addColumn<QString>("field");
QTest::newRow("port") << "port";
QTest::newRow("transport") << "transport";
QTest::newRow("security") << "security";
QTest::newRow("sni") << "sni";
QTest::newRow("path") << "path";
QTest::newRow("mode") << "mode";
QTest::newRow("host") << "host";
QTest::newRow("xPaddingObfsMode") << "obfs";
}
void TestXrayGeneratorMatrix::serverChangesChangeServerView()
{
QFETCH(QString, field);
const XrayServerConfig before = baseConfig("xhttp", "reality", "");
XrayServerConfig after = before;
if (field == "port")
after.port = QStringLiteral("8443");
else if (field == "transport")
after.transport = QStringLiteral("raw");
else if (field == "security")
after.security = QStringLiteral("tls");
else if (field == "sni") {
after.sni = QStringLiteral("www.microsoft.com");
after.site = after.sni;
} else if (field == "path")
after.xhttp.path = QStringLiteral("/other");
else if (field == "mode")
after.xhttp.mode = QStringLiteral("Stream-up");
else if (field == "host")
after.xhttp.host = QStringLiteral("cdn.example.com");
else if (field == "obfs")
after.xhttp.xPadding.obfsMode = !before.xhttp.xPadding.obfsMode;
QVERIFY2(!before.hasEqualServerSettings(after), qPrintable(field + " must count as a server change"));
}
QTEST_APPLESS_MAIN(TestXrayGeneratorMatrix)
#include "testXrayGeneratorMatrix.moc"
+248
View File
@@ -0,0 +1,248 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QTest>
#include "core/models/protocols/xrayProtocolConfig.h"
using namespace amnezia;
namespace
{
XrayServerConfig fullyConfigured()
{
XrayServerConfig c;
c.port = "444";
c.transportProto = "tcp";
c.site = "dest.example.com";
c.security = "reality";
c.flow = "xtls-rprx-vision";
c.sni = "sni.example.com";
c.alpn = "h2";
c.fingerprint = "firefox";
c.transport = "xhttp";
c.xhttp.mode = "Stream-up";
c.xhttp.host = "host.example.com";
c.xhttp.path = "/p";
c.xhttp.uplinkMethod = "PUT";
c.xhttp.uplinkChunkSize = "4096";
c.xhttp.sessionPlacement = "Query";
c.xhttp.sessionKey = "sid";
c.xhttp.seqPlacement = "Query";
c.xhttp.seqKey = "page";
c.xhttp.uplinkDataPlacement = "Auto";
c.xhttp.uplinkDataKey = "x_data";
c.xhttp.scMaxBufferedPosts = "77";
c.xhttp.scMaxEachPostBytesMin = "1000";
c.xhttp.scMaxEachPostBytesMax = "2000";
c.xhttp.scMinPostsIntervalMsMin = "30";
c.xhttp.scMinPostsIntervalMsMax = "60";
c.xhttp.scStreamUpServerSecsMin = "5";
c.xhttp.scStreamUpServerSecsMax = "9";
c.xhttp.xPadding.bytesMin = "100";
c.xhttp.xPadding.bytesMax = "1000";
c.xhttp.xPadding.key = "_dc";
c.xhttp.xPadding.header = "Referer";
c.xhttp.xPadding.placement = "Query in header";
c.xhttp.xPadding.method = "Tokenish";
c.xhttp.xmux.maxConcurrencyMin = "16";
c.xhttp.xmux.maxConcurrencyMax = "32";
c.xhttp.xmux.hKeepAlivePeriod = "45";
return c;
}
XrayServerInboundInputs inboundInputs()
{
XrayServerInboundInputs in;
in.clients = QJsonArray { QJsonObject { { "id", "11111111-2222-3333-4444-555555555555" },
{ "flow", "xtls-rprx-vision" } } };
in.realityPrivateKey = "private-key";
in.realityShortId = "shortid";
return in;
}
QString compact(const QJsonObject &obj)
{
return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}
}
class TestXrayServerInbound : public QObject
{
Q_OBJECT
private slots:
void serverInboundCarriesNoDeviceSettings_data();
void serverInboundCarriesNoDeviceSettings();
void clientOutboundCarriesDeviceSettings();
void serverInboundStillCarriesWhatTheServerListensWith();
void rebuildFromServerLosesOnlyDeviceSettings();
void realityIsDowngradedOnlyForMkcp_data();
void realityIsDowngradedOnlyForMkcp();
void udp443FlowStaysOnTheDevice();
void mkcpTlsOutboundHasNoFingerprint();
void xhttpPathGetsLeadingSlash();
};
void TestXrayServerInbound::serverInboundCarriesNoDeviceSettings_data()
{
QTest::addColumn<QString>("key");
QTest::newRow("uplinkHTTPMethod") << QStringLiteral("uplinkHTTPMethod");
QTest::newRow("uplinkChunkSize") << QStringLiteral("uplinkChunkSize");
QTest::newRow("scMinPostsIntervalMs") << QStringLiteral("scMinPostsIntervalMs");
QTest::newRow("xmux") << QStringLiteral("xmux");
QTest::newRow("fingerprint") << QStringLiteral("fingerprint");
}
void TestXrayServerInbound::serverInboundCarriesNoDeviceSettings()
{
QFETCH(QString, key);
const QString inbound = compact(fullyConfigured().toServerInboundJson(inboundInputs()));
QVERIFY2(!inbound.contains(QStringLiteral("\"%1\"").arg(key)),
qPrintable(QStringLiteral("%1 must not reach server.json. It is a setting of this device: "
"xray's inbound never reads it, so a copy there is a second "
"source of truth. Inbound was: %2")
.arg(key, inbound)));
}
void TestXrayServerInbound::clientOutboundCarriesDeviceSettings()
{
XrayProtocolConfig cfg;
cfg.serverConfig = fullyConfigured();
XrayClientOutboundInputs inputs;
inputs.serverAddress = "203.0.113.10";
inputs.clientId = "11111111-2222-3333-4444-555555555555";
inputs.realityPublicKey = "public-key";
inputs.realityShortId = "shortid";
const QString outbound = compact(cfg.toClientOutboundJson(inputs));
QVERIFY2(outbound.contains(QStringLiteral("\"uplinkHTTPMethod\":\"PUT\"")), qPrintable(outbound));
QVERIFY2(outbound.contains(QStringLiteral("\"uplinkChunkSize\":4096")), qPrintable(outbound));
QVERIFY2(outbound.contains(QStringLiteral("\"scMinPostsIntervalMs\":\"30-60\"")), qPrintable(outbound));
QVERIFY2(outbound.contains(QStringLiteral("\"xmux\"")), qPrintable(outbound));
QVERIFY2(outbound.contains(QStringLiteral("\"fingerprint\":\"firefox\"")), qPrintable(outbound));
}
void TestXrayServerInbound::serverInboundStillCarriesWhatTheServerListensWith()
{
const QString inbound = compact(fullyConfigured().toServerInboundJson(inboundInputs()));
QVERIFY2(inbound.contains(QStringLiteral("\"port\":444")), qPrintable(inbound));
QVERIFY2(inbound.contains(QStringLiteral("\"network\":\"xhttp\"")), qPrintable(inbound));
QVERIFY2(inbound.contains(QStringLiteral("\"security\":\"reality\"")), qPrintable(inbound));
QVERIFY2(inbound.contains(QStringLiteral("\"host\":\"host.example.com\"")), qPrintable(inbound));
QVERIFY2(inbound.contains(QStringLiteral("\"path\":\"/p\"")), qPrintable(inbound));
}
void TestXrayServerInbound::rebuildFromServerLosesOnlyDeviceSettings()
{
const XrayServerConfig before = fullyConfigured();
const QJsonObject inbound = before.toServerInboundJson(inboundInputs());
XrayServerConfig rebuilt;
QCOMPARE(XrayServerConfig::fromServerInboundJson(inbound, rebuilt), XrayServerJsonStatus::Ok);
QCOMPARE(rebuilt.port, before.port);
QCOMPARE(rebuilt.transport, before.transport);
QCOMPARE(rebuilt.security, before.security);
QCOMPARE(rebuilt.sni, before.sni);
QCOMPARE(rebuilt.site, before.site);
QCOMPARE(rebuilt.xhttp.mode, before.xhttp.mode);
QCOMPARE(rebuilt.xhttp.host, before.xhttp.host);
QCOMPARE(rebuilt.xhttp.path, before.xhttp.path);
QCOMPARE(rebuilt.xhttp.xPadding.placement, before.xhttp.xPadding.placement);
QCOMPARE(rebuilt.xhttp.xPadding.method, before.xhttp.xPadding.method);
QVERIFY(rebuilt.xhttp.uplinkMethod.isEmpty());
QVERIFY(rebuilt.xhttp.uplinkChunkSize.isEmpty());
QVERIFY(rebuilt.xhttp.scMinPostsIntervalMsMin.isEmpty());
QCOMPARE(rebuilt.xhttp.xmux.maxConcurrencyMin, QStringLiteral("0"));
}
void TestXrayServerInbound::realityIsDowngradedOnlyForMkcp_data()
{
QTest::addColumn<QString>("transport");
QTest::addColumn<QString>("security");
QTest::addColumn<QString>("effective");
QTest::newRow("mkcp + reality") << "mkcp" << "reality" << "none";
QTest::newRow("mkcp + tls") << "mkcp" << "tls" << "tls";
QTest::newRow("mkcp + none") << "mkcp" << "none" << "none";
QTest::newRow("raw + reality") << "raw" << "reality" << "reality";
QTest::newRow("xhttp + reality") << "xhttp" << "reality" << "reality";
}
void TestXrayServerInbound::realityIsDowngradedOnlyForMkcp()
{
QFETCH(QString, transport);
QFETCH(QString, security);
QFETCH(QString, effective);
XrayServerConfig c;
c.transport = transport;
c.security = security;
QCOMPARE(xrayEffective::security(c), effective);
QCOMPARE(c.security, security);
}
void TestXrayServerInbound::udp443FlowStaysOnTheDevice()
{
XrayServerConfig c = fullyConfigured();
c.transport = "raw";
c.flow = "xtls-rprx-vision-udp443";
QCOMPARE(xrayEffective::serverFlow(c), QStringLiteral("xtls-rprx-vision"));
QCOMPARE(xrayEffective::clientFlow(c), QStringLiteral("xtls-rprx-vision-udp443"));
XrayProtocolConfig cfg;
cfg.serverConfig = c;
XrayClientOutboundInputs inputs;
inputs.serverAddress = "203.0.113.10";
inputs.clientId = "11111111-2222-3333-4444-555555555555";
inputs.realityPublicKey = "public-key";
inputs.realityShortId = "shortid";
const QString outbound = compact(cfg.toClientOutboundJson(inputs));
QVERIFY2(outbound.contains(QStringLiteral("\"flow\":\"xtls-rprx-vision-udp443\"")), qPrintable(outbound));
XrayServerConfig seeded = c;
QCOMPARE(XrayServerConfig::fromServerInboundJson(c.toServerInboundJson(inboundInputs()), seeded),
XrayServerJsonStatus::Ok);
QCOMPARE(seeded.flow, QStringLiteral("xtls-rprx-vision-udp443"));
}
void TestXrayServerInbound::mkcpTlsOutboundHasNoFingerprint()
{
XrayProtocolConfig cfg;
cfg.serverConfig = fullyConfigured();
cfg.serverConfig.transport = "mkcp";
cfg.serverConfig.security = "tls";
XrayClientOutboundInputs inputs;
inputs.serverAddress = "203.0.113.10";
inputs.clientId = "11111111-2222-3333-4444-555555555555";
const QString outbound = compact(cfg.toClientOutboundJson(inputs));
QVERIFY2(outbound.contains(QStringLiteral("\"tlsSettings\"")), qPrintable(outbound));
QVERIFY2(!outbound.contains(QStringLiteral("\"fingerprint\"")), qPrintable(outbound));
}
void TestXrayServerInbound::xhttpPathGetsLeadingSlash()
{
XrayServerConfig c = fullyConfigured();
c.xhttp.path = "text";
const QString inbound = compact(c.toServerInboundJson(inboundInputs()));
QVERIFY2(inbound.contains(QStringLiteral("\"path\":\"/text\"")), qPrintable(inbound));
}
QTEST_APPLESS_MAIN(TestXrayServerInbound)
#include "testXrayServerInbound.moc"
+6
View File
@@ -40,6 +40,9 @@ public:
, m_servicesCatalogUiController(servicesCatalogUiControllerProtected())
, m_apiNewsUiController(apiNewsUiControllerProtected())
, m_subscriptionController(subscriptionControllerProtected())
, m_apiCountryModel(apiCountryModelProtected())
, m_apiCountryListModel(apiCountryListModelProtected())
, m_apiConfigsCountryListModel(apiConfigsCountryListModelProtected())
{
}
@@ -73,6 +76,9 @@ public:
ServicesCatalogUiController *m_servicesCatalogUiController;
ApiNewsUiController *m_apiNewsUiController;
SubscriptionController *m_subscriptionController;
ApiCountryModel *m_apiCountryModel;
ApiCountryListModel *m_apiCountryListModel;
ApiCountryListModel *m_apiConfigsCountryListModel;
};
#endif // TESTCORECONTROLLER_H
+3 -1
View File
@@ -22,11 +22,13 @@ QString getEnvValue(const QString &key)
void logEnvValueState(const QString &key)
{
const QString value = getEnvValue(key);
const bool printable = key.endsWith(QLatin1String("_HOST_NAME")) || key.endsWith(QLatin1String("_USER_NAME"))
|| key.endsWith(QLatin1String("_PORT")) || key.endsWith(QLatin1String("_PATH"));
qInfo().noquote() << QString("%1: configured=%2 length=%3 value=\"%4\"")
.arg(key,
isEnvValueConfigured(value) ? "yes" : "no",
QString::number(value.size()),
value);
printable || value.isEmpty() ? value : QStringLiteral("<hidden>"));
}
bool isEnvValueConfigured(const QString &value)
+201
View File
@@ -0,0 +1,201 @@
#ifndef XRAY_TEST_MATRIX_H
#define XRAY_TEST_MATRIX_H
#include <QByteArray>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QList>
#include <QString>
#include <QStringList>
#include "core/models/protocols/xrayProtocolConfig.h"
namespace xraytest
{
struct MatrixCase
{
QString name;
amnezia::XrayServerConfig srv;
bool allowedByUi = true;
};
inline QString clientId()
{
return QStringLiteral("11111111-2222-4333-8444-555555555555");
}
inline QString key32(char fill)
{
return QString::fromLatin1(QByteArray(32, fill).toBase64(QByteArray::Base64UrlEncoding
| QByteArray::OmitTrailingEquals));
}
inline QString shortId()
{
return QStringLiteral("0123456789abcdef");
}
inline QString tlsPin()
{
return QString(64, QLatin1Char('a'));
}
inline amnezia::XrayServerConfig baseConfig(const QString &transport, const QString &security, const QString &flow)
{
amnezia::XrayServerConfig srv;
srv.port = QStringLiteral("443");
srv.transportProto = QStringLiteral("tcp");
srv.transport = transport;
srv.security = security;
srv.flow = flow;
srv.sni = QStringLiteral("www.example.com");
srv.site = QStringLiteral("www.example.com");
srv.applyDefaults(false);
srv.settingsVersion = amnezia::protocols::xray::settingsVersionCurrent;
return srv;
}
inline QList<MatrixCase> matrix()
{
const QStringList transports = { "raw", "xhttp", "mkcp" };
const QStringList securities = { "none", "tls", "reality" };
const QStringList flows = { "", "xtls-rprx-vision", "xtls-rprx-vision-udp443" };
const QStringList modes = { "Auto", "Packet-up", "Stream-up", "Stream-one" };
const QStringList placements = { "Body", "Auto", "Header", "Cookie" };
QList<MatrixCase> cases;
for (const QString &transport : transports) {
for (const QString &security : securities) {
for (const QString &flow : flows) {
const QString flowName = flow.isEmpty() ? QStringLiteral("noflow") : flow;
if (transport != QLatin1String("xhttp")) {
cases.append({ QStringLiteral("%1/%2/%3").arg(transport, security, flowName),
baseConfig(transport, security, flow), true });
continue;
}
for (const QString &mode : modes) {
for (const QString &placement : placements) {
for (const bool obfs : { true, false }) {
MatrixCase c;
c.srv = baseConfig(transport, security, flow);
c.srv.xhttp.mode = mode;
c.srv.xhttp.uplinkDataPlacement = placement;
c.srv.xhttp.xPadding.obfsMode = obfs;
c.srv.xhttp.uplinkMethod = placement == QLatin1String("Header") ? QStringLiteral("GET")
: QStringLiteral("PUT");
c.srv.xhttp.path = placement == QLatin1String("Cookie") ? QStringLiteral("text")
: QStringLiteral("/p");
auto &xmux = c.srv.xhttp.xmux;
xmux.enabled = placement != QLatin1String("Auto");
if (placement == QLatin1String("Body")) {
xmux.maxConcurrencyMin = QStringLiteral("16");
xmux.maxConcurrencyMax = QStringLiteral("32");
} else {
xmux.maxConnectionsMin = QStringLiteral("4");
xmux.maxConnectionsMax = QStringLiteral("4");
}
if (!obfs) {
c.srv.xhttp.xPadding.bytesMin = QStringLiteral("200");
c.srv.xhttp.xPadding.bytesMax = QStringLiteral("400");
}
c.name = QStringLiteral("xhttp/%1/%2/%3/%4/obfs-%5")
.arg(security, flowName, mode, placement,
obfs ? QStringLiteral("on") : QStringLiteral("off"));
cases.append(c);
}
}
}
}
}
}
MatrixCase bothXmux;
bothXmux.srv = baseConfig("xhttp", "tls", "");
bothXmux.srv.xhttp.xmux.maxConcurrencyMin = QStringLiteral("1");
bothXmux.srv.xhttp.xmux.maxConcurrencyMax = QStringLiteral("1");
bothXmux.srv.xhttp.xmux.maxConnectionsMin = QStringLiteral("1");
bothXmux.srv.xhttp.xmux.maxConnectionsMax = QStringLiteral("1");
bothXmux.allowedByUi = false;
bothXmux.name = QStringLiteral("xhttp/tls/xmux-concurrency-and-connections");
cases.append(bothXmux);
return cases;
}
inline amnezia::XrayServerInboundInputs inboundInputs(const amnezia::XrayServerConfig &srv)
{
amnezia::XrayServerInboundInputs in;
in.clients = QJsonArray { amnezia::XrayServerConfig::makeClientEntry(clientId(),
amnezia::xrayEffective::serverFlow(srv)) };
in.realityPrivateKey = key32('\x01');
in.realityShortId = shortId();
return in;
}
inline amnezia::XrayClientOutboundInputs outboundInputs()
{
amnezia::XrayClientOutboundInputs in;
in.serverAddress = QStringLiteral("203.0.113.10");
in.clientId = clientId();
in.realityPublicKey = key32('\x02');
in.realityShortId = shortId();
in.tlsPinnedPeerCertSha256 = tlsPin();
return in;
}
inline QJsonObject serverJson(const amnezia::XrayServerConfig &srv)
{
return srv.toServerInboundJson(inboundInputs(srv));
}
inline QJsonObject clientJson(const amnezia::XrayServerConfig &srv)
{
amnezia::XrayProtocolConfig cfg;
cfg.serverConfig = srv;
return cfg.toClientOutboundJson(outboundInputs());
}
inline QJsonObject firstInbound(const QJsonObject &config)
{
return config.value(QStringLiteral("inbounds")).toArray().first().toObject();
}
inline QJsonObject vlessOutbound(const QJsonObject &config)
{
for (const QJsonValue &v : config.value(QStringLiteral("outbounds")).toArray()) {
if (v.toObject().value(QStringLiteral("protocol")).toString() == QLatin1String("vless"))
return v.toObject();
}
return {};
}
inline QJsonObject streamOf(const QJsonObject &bound)
{
return bound.value(QStringLiteral("streamSettings")).toObject();
}
inline bool containsKeyDeep(const QJsonValue &value, const QString &key)
{
if (value.isObject()) {
const QJsonObject obj = value.toObject();
for (auto it = obj.begin(); it != obj.end(); ++it) {
if (it.key() == key || containsKeyDeep(it.value(), key))
return true;
}
} else if (value.isArray()) {
for (const QJsonValue &v : value.toArray()) {
if (containsKeyDeep(v, key))
return true;
}
}
return false;
}
inline QString compact(const QJsonObject &obj)
{
return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}
}
#endif
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 294 files, more files were not shown because too many files have changed in this diff. Show more