Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD

This commit is contained in:
vladimir.kuznetsov 2024-12-19 13:37:06 +07:00
commit 5961d4cefc
194 changed files with 7632 additions and 5277 deletions

39
.clang-format Normal file
View file

@ -0,0 +1,39 @@
BasedOnStyle: WebKit
AccessModifierOffset: '-4'
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: 'true'
AlignTrailingComments: 'true'
AllowAllArgumentsOnNextLine: 'true'
AllowAllParametersOfDeclarationOnNextLine: 'true'
AllowShortBlocksOnASingleLine: 'false'
AllowShortCaseLabelsOnASingleLine: 'true'
AllowShortEnumsOnASingleLine: 'false'
AllowShortFunctionsOnASingleLine: None
AlwaysBreakTemplateDeclarations: 'No'
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakConstructorInitializers: BeforeColon
ColumnLimit: '120'
CommentPragmas: '"^!|^:"'
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
ConstructorInitializerIndentWidth: '4'
ContinuationIndentWidth: '8'
IndentPPDirectives: BeforeHash
NamespaceIndentation: All
PenaltyExcessCharacter: '10'
PointerAlignment: Right
SortIncludes: 'true'
SpaceAfterTemplateKeyword: 'false'
Standard: Auto

20
.clang-format-ignore Normal file
View file

@ -0,0 +1,20 @@
/client/3rd
/client/3rd-prebuild
/client/android
/client/cmake
/client/core/serialization
/client/daemon
/client/fonts
/client/images
/client/ios
/client/mozilla
/client/platforms/dummy
/client/platforms/linux
/client/platforms/macos
/client/platforms/windows
/client/server_scripts
/client/translations
/deploy
/docs
/metadata
/service/src

View file

@ -16,6 +16,10 @@ jobs:
QT_VERSION: 6.6.2 QT_VERSION: 6.6.2
QIF_VERSION: 4.7 QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps: steps:
- name: 'Install Qt' - name: 'Install Qt'
@ -82,6 +86,10 @@ jobs:
QIF_VERSION: 4.7 QIF_VERSION: 4.7
BUILD_ARCH: 64 BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps: steps:
- name: 'Get sources' - name: 'Get sources'
@ -144,6 +152,10 @@ jobs:
CC: cc CC: cc
CXX: c++ CXX: c++
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps: steps:
- name: 'Setup xcode' - name: 'Setup xcode'
@ -178,7 +190,7 @@ jobs:
- name: 'Install go' - name: 'Install go'
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.20' go-version: '1.22.1'
cache: false cache: false
- name: 'Setup gomobile' - name: 'Setup gomobile'
@ -205,7 +217,11 @@ jobs:
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin" export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin"
export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos" export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos"
export PATH=$PATH:~/go/bin export PATH=$PATH:~/go/bin
sh deploy/build_ios.sh sh deploy/build_ios.sh | \
sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \
sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \
sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \
sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d'
env: env:
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }} IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }} IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
@ -235,12 +251,16 @@ jobs:
QT_VERSION: 6.4.3 QT_VERSION: 6.4.3
QIF_VERSION: 4.6 QIF_VERSION: 4.6
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps: steps:
- name: 'Setup xcode' - name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1 uses: maxim-lobanov/setup-xcode@v1
with: with:
xcode-version: '14.3.1' xcode-version: '15.4.0'
- name: 'Install Qt' - name: 'Install Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
@ -297,24 +317,28 @@ jobs:
env: env:
ANDROID_BUILD_PLATFORM: android-34 ANDROID_BUILD_PLATFORM: android-34
QT_VERSION: 6.6.2 QT_VERSION: 6.7.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps: steps:
- name: 'Install desktop Qt' - name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
host: 'linux' host: 'linux'
target: 'desktop' target: 'desktop'
arch: 'gcc_64' arch: 'linux_gcc_64'
modules: ${{ env.QT_MODULES }} modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }} dir: ${{ runner.temp }}
extra: '--external 7z --base ${{ env.QT_MIRROR }}' extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt' - name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
host: 'linux' host: 'linux'
@ -325,7 +349,7 @@ jobs:
extra: '--external 7z --base ${{ env.QT_MIRROR }}' extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86 Qt' - name: 'Install android_x86 Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
host: 'linux' host: 'linux'
@ -336,7 +360,7 @@ jobs:
extra: '--external 7z --base ${{ env.QT_MIRROR }}' extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Install android_armv7 Qt' - name: 'Install android_armv7 Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
host: 'linux' host: 'linux'
@ -347,7 +371,7 @@ jobs:
extra: '--external 7z --base ${{ env.QT_MIRROR }}' extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Install android_arm64_v8a Qt' - name: 'Install android_arm64_v8a Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
host: 'linux' host: 'linux'

View file

@ -16,6 +16,10 @@ jobs:
QT_VERSION: 6.4.1 QT_VERSION: 6.4.1
QIF_VERSION: 4.5 QIF_VERSION: 4.5
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
steps: steps:
- name: 'Install desktop Qt' - name: 'Install desktop Qt'

View file

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.7.0.0 project(${PROJECT} VERSION 4.8.2.4
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/" HOMEPAGE_URL "https://amnezia.org/"
) )
@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 57) set(APP_ANDROID_VERSION_CODE 2071)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")

View file

@ -1,30 +1,31 @@
# Amnezia VPN # Amnezia VPN
## _The best client for self-hosted VPN_
### _The best client for self-hosted VPN_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. ### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md)
![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)
<br> [Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_4.7.0.0_x64.exe"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/win.png" width="150" style="max-width: 100%;"></a> [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_4.7.0.0.dmg"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/mac.png" width="150" style="max-width: 100%;"></a>
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_Linux_4.7.0.0.tar.zip"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/lin.png" width="150" style="max-width: 100%;"></a>
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/tag/4.7.0.0"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/andr.png" width="150" style="max-width: 100%;"></a>
<br> ### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting)
<a href="https://play.google.com/store/search?q=amnezia+vpn&c=apps"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/play.png" width="150" style="max-width: 100%;"></a> > [!TIP]
<a href="https://apps.apple.com/us/app/amneziavpn/id1600529900"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/apl.png" width="150" style="max-width: 100%;"></a> > If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org).
<a href="https://amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-alt.svg" width="150" style="max-width: 100%;"></a>
[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) [All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
<br> <br/>
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
## Features ## Features
@ -37,7 +38,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
## Links ## Links
- [https://amnezia.org](https://amnezia.org) - project website - [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit - [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) - [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) - [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
@ -186,8 +188,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br> Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br> USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br> USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
## Acknowledgments ## Acknowledgments
This project is tested with BrowserStack. This project is tested with BrowserStack.

181
README_RU.md Normal file
View file

@ -0,0 +1,181 @@
# Amnezia VPN
### _Лучший клиент для создания VPN на собственном сервере_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский
[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
> [!TIP]
> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org).
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website-ru.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
<br/>
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
## Особенности
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS.
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
## Ссылки
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
## Технологии
AmneziaVPN использует несколько проектов с открытым исходным кодом:
- [OpenSSL](https://www.openssl.org/)
- [OpenVPN](https://openvpn.net/)
- [Shadowsocks](https://shadowsocks.org/)
- [Qt](https://www.qt.io/)
- [LibSsh](https://libssh.org)
- и другие...
## Проверка исходного кода
После клонирования репозитория обязательно загрузите все подмодули.
```bash
git submodule update --init --recursive
```
## Разработка
Хотите внести свой вклад? Добро пожаловать!
### Помощь с переводами
Загрузите самые актуальные файлы перевода.
Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations".
Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка.
Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом.
### Сборка исходного кода и деплой
Проверьте папку deploy для скриптов сборки.
### Как собрать iOS-приложение из исходного кода на MacOS
1. Убедитесь, что у вас установлен XCode версии 14 или выше.
2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
- MacOS
- iOS
- Модуль совместимости с Qt 5
- Qt Shader Tools
- Дополнительные библиотеки:
- Qt Image Formats
- Qt Multimedia
- Qt Remote Objects
3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь.
4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile:
```bash
export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
5. Соберите проект:
```bash
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
export QT_IOS_BIN=$QT_BIN_DIR
export PATH=$PATH:~/go/bin
mkdir build-ios
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
```
Замените <PATH-TO-QT-FOLDER> и <QT-VERSION> на ваши значения.
Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile:
```bash
export PATH=$(PATH):/path/to/GOPATH/bin
```
6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение.
Если сборка завершится с ошибкой:
```
make: ***
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
Error 1
```
Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM:
```
arch -arm64 brew install cmake
```
При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку.
## Как собрать Android-приложение
Сборка тестировалась на MacOS. Требования:
- JDK 11
- Android SDK 33
- CMake 3.25.0
Установите QT, QT Creator и Android Studio.
Настройте QT Creator:
- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`.
- Укажите путь к JDK 11.
- Укажите путь к Android SDK (`$ANDROID_HOME`)
Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере!
Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake.
Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK.
### Разработка Android-компонентов
После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` в качестве корневой.
Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения.
Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`<path>/client/android-build/.`).
## Лицензия
GPL v3.0
## Донаты
Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn)
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
## Благодарности
Этот проект тестируется с помощью BrowserStack.
Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта.

@ -1 +1 @@
Subproject commit c38a587fcda89bab4009560d36239fa8de74705e Subproject commit ba580dc5bd7784f7b1e110ff0365f3286e549a61

@ -1 +1 @@
Subproject commit dea6040996298e947d63fb172709e6abfec2ba93 Subproject commit 7c821a8d5c1ad5ad94e0763b4f25a875b5a6fe1b

View file

@ -1,25 +0,0 @@
include_directories(${CMAKE_CURRENT_LIST_DIR})
find_package(Qt6 REQUIRED COMPONENTS
Core Network
)
set(LIBS ${LIBS} Qt6::Core Qt6::Network)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/singleapplication.h
${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.h
)
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/singleapplication.cpp
${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.cpp
)
if(WIN32)
if(MSVC)
set(LIBS ${LIBS} Advapi32.lib)
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
set(LIBS ${LIBS} advapi32)
endif()
endif()

View file

@ -1,274 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include <QtCore/QElapsedTimer>
#include <QtCore/QByteArray>
#include <QtCore/QSharedMemory>
#include "singleapplication.h"
#include "singleapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program
* if another instance already exists
* @param argc
* @param argv
* @param allowSecondary Whether to enable secondary instance support
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData )
: app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) )
{
Q_D( SingleApplication );
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
// On Android and iOS since the library is not supported fallback to
// standard QApplication behaviour by simply returning at this point.
qWarning() << "SingleApplication is not supported on Android and iOS systems.";
return;
#endif
// Store the current mode of the program
d->options = options;
// Add any unique user data
if ( ! userData.isEmpty() )
d->addAppData( userData );
// Generating an application ID used for identifying the shared memory
// block and QLocalServer
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes
// attempting to attach at the same time
SingleApplicationPrivate::randomSleep();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the
// memory is deleted even after the process has crashed on Unix.
d->memory = new QSharedMemory( d->blockServerName );
d->memory->attach();
delete d->memory;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory = new QSharedMemory( d->blockServerName );
// Create a shared memory block
if( d->memory->create( sizeof( InstancesInfo ) )){
// Initialize the shared memory block
if( ! d->memory->lock() ){
qCritical() << "SingleApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
} else {
if( d->memory->error() == QSharedMemory::AlreadyExists ){
// Attempt to attach to the memory segment
if( ! d->memory->attach() ){
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
abortSafely();
}
if( ! d->memory->lock() ){
qCritical() << "SingleApplication: Unable to lock memory block after attach.";
abortSafely();
}
} else {
qCritical() << "SingleApplication: Unable to create block.";
abortSafely();
}
}
auto *inst = static_cast<InstancesInfo*>( d->memory->data() );
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
while( true ){
// If the shared memory block's checksum is valid continue
if( d->blockChecksum() == inst->checksum ) break;
// If more than 5s have elapsed, assume the primary instance crashed and
// assume it's position
if( time.elapsed() > 5000 ){
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
// Otherwise wait for a random period and try again. The random sleep here
// limits the probability of a collision between two racing apps and
// allows the app to initialise faster
if( ! d->memory->unlock() ){
qDebug() << "SingleApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory->errorString();
}
SingleApplicationPrivate::randomSleep();
if( ! d->memory->lock() ){
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if( inst->primary == false ){
d->startPrimary();
if( ! d->memory->unlock() ){
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
qDebug() << d->memory->errorString();
}
return;
}
// Check if another instance can be started
if( allowSecondary ){
d->startSecondary();
if( d->options & Mode::SecondaryNotification ){
d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance );
}
if( ! d->memory->unlock() ){
qDebug() << "SingleApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory->errorString();
}
return;
}
if( ! d->memory->unlock() ){
qDebug() << "SingleApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory->errorString();
}
d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance );
delete d;
::exit( EXIT_SUCCESS );
}
SingleApplication::~SingleApplication()
{
Q_D( SingleApplication );
delete d;
}
/**
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleApplication::isPrimary() const
{
Q_D( const SingleApplication );
return d->server != nullptr;
}
/**
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleApplication::isSecondary() const
{
Q_D( const SingleApplication );
return d->server == nullptr;
}
/**
* Allows you to identify an instance by returning unique consecutive instance
* ids. It is reset when the first (primary) instance of your app starts and
* only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleApplication::instanceId() const
{
Q_D( const SingleApplication );
return d->instanceNumber;
}
/**
* Returns the OS PID (Process Identifier) of the process running the primary
* instance. Especially useful when SingleApplication is coupled with OS.
* specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleApplication::primaryPid() const
{
Q_D( const SingleApplication );
return d->primaryPid();
}
/**
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleApplication::primaryUser() const
{
Q_D( const SingleApplication );
return d->primaryUser();
}
/**
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleApplication::currentUser() const
{
return SingleApplicationPrivate::getUsername();
}
/**
* Sends message to the Primary Instance.
* @param message The message to send.
* @param timeout the maximum timeout in milliseconds for blocking functions.
* @return true if the message was sent successfuly, false otherwise.
*/
bool SingleApplication::sendMessage( const QByteArray &message, int timeout )
{
Q_D( SingleApplication );
// Nobody to connect to
if( isPrimary() ) return false;
// Make sure the socket is connected
if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) )
return false;
d->socket->write( message );
bool dataWritten = d->socket->waitForBytesWritten( timeout );
d->socket->flush();
return dataWritten;
}
/**
* Cleans up the shared memory block and exits with a failure.
* This function halts program execution.
*/
void SingleApplication::abortSafely()
{
Q_D( SingleApplication );
qCritical() << "SingleApplication: " << d->memory->error() << d->memory->errorString();
delete d;
::exit( EXIT_FAILURE );
}
QStringList SingleApplication::userData() const
{
Q_D( const SingleApplication );
return d->appData();
}

View file

@ -1,154 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#ifndef SINGLE_APPLICATION_H
#define SINGLE_APPLICATION_H
#include <QtCore/QtGlobal>
#include <QtNetwork/QLocalSocket>
#ifndef QAPPLICATION_CLASS
#define QAPPLICATION_CLASS QApplication
#endif
#include QT_STRINGIFY(QAPPLICATION_CLASS)
class SingleApplicationPrivate;
/**
* @brief The SingleApplication class handles multiple instances of the same
* Application
* @see QCoreApplication
*/
class SingleApplication : public QAPPLICATION_CLASS
{
Q_OBJECT
using app_t = QAPPLICATION_CLASS;
public:
/**
* @brief Mode of operation of SingleApplication.
* Whether the block should be user-wide or system-wide and whether the
* primary instance should be notified when a secondary instance had been
* started.
* @note Operating system can restrict the shared memory blocks to the same
* user, in which case the User/System modes will have no effect and the
* block will be user wide.
* @enum
*/
enum Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
};
Q_DECLARE_FLAGS(Options, Mode)
/**
* @brief Intitializes a SingleApplication instance with argc command line
* arguments in argv
* @arg {int &} argc - Number of arguments in argv
* @arg {const char *[]} argv - Supplied command line arguments
* @arg {bool} allowSecondary - Whether to start the instance as secondary
* if there is already a primary instance.
* @arg {Mode} mode - Whether for the SingleApplication block to be applied
* User wide or System wide.
* @arg {int} timeout - Timeout to wait in milliseconds.
* @note argc and argv may be changed as Qt removes arguments that it
* recognizes
* @note Mode::SecondaryNotification only works if set on both the primary
* instance and the secondary instance.
* @note The timeout is just a hint for the maximum time of blocking
* operations. It does not guarantee that the SingleApplication
* initialisation will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario.
* @see See the corresponding QAPPLICATION_CLASS constructor for reference
*/
explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, const QString &userData = {} );
~SingleApplication() override;
/**
* @brief Returns if the instance is the primary instance
* @returns {bool}
*/
bool isPrimary() const;
/**
* @brief Returns if the instance is a secondary instance
* @returns {bool}
*/
bool isSecondary() const;
/**
* @brief Returns a unique identifier for the current instance
* @returns {qint32}
*/
quint32 instanceId() const;
/**
* @brief Returns the process ID (PID) of the primary instance
* @returns {qint64}
*/
qint64 primaryPid() const;
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser() const;
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser() const;
/**
* @brief Sends a message to the primary instance. Returns true on success.
* @param {int} timeout - Timeout for connecting
* @returns {bool}
* @note sendMessage() will return false if invoked from the primary
* instance.
*/
bool sendMessage( const QByteArray &message, int timeout = 100 );
/**
* @brief Get the set user data.
* @returns {QStringList}
*/
QStringList userData() const;
Q_SIGNALS:
void instanceStarted();
void receivedMessage( quint32 instanceId, QByteArray message );
private:
SingleApplicationPrivate *d_ptr;
Q_DECLARE_PRIVATE(SingleApplication)
void abortSafely();
};
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
#endif // SINGLE_APPLICATION_H

View file

@ -1,15 +0,0 @@
QT += core network
CONFIG += c++11
HEADERS += \
$$PWD/singleapplication.h \
$$PWD/singleapplication_p.h
SOURCES += $$PWD/singleapplication.cpp \
$$PWD/singleapplication_p.cpp
INCLUDEPATH += $$PWD
win32 {
msvc:LIBS += Advapi32.lib
gcc:LIBS += -ladvapi32
}

View file

@ -1,486 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This file is not part of the SingleApplication API. It is used purely as an
// implementation detail. This header file may change from version to
// version without notice, or may even be removed.
//
#include <cstdlib>
#include <cstddef>
#include <QtCore/QDir>
#include <QtCore/QThread>
#include <QtCore/QByteArray>
#include <QtCore/QDataStream>
#include <QtCore/QElapsedTimer>
#include <QtCore/QCryptographicHash>
#include <QtNetwork/QLocalServer>
#include <QtNetwork/QLocalSocket>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
#include <QtCore/QRandomGenerator>
#else
#include <QtCore/QDateTime>
#endif
#include "singleapplication.h"
#include "singleapplication_p.h"
#ifdef Q_OS_UNIX
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#endif
#ifdef Q_OS_WIN
#ifndef NOMINMAX
#define NOMINMAX 1
#endif
#include <windows.h>
#include <lmcons.h>
#endif
SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr )
: q_ptr( q_ptr )
{
server = nullptr;
socket = nullptr;
memory = nullptr;
instanceNumber = 0;
}
SingleApplicationPrivate::~SingleApplicationPrivate()
{
if( socket != nullptr ){
socket->close();
delete socket;
}
if( memory != nullptr ){
memory->lock();
auto *inst = static_cast<InstancesInfo*>(memory->data());
if( server != nullptr ){
server->close();
delete server;
inst->primary = false;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
}
memory->unlock();
delete memory;
}
}
QString SingleApplicationPrivate::getUsername()
{
#ifdef Q_OS_WIN
wchar_t username[UNLEN + 1];
// Specifies size of the buffer on input
DWORD usernameLength = UNLEN + 1;
if( GetUserNameW( username, &usernameLength ) )
return QString::fromWCharArray( username );
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
return QString::fromLocal8Bit( qgetenv( "USERNAME" ) );
#else
return qEnvironmentVariable( "USERNAME" );
#endif
#endif
#ifdef Q_OS_UNIX
QString username;
uid_t uid = geteuid();
struct passwd *pw = getpwuid( uid );
if( pw )
username = QString::fromLocal8Bit( pw->pw_name );
if ( username.isEmpty() ){
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
username = QString::fromLocal8Bit( qgetenv( "USER" ) );
#else
username = qEnvironmentVariable( "USER" );
#endif
}
return username;
#endif
}
void SingleApplicationPrivate::genBlockServerName()
{
QCryptographicHash appData( QCryptographicHash::Sha256 );
appData.addData( "SingleApplication", 17 );
appData.addData( SingleApplication::app_t::applicationName().toUtf8() );
appData.addData( SingleApplication::app_t::organizationName().toUtf8() );
appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() );
if ( ! appDataList.isEmpty() )
appData.addData( appDataList.join( "" ).toUtf8() );
if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){
appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() );
}
if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){
#ifdef Q_OS_WIN
appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() );
#else
appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() );
#endif
}
// User level block requires a user specific data in the hash
if( options & SingleApplication::Mode::User ){
appData.addData( getUsername().toUtf8() );
}
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with
// server naming requirements.
blockServerName = appData.result().toBase64().replace("/", "_");
}
void SingleApplicationPrivate::initializeMemoryBlock() const
{
auto *inst = static_cast<InstancesInfo*>( memory->data() );
inst->primary = false;
inst->secondary = 0;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
}
void SingleApplicationPrivate::startPrimary()
{
// Reset the number of connections
auto *inst = static_cast <InstancesInfo*>( memory->data() );
inst->primary = true;
inst->primaryPid = QCoreApplication::applicationPid();
qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) );
inst->checksum = blockChecksum();
instanceNumber = 0;
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer( blockServerName );
server = new QLocalServer();
// Restrict access to the socket according to the
// SingleApplication::Mode::User flag on User level or no restrictions
if( options & SingleApplication::Mode::User ){
server->setSocketOptions( QLocalServer::UserAccessOption );
} else {
server->setSocketOptions( QLocalServer::WorldAccessOption );
}
server->listen( blockServerName );
QObject::connect(
server,
&QLocalServer::newConnection,
this,
&SingleApplicationPrivate::slotConnectionEstablished
);
}
void SingleApplicationPrivate::startSecondary()
{
auto *inst = static_cast <InstancesInfo*>( memory->data() );
inst->secondary += 1;
inst->checksum = blockChecksum();
instanceNumber = inst->secondary;
}
bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType )
{
QElapsedTimer time;
time.start();
// Connect to the Local Server of the Primary Instance if not already
// connected.
if( socket == nullptr ){
socket = new QLocalSocket();
}
if( socket->state() == QLocalSocket::ConnectedState ) return true;
if( socket->state() != QLocalSocket::ConnectedState ){
while( true ){
randomSleep();
if( socket->state() != QLocalSocket::ConnectingState )
socket->connectToServer( blockServerName );
if( socket->state() == QLocalSocket::ConnectingState ){
socket->waitForConnected( static_cast<int>(msecs - time.elapsed()) );
}
// If connected break out of the loop
if( socket->state() == QLocalSocket::ConnectedState ) break;
// If elapsed time since start is longer than the method timeout return
if( time.elapsed() >= msecs ) return false;
}
}
// Initialisation message according to the SingleApplication protocol
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
writeStream.setVersion(QDataStream::Qt_5_6);
#endif
writeStream << blockServerName.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
#else
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
#endif
writeStream << checksum;
// The header indicates the message length that follows
QByteArray header;
QDataStream headerStream(&header, QIODevice::WriteOnly);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
headerStream.setVersion(QDataStream::Qt_5_6);
#endif
headerStream << static_cast <quint64>( initMsg.length() );
socket->write( header );
socket->write( initMsg );
bool result = socket->waitForBytesWritten( static_cast<int>(msecs - time.elapsed()) );
socket->flush();
return result;
}
quint16 SingleApplicationPrivate::blockChecksum() const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory->constData()), offsetof(InstancesInfo, checksum)));
#else
quint16 checksum = qChecksum(static_cast<const char*>(memory->constData()), offsetof(InstancesInfo, checksum));
#endif
return checksum;
}
qint64 SingleApplicationPrivate::primaryPid() const
{
qint64 pid;
memory->lock();
auto *inst = static_cast<InstancesInfo*>( memory->data() );
pid = inst->primaryPid;
memory->unlock();
return pid;
}
QString SingleApplicationPrivate::primaryUser() const
{
QByteArray username;
memory->lock();
auto *inst = static_cast<InstancesInfo*>( memory->data() );
username = inst->primaryUser;
memory->unlock();
return QString::fromUtf8( username );
}
/**
* @brief Executed when a connection has been made to the LocalServer
*/
void SingleApplicationPrivate::slotConnectionEstablished()
{
QLocalSocket *nextConnSocket = server->nextPendingConnection();
connectionMap.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
[nextConnSocket, this](){
auto &info = connectionMap[nextConnSocket];
Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId );
}
);
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed,
[nextConnSocket, this](){
connectionMap.remove(nextConnSocket);
}
);
QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
[nextConnSocket, this](){
auto &info = connectionMap[nextConnSocket];
switch(info.stage){
case StageHeader:
readInitMessageHeader(nextConnSocket);
break;
case StageBody:
readInitMessageBody(nextConnSocket);
break;
case StageConnected:
Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId );
break;
default:
break;
};
}
);
}
void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
{
if (!connectionMap.contains( sock )){
return;
}
if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){
return;
}
QDataStream headerStream( sock );
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
headerStream.setVersion( QDataStream::Qt_5_6 );
#endif
// Read the header to know the message length
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap[sock];
info.stage = StageBody;
info.msgLen = msgLen;
if ( sock->bytesAvailable() >= (qint64) msgLen ){
readInitMessageBody( sock );
}
}
void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
{
Q_Q(SingleApplication);
if (!connectionMap.contains( sock )){
return;
}
ConnectionInfo &info = connectionMap[sock];
if( sock->bytesAvailable() < ( qint64 )info.msgLen ){
return;
}
// Read the message body
QByteArray msgBytes = sock->read(info.msgLen);
QDataStream readStream(msgBytes);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
readStream.setVersion( QDataStream::Qt_5_6 );
#endif
// server name
QByteArray latin1Name;
readStream >> latin1Name;
// connection type
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast <ConnectionType>( connTypeVal );
// instance id
quint32 instanceId = 0;
readStream >> instanceId;
// checksum
quint16 msgChecksum = 0;
readStream >> msgChecksum;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
#else
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
#endif
bool isValid = readStream.status() == QDataStream::Ok &&
QLatin1String(latin1Name) == blockServerName &&
msgChecksum == actualChecksum;
if( !isValid ){
sock->close();
return;
}
info.instanceId = instanceId;
info.stage = StageConnected;
if( connectionType == NewInstance ||
( connectionType == SecondaryInstance &&
options & SingleApplication::Mode::SecondaryNotification ) )
{
Q_EMIT q->instanceStarted();
}
if (sock->bytesAvailable() > 0){
Q_EMIT this->slotDataAvailable( sock, instanceId );
}
}
void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId )
{
Q_Q(SingleApplication);
Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() );
}
void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId )
{
if( closedSocket->bytesAvailable() > 0 )
Q_EMIT slotDataAvailable( closedSocket, instanceId );
}
void SingleApplicationPrivate::randomSleep()
{
#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 )
QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u ));
#else
qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() );
QThread::msleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ));
#endif
}
void SingleApplicationPrivate::addAppData(const QString &data)
{
appDataList.push_back(data);
}
QStringList SingleApplicationPrivate::appData() const
{
return appDataList;
}

View file

@ -1,104 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This file is not part of the SingleApplication API. It is used purely as an
// implementation detail. This header file may change from version to
// version without notice, or may even be removed.
//
#ifndef SINGLEAPPLICATION_P_H
#define SINGLEAPPLICATION_P_H
#include <QtCore/QSharedMemory>
#include <QtNetwork/QLocalServer>
#include <QtNetwork/QLocalSocket>
#include "singleapplication.h"
struct InstancesInfo {
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum; // Must be the last field
};
struct ConnectionInfo {
qint64 msgLen = 0;
quint32 instanceId = 0;
quint8 stage = 0;
};
class SingleApplicationPrivate : public QObject {
Q_OBJECT
public:
enum ConnectionType : quint8 {
InvalidConnection = 0,
NewInstance = 1,
SecondaryInstance = 2,
Reconnect = 3
};
enum ConnectionStage : quint8 {
StageHeader = 0,
StageBody = 1,
StageConnected = 2,
};
Q_DECLARE_PUBLIC(SingleApplication)
SingleApplicationPrivate( SingleApplication *q_ptr );
~SingleApplicationPrivate() override;
static QString getUsername();
void genBlockServerName();
void initializeMemoryBlock() const;
void startPrimary();
void startSecondary();
bool connectToPrimary( int msecs, ConnectionType connectionType );
quint16 blockChecksum() const;
qint64 primaryPid() const;
QString primaryUser() const;
void readInitMessageHeader(QLocalSocket *socket);
void readInitMessageBody(QLocalSocket *socket);
static void randomSleep();
void addAppData(const QString &data);
QStringList appData() const;
SingleApplication *q_ptr;
QSharedMemory *memory;
QLocalSocket *socket;
QLocalServer *server;
quint32 instanceNumber;
QString blockServerName;
SingleApplication::Options options;
QMap<QLocalSocket*, ConnectionInfo> connectionMap;
QStringList appDataList;
public Q_SLOTS:
void slotConnectionEstablished();
void slotDataAvailable( QLocalSocket*, quint32 );
void slotClientConnectionClosed( QLocalSocket*, quint32 );
};
#endif // SINGLEAPPLICATION_P_H

@ -1 +1 @@
Subproject commit 74776e2a3e2d98d19943e0968901c5b5e04cc1bd Subproject commit 7460df6a978669290de5b56c2d98b199b61c3f88

View file

@ -25,7 +25,11 @@ execute_process(
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
if(IOS) if(IOS)
set(PACKAGES ${PACKAGES} Multimedia) set(PACKAGES ${PACKAGES} Multimedia)
@ -58,6 +62,7 @@ qt_add_executable(${PROJECT} MANUAL_FINALIZATION)
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
endif() endif()
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc) qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
@ -110,6 +115,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake)
include_directories( include_directories(
${CMAKE_CURRENT_LIST_DIR}/../ipc ${CMAKE_CURRENT_LIST_DIR}/../ipc
${CMAKE_CURRENT_LIST_DIR}/../common/logger
${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}
) )
@ -131,7 +137,6 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h
${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h
${CMAKE_CURRENT_LIST_DIR}/ui/pages.h ${CMAKE_CURRENT_LIST_DIR}/ui/pages.h
${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h
${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h
${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h
${CMAKE_CURRENT_BINARY_DIR}/version.h ${CMAKE_CURRENT_BINARY_DIR}/version.h
@ -140,6 +145,7 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h ${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h ${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h ${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h
) )
# Mozilla headres # Mozilla headres
@ -190,6 +196,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp
) )
# Mozilla sources # Mozilla sources

View file

@ -10,6 +10,8 @@
#include <QTextDocument> #include <QTextDocument>
#include <QTimer> #include <QTimer>
#include <QTranslator> #include <QTranslator>
#include <QLocalSocket>
#include <QLocalServer>
#include "logger.h" #include "logger.h"
#include "ui/models/installedAppsModel.h" #include "ui/models/installedAppsModel.h"
@ -28,13 +30,7 @@
#include <AmneziaVPN-Swift.h> #include <AmneziaVPN-Swift.h>
#endif #endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv)
#else
AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options, int timeout,
const QString &userData)
: SingleApplication(argc, argv, allowSecondary, options, timeout, userData)
#endif
{ {
setQuitOnLastWindowClosed(false); setQuitOnLastWindowClosed(false);
@ -115,10 +111,11 @@ void AmneziaApplication::init()
qFatal("Android controller initialization failed"); qFatal("Android controller initialization failed");
} }
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) { connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) {
m_pageController->goToPageHome(); emit m_pageController->goToPageHome();
m_importController->extractConfigFromData(data); m_importController->extractConfigFromData(data);
m_pageController->goToPageViewConfig(); data.clear();
emit m_pageController->goToPageViewConfig();
}); });
m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider);
@ -126,16 +123,16 @@ void AmneziaApplication::init()
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
IosController::Instance()->initialize(); IosController::Instance()->initialize();
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) { connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
m_pageController->goToPageHome(); emit m_pageController->goToPageHome();
m_importController->extractConfigFromData(data); m_importController->extractConfigFromData(data);
m_pageController->goToPageViewConfig(); emit m_pageController->goToPageViewConfig();
}); });
connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) { connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) {
m_pageController->goToPageHome(); emit m_pageController->goToPageHome();
m_pageController->goToPageSettingsBackup(); m_pageController->goToPageSettingsBackup();
m_settingsController->importBackupFromOutside(filePath); emit m_settingsController->importBackupFromOutside(filePath);
}); });
QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); });
@ -164,7 +161,7 @@ void AmneziaApplication::init()
bool enabled = m_settings->isSaveLogs(); bool enabled = m_settings->isSaveLogs();
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
if (enabled) { if (enabled) {
if (!Logger::init()) { if (!Logger::init(false)) {
qWarning() << "Initialization of debug subsystem failed"; qWarning() << "Initialization of debug subsystem failed";
} }
} }
@ -180,16 +177,6 @@ void AmneziaApplication::init()
m_pageController->showOnStartup(); m_pageController->showOnStartup();
#endif #endif
// TODO - fix
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (isPrimary()) {
QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() {
qDebug() << "Secondary instance started, showing this window instead";
emit m_pageController->raiseMainWindow();
});
}
#endif
// Android TextArea clipboard workaround // Android TextArea clipboard workaround
// Text from TextArea always has "text/html" mime-type: // Text from TextArea always has "text/html" mime-type:
// /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1865 // /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1865
@ -294,6 +281,24 @@ bool AmneziaApplication::parseCommands()
return true; return true;
} }
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
void AmneziaApplication::startLocalServer() {
const QString serverName("AmneziaVPNInstance");
QLocalServer::removeServer(serverName);
QLocalServer* server = new QLocalServer(this);
server->listen(serverName);
QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() {
if (server) {
QLocalSocket* clientConnection = server->nextPendingConnection();
clientConnection->deleteLater();
}
emit m_pageController->raiseMainWindow();
});
}
#endif
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
{ {
return m_engine; return m_engine;

View file

@ -53,22 +53,14 @@
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#define AMNEZIA_BASE_CLASS QGuiApplication #define AMNEZIA_BASE_CLASS QGuiApplication
#else #else
#define AMNEZIA_BASE_CLASS SingleApplication #define AMNEZIA_BASE_CLASS QApplication
#define QAPPLICATION_CLASS QApplication
#include "singleapplication.h"
#endif #endif
class AmneziaApplication : public AMNEZIA_BASE_CLASS class AmneziaApplication : public AMNEZIA_BASE_CLASS
{ {
Q_OBJECT Q_OBJECT
public: public:
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
AmneziaApplication(int &argc, char *argv[]); AmneziaApplication(int &argc, char *argv[]);
#else
AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false,
SingleApplication::Options options = SingleApplication::User, int timeout = 1000,
const QString &userData = {});
#endif
virtual ~AmneziaApplication(); virtual ~AmneziaApplication();
void init(); void init();
@ -78,6 +70,10 @@ public:
void updateTranslator(const QLocale &locale); void updateTranslator(const QLocale &locale);
bool parseCommands(); bool parseCommands();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
void startLocalServer();
#endif
QQmlApplicationEngine *qmlEngine() const; QQmlApplicationEngine *qmlEngine() const;
QNetworkAccessManager *manager() { return m_nam; } QNetworkAccessManager *manager() { return m_nam; }

View file

@ -3,7 +3,6 @@
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.amnezia.vpn"
android:versionName="-- %%INSERT_VERSION_NAME%% --" android:versionName="-- %%INSERT_VERSION_NAME%% --"
android:versionCode="-- %%INSERT_VERSION_CODE%% --" android:versionCode="-- %%INSERT_VERSION_CODE%% --"
android:installLocation="auto"> android:installLocation="auto">
@ -21,7 +20,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- To request network state --> <!-- To request network state -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -46,7 +45,7 @@
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc" |fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="stateUnchanged|adjustResize"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@ -68,9 +67,6 @@
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" /> android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
</activity> </activity>
<activity <activity
@ -88,6 +84,13 @@
android:exported="false" android:exported="false"
android:theme="@style/Translucent" /> android:theme="@style/Translucent" />
<activity android:name=".AuthActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:exported="false"
android:theme="@style/Translucent" />
<activity <activity
android:name=".ImportConfigActivity" android:name=".ImportConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"

View file

@ -1,81 +1,21 @@
package org.amnezia.vpn.protocol.awg package org.amnezia.vpn.protocol.awg
import org.amnezia.vpn.protocol.wireguard.Wireguard import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.protocol.wireguard.WireguardConfig
import org.json.JSONObject import org.json.JSONObject
/**
* Config example:
* {
* "protocol": "awg",
* "description": "Server 1",
* "dns1": "1.1.1.1",
* "dns2": "1.0.0.1",
* "hostName": "100.100.100.0",
* "splitTunnelSites": [
* ],
* "splitTunnelType": 0,
* "awg_config_data": {
* "H1": "969537490",
* "H2": "481688153",
* "H3": "2049399200",
* "H4": "52029755",
* "Jc": "3",
* "Jmax": "1000",
* "Jmin": "50",
* "S1": "49",
* "S2": "60",
* "client_ip": "10.8.1.1",
* "hostName": "100.100.100.0",
* "port": 12345,
* "client_pub_key": "clientPublicKeyBase64",
* "client_priv_key": "privateKeyBase64",
* "psk_key": "presharedKeyBase64",
* "server_pub_key": "publicKeyBase64",
* "config": "[Interface]
* Address = 10.8.1.1/32
* DNS = 1.1.1.1, 1.0.0.1
* PrivateKey = privateKeyBase64
* Jc = 3
* Jmin = 50
* Jmax = 1000
* S1 = 49
* S2 = 60
* H1 = 969537490
* H2 = 481688153
* H3 = 2049399200
* H4 = 52029755
*
* [Peer]
* PublicKey = publicKeyBase64
* PresharedKey = presharedKeyBase64
* AllowedIPs = 0.0.0.0/0, ::/0
* Endpoint = 100.100.100.0:12345
* PersistentKeepalive = 25
* "
* }
* }
*/
class Awg : Wireguard() { class Awg : Wireguard() {
override val ifName: String = "awg0" override val ifName: String = "awg0"
override fun parseConfig(config: JSONObject): AwgConfig { override fun parseConfig(config: JSONObject): WireguardConfig {
val configDataJson = config.getJSONObject("awg_config_data") val configData = config.getJSONObject("awg_config_data")
val configData = parseConfigData(configDataJson.getString("config")) return WireguardConfig.build {
return AwgConfig.build { setUseProtocolExtension(true)
configWireguard(configData, configDataJson) configExtensionParameters(configData)
configWireguard(config, configData)
configSplitTunneling(config) configSplitTunneling(config)
configAppSplitTunneling(config) configAppSplitTunneling(config)
configData["Jc"]?.let { setJc(it.toInt()) }
configData["Jmin"]?.let { setJmin(it.toInt()) }
configData["Jmax"]?.let { setJmax(it.toInt()) }
configData["S1"]?.let { setS1(it.toInt()) }
configData["S2"]?.let { setS2(it.toInt()) }
configData["H1"]?.let { setH1(it.toLong()) }
configData["H2"]?.let { setH2(it.toLong()) }
configData["H3"]?.let { setH3(it.toLong()) }
configData["H4"]?.let { setH4(it.toLong()) }
} }
} }
} }

View file

@ -1,108 +0,0 @@
package org.amnezia.vpn.protocol.awg
import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.wireguard.WireguardConfig
class AwgConfig private constructor(
wireguardConfigBuilder: WireguardConfig.Builder,
val jc: Int,
val jmin: Int,
val jmax: Int,
val s1: Int,
val s2: Int,
val h1: Long,
val h2: Long,
val h3: Long,
val h4: Long
) : WireguardConfig(wireguardConfigBuilder) {
private constructor(builder: Builder) : this(
builder,
builder.jc,
builder.jmin,
builder.jmax,
builder.s1,
builder.s2,
builder.h1,
builder.h2,
builder.h3,
builder.h4
)
override fun appendDeviceLine(sb: StringBuilder) = with(sb) {
super.appendDeviceLine(this)
appendLine("jc=$jc")
appendLine("jmin=$jmin")
appendLine("jmax=$jmax")
appendLine("s1=$s1")
appendLine("s2=$s2")
appendLine("h1=$h1")
appendLine("h2=$h2")
appendLine("h3=$h3")
appendLine("h4=$h4")
}
class Builder : WireguardConfig.Builder() {
private var _jc: Int? = null
internal var jc: Int
get() = _jc ?: throw BadConfigException("AWG: parameter jc is undefined")
private set(value) { _jc = value }
private var _jmin: Int? = null
internal var jmin: Int
get() = _jmin ?: throw BadConfigException("AWG: parameter jmin is undefined")
private set(value) { _jmin = value }
private var _jmax: Int? = null
internal var jmax: Int
get() = _jmax ?: throw BadConfigException("AWG: parameter jmax is undefined")
private set(value) { _jmax = value }
private var _s1: Int? = null
internal var s1: Int
get() = _s1 ?: throw BadConfigException("AWG: parameter s1 is undefined")
private set(value) { _s1 = value }
private var _s2: Int? = null
internal var s2: Int
get() = _s2 ?: throw BadConfigException("AWG: parameter s2 is undefined")
private set(value) { _s2 = value }
private var _h1: Long? = null
internal var h1: Long
get() = _h1 ?: throw BadConfigException("AWG: parameter h1 is undefined")
private set(value) { _h1 = value }
private var _h2: Long? = null
internal var h2: Long
get() = _h2 ?: throw BadConfigException("AWG: parameter h2 is undefined")
private set(value) { _h2 = value }
private var _h3: Long? = null
internal var h3: Long
get() = _h3 ?: throw BadConfigException("AWG: parameter h3 is undefined")
private set(value) { _h3 = value }
private var _h4: Long? = null
internal var h4: Long
get() = _h4 ?: throw BadConfigException("AWG: parameter h4 is undefined")
private set(value) { _h4 = value }
fun setJc(jc: Int) = apply { this.jc = jc }
fun setJmin(jmin: Int) = apply { this.jmin = jmin }
fun setJmax(jmax: Int) = apply { this.jmax = jmax }
fun setS1(s1: Int) = apply { this.s1 = s1 }
fun setS2(s2: Int) = apply { this.s2 = s2 }
fun setH1(h1: Long) = apply { this.h1 = h1 }
fun setH2(h2: Long) = apply { this.h2 = h2 }
fun setH3(h3: Long) = apply { this.h3 = h3 }
fun setH4(h4: Long) = apply { this.h4 = h4 }
override fun build(): AwgConfig = configBuild().run { AwgConfig(this@Builder) }
}
companion object {
inline fun build(block: Builder.() -> Unit): AwgConfig = Builder().apply(block).build()
}
}

View file

@ -3,3 +3,6 @@
// android.bundle.enableUncompressedNativeLibs is deprecated // android.bundle.enableUncompressedNativeLibs is deprecated
// disable adding gradle property android.bundle.enableUncompressedNativeLibs by androiddeployqt // disable adding gradle property android.bundle.enableUncompressedNativeLibs by androiddeployqt
useLegacyPackaging useLegacyPackaging
// package name for androiddeployqt
namespace = "org.amnezia.vpn"

View file

@ -115,9 +115,11 @@ dependencies {
implementation(project(":xray")) implementation(project(":xray"))
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.activity) implementation(libs.androidx.activity)
implementation(libs.androidx.fragment)
implementation(libs.kotlinx.coroutines) implementation(libs.kotlinx.coroutines)
implementation(libs.kotlinx.serialization.protobuf) implementation(libs.kotlinx.serialization.protobuf)
implementation(libs.bundles.androidx.camera) implementation(libs.bundles.androidx.camera)
implementation(libs.google.mlkit) implementation(libs.google.mlkit)
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore)
implementation(libs.androidx.biometric)
} }

View file

@ -3,40 +3,16 @@ package org.amnezia.vpn.protocol.cloak
import android.util.Base64 import android.util.Base64
import net.openvpn.ovpn3.ClientAPI_Config import net.openvpn.ovpn3.ClientAPI_Config
import org.amnezia.vpn.protocol.openvpn.OpenVpn import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
import org.json.JSONObject import org.json.JSONObject
/**
* Config Example:
* {
* "protocol": "cloak",
* "description": "Server 1",
* "dns1": "1.1.1.1",
* "dns2": "1.0.0.1",
* "hostName": "100.100.100.0",
* "splitTunnelSites": [
* ],
* "splitTunnelType": 0,
* "openvpn_config_data": {
* "config": "openVpnConfig"
* }
* "cloak_config_data": {
* "BrowserSig": "chrome",
* "EncryptionMethod": "aes-gcm",
* "NumConn": 1,
* "ProxyMethod": "openvpn",
* "PublicKey": "PublicKey=",
* "RemoteHost": "100.100.100.0",
* "RemotePort": "443",
* "ServerName": "servername",
* "StreamTimeout": 300,
* "Transport": "direct",
* "UID": "UID="
* }
* }
*/
class Cloak : OpenVpn() { class Cloak : OpenVpn() {
override fun internalInit() {
super.internalInit()
if (!isInitialized) loadSharedLibrary(context, "ck-ovpn-plugin")
}
override fun parseConfig(config: JSONObject): ClientAPI_Config { override fun parseConfig(config: JSONObject): ClientAPI_Config {
val openVpnConfig = ClientAPI_Config() val openVpnConfig = ClientAPI_Config()

View file

@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false
# For development copy and set local values for these parameters in local.properties # For development copy and set local values for these parameters in local.properties
#androidCompileSdkVersion=android-34 #androidCompileSdkVersion=android-34
#androidBuildToolsVersion=34.0.0 #androidBuildToolsVersion=34.0.0
#qtMinSdkVersion=24 #qtMinSdkVersion=26
#qtTargetSdkVersion=34 #qtTargetSdkVersion=34
#androidNdkVersion=26.1.10909125 #androidNdkVersion=26.1.10909125
#qtTargetAbiList=x86_64 #qtTargetAbiList=x86_64

View file

@ -1,24 +1,28 @@
[versions] [versions]
agp = "8.2.0" agp = "8.5.2"
kotlin = "1.9.20" kotlin = "1.9.24"
androidx-core = "1.12.0" androidx-core = "1.13.1"
androidx-activity = "1.8.1" androidx-activity = "1.9.1"
androidx-annotation = "1.7.0" androidx-annotation = "1.8.2"
androidx-camera = "1.3.0" androidx-biometric = "1.2.0-alpha05"
androidx-camera = "1.3.4"
androidx-fragment = "1.8.2"
androidx-security-crypto = "1.1.0-alpha06" androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.0-beta01" androidx-datastore = "1.1.1"
kotlinx-coroutines = "1.7.3" kotlinx-coroutines = "1.8.1"
kotlinx-serialization = "1.6.3" kotlinx-serialization = "1.6.3"
google-mlkit = "17.2.0" google-mlkit = "17.3.0"
[libraries] [libraries]
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-biometric = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometric" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" } androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" } androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" } androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" } androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" }
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" } androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }

Binary file not shown.

View file

@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

View file

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View file

@ -11,28 +11,12 @@ import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.getLocalNetworks import org.amnezia.vpn.util.net.getLocalNetworks
import org.amnezia.vpn.util.net.parseInetAddress import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject import org.json.JSONObject
/**
* Config Example:
* {
* "protocol": "openvpn",
* "description": "Server 1",
* "dns1": "1.1.1.1",
* "dns2": "1.0.0.1",
* "hostName": "100.100.100.0",
* "splitTunnelSites": [
* ],
* "splitTunnelType": 0,
* "openvpn_config_data": {
* "config": "openVpnConfig"
* }
* }
*/
open class OpenVpn : Protocol() { open class OpenVpn : Protocol() {
private var openVpnClient: OpenVpnClient? = null private var openVpnClient: OpenVpnClient? = null
@ -51,14 +35,17 @@ open class OpenVpn : Protocol() {
} }
override fun internalInit() { override fun internalInit() {
if (!isInitialized) loadSharedLibrary(context, "ovpn3") if (!isInitialized) {
loadSharedLibrary(context, "ovpn3")
loadSharedLibrary(context, "ovpnutil")
}
if (this::scope.isInitialized) { if (this::scope.isInitialized) {
scope.cancel() scope.cancel()
} }
scope = CoroutineScope(Dispatchers.IO) scope = CoroutineScope(Dispatchers.IO)
} }
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
val configBuilder = OpenVpnConfig.Builder() val configBuilder = OpenVpnConfig.Builder()
openVpnClient = OpenVpnClient( openVpnClient = OpenVpnClient(

View file

@ -2,7 +2,6 @@ package org.amnezia.vpn.protocol
sealed class ProtocolException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) sealed class ProtocolException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
class LoadLibraryException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
class BadConfigException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) class BadConfigException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
class VpnStartException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) class VpnStartException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)

View file

@ -1,6 +1,5 @@
package org.amnezia.vpn.protocol package org.amnezia.vpn.protocol
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.IpPrefix import android.net.IpPrefix
import android.net.VpnService import android.net.VpnService
@ -8,9 +7,6 @@ import android.net.VpnService.Builder
import android.os.Build import android.os.Build
import android.system.OsConstants import android.system.OsConstants
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipFile
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.InetNetwork
@ -42,7 +38,7 @@ abstract class Protocol {
protected abstract fun internalInit() protected abstract fun internalInit()
abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) abstract suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean)
abstract fun stopVpn() abstract fun stopVpn()
@ -158,60 +154,6 @@ abstract class Protocol {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
vpnBuilder.setMetered(false) vpnBuilder.setMetered(false)
} }
companion object {
private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean {
Log.d(TAG, "Extracting library: $libraryName")
val apks = hashSetOf<String>()
context.applicationInfo.run {
sourceDir?.let { apks += it }
splitSourceDirs?.let { apks += it }
}
for (abi in Build.SUPPORTED_ABIS) {
for (apk in apks) {
ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile ->
val mappedName = System.mapLibraryName(libraryName)
val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator)
val zipEntry = zipFile.getEntry(libraryZipPath)
zipEntry?.let {
Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}")
FileOutputStream(destination).use { outStream ->
zipFile.getInputStream(zipEntry).use { inStream ->
inStream.copyTo(outStream, 32 * 1024)
outStream.fd.sync()
}
}
}
return true
}
}
}
return false
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
fun loadSharedLibrary(context: Context, libraryName: String) {
Log.d(TAG, "Loading library: $libraryName")
try {
System.loadLibrary(libraryName)
return
} catch (_: UnsatisfiedLinkError) {
Log.d(TAG, "Failed to load library, try to extract it from apk")
}
var tempFile: File? = null
try {
tempFile = File.createTempFile("lib", ".so", context.codeCacheDir)
if (extractLibrary(context, libraryName, tempFile)) {
System.load(tempFile.absolutePath)
return
}
} catch (e: Exception) {
throw LoadLibraryException("Failed to load library apk: $libraryName", e)
} finally {
tempFile?.delete()
}
}
}
} }
private fun VpnService.Builder.addAddress(addr: InetNetwork) = addAddress(addr.address, addr.mask) private fun VpnService.Builder.addAddress(addr: InetNetwork) = addAddress(addr.address, addr.mask)

View file

@ -21,5 +21,5 @@ android {
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) api(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
} }

View file

@ -3,7 +3,6 @@
<!-- DO NOT EDIT THIS: This file is populated automatically by the deployment tool. --> <!-- DO NOT EDIT THIS: This file is populated automatically by the deployment tool. -->
<array name="bundled_libs"> <array name="bundled_libs">
<!-- %%INSERT_EXTRA_LIBS%% -->
</array> </array>
<array name="qt_libs"> <array name="qt_libs">

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="black">#FF0E0E11</color>
<style name="NoActionBar"> <style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
</style> </style>

View file

@ -22,7 +22,7 @@ dependencyResolutionManagement {
includeBuild("./gradle/plugins") includeBuild("./gradle/plugins")
plugins { plugins {
id("com.android.settings") version "8.2.0" id("com.android.settings") version "8.5.2"
id("settings-property-delegate") id("settings-property-delegate")
} }

View file

@ -21,6 +21,7 @@ import android.os.Looper
import android.os.Message import android.os.Message
import android.os.Messenger import android.os.Messenger
import android.provider.Settings import android.provider.Settings
import android.view.MotionEvent
import android.view.WindowManager.LayoutParams import android.view.WindowManager.LayoutParams
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
@ -43,6 +44,7 @@ import kotlinx.coroutines.withContext
import org.amnezia.vpn.protocol.getStatistics import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.Prefs
import org.json.JSONException import org.json.JSONException
@ -157,7 +159,12 @@ class AmneziaActivity : QtActivity() {
*/ */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity: $intent") Log.d(TAG, "Create Amnezia activity")
loadLibs()
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = getColor(R.color.black)
}
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val proto = mainScope.async(Dispatchers.IO) { val proto = mainScope.async(Dispatchers.IO) {
VpnStateStore.getVpnState().vpnProto VpnStateStore.getVpnState().vpnProto
@ -175,6 +182,17 @@ class AmneziaActivity : QtActivity() {
runBlocking { vpnProto = proto.await() } runBlocking { vpnProto = proto.await() }
} }
private fun loadLibs() {
listOf(
"rsapss",
"crypto_3",
"ssl_3",
"ssh"
).forEach {
loadSharedLibrary(this.applicationContext, it)
}
}
private fun registerBroadcastReceivers() { private fun registerBroadcastReceivers() {
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver( registerBroadcastReceiver(
@ -183,7 +201,7 @@ class AmneziaActivity : QtActivity() {
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
) )
) { ) {
Log.d( Log.v(
TAG, "Notification state changed: ${it?.action}, blocked = " + TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}" "${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
) )
@ -197,7 +215,7 @@ class AmneziaActivity : QtActivity() {
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
Log.d(TAG, "onNewIntent: $intent") Log.v(TAG, "onNewIntent: $intent")
intent?.let(::processIntent) intent?.let(::processIntent)
} }
@ -386,7 +404,7 @@ class AmneziaActivity : QtActivity() {
@MainThread @MainThread
private fun startVpn(vpnConfig: String) { private fun startVpn(vpnConfig: String) {
getVpnProto(vpnConfig)?.let { proto -> getVpnProto(vpnConfig)?.let { proto ->
Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto") Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto")
if (isServiceConnected) { if (isServiceConnected) {
if (proto.serviceClass == vpnProto?.serviceClass) { if (proto.serviceClass == vpnProto?.serviceClass) {
vpnProto = proto vpnProto = proto
@ -499,7 +517,7 @@ class AmneziaActivity : QtActivity() {
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = { onSuccess = {
it?.data?.let { uri -> it?.data?.let { uri ->
Log.d(TAG, "Save file to $uri") Log.v(TAG, "Save file to $uri")
try { try {
contentResolver.openOutputStream(uri)?.use { os -> contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) } os.bufferedWriter().use { it.write(data) }
@ -548,7 +566,7 @@ class AmneziaActivity : QtActivity() {
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
onAny = { onAny = {
val uri = it?.data?.toString() ?: "" val uri = it?.data?.toString() ?: ""
Log.d(TAG, "Open file: $uri") Log.v(TAG, "Open file: $uri")
mainScope.launch { mainScope.launch {
qtInitialized.await() qtInitialized.await()
QtAndroidController.onFileOpened(uri) QtAndroidController.onFileOpened(uri)
@ -610,6 +628,14 @@ class AmneziaActivity : QtActivity() {
} }
} }
@Suppress("unused")
fun setNavigationBarColor(color: Int) {
Log.v(TAG, "Change navigation bar color: ${"#%08X".format(color)}")
mainScope.launch {
window.navigationBarColor = color
}
}
@Suppress("unused") @Suppress("unused")
fun minimizeApp() { fun minimizeApp() {
Log.v(TAG, "Minimize application") Log.v(TAG, "Minimize application")
@ -684,6 +710,77 @@ class AmneziaActivity : QtActivity() {
.show() .show()
} }
@Suppress("unused")
fun requestAuthentication() {
Log.v(TAG, "Request authentication")
mainScope.launch {
qtInitialized.await()
Intent(this@AmneziaActivity, AuthActivity::class.java).also {
startActivity(it)
}
}
}
// workaround for a bug in Qt that causes the mouse click event not to be handled
// also disable right-click, as it causes the application to crash
private var lastButtonState = 0
private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain(
downTime,
eventTime,
action,
pointerCount,
(0 until pointerCount).map { i ->
MotionEvent.PointerProperties().apply {
getPointerProperties(i, this)
}
}.toTypedArray(),
(0 until pointerCount).map { i ->
MotionEvent.PointerCoords().apply {
getPointerCoords(i, this)
}
}.toTypedArray(),
metaState,
MotionEvent.BUTTON_PRIMARY,
xPrecision,
yPrecision,
deviceId,
edgeFlags,
source,
flags
)
private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastButtonState = ev.buttonState
if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true
}
MotionEvent.ACTION_UP -> {
when (lastButtonState) {
MotionEvent.BUTTON_SECONDARY -> return true
MotionEvent.BUTTON_PRIMARY -> {
val modEvent = ev.fixCopy()
return superDispatch(modEvent).apply { modEvent.recycle() }
}
}
}
}
return superDispatch(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
}
return super.dispatchTouchEvent(ev)
}
override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }}
return super.dispatchTrackballEvent(ev)
}
/** /**
* Utils methods * Utils methods
*/ */

View file

@ -22,6 +22,7 @@ import androidx.annotation.MainThread
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import java.net.UnknownHostException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@ -31,6 +32,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@ -39,7 +41,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.LoadLibraryException
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
@ -49,6 +50,7 @@ import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
import org.amnezia.vpn.protocol.VpnException import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.putStatus import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.util.LoadLibraryException
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState import org.amnezia.vpn.util.net.NetworkState
@ -111,6 +113,10 @@ open class AmneziaVpnService : VpnService() {
get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME } get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
connectionJob?.cancel()
connectionJob = null
disconnectionJob?.cancel()
disconnectionJob = null
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
when (e) { when (e) {
is IllegalArgumentException, is IllegalArgumentException,
@ -122,6 +128,8 @@ open class AmneziaVpnService : VpnService() {
is LoadLibraryException -> onError("${e.message}. Caused: ${e.cause?.message}") is LoadLibraryException -> onError("${e.message}. Caused: ${e.cause?.message}")
is UnknownHostException -> onError("Unknown host")
else -> throw e else -> throw e
} }
} }
@ -292,7 +300,7 @@ open class AmneziaVpnService : VpnService() {
arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
) { ) {
it?.action?.let { action -> it?.action?.let { action ->
Log.d(TAG, "Broadcast request received: $action") Log.v(TAG, "Broadcast request received: $action")
when (action) { when (action) {
ACTION_CONNECT -> connect() ACTION_CONNECT -> connect()
ACTION_DISCONNECT -> disconnect() ACTION_DISCONNECT -> disconnect()
@ -309,7 +317,7 @@ open class AmneziaVpnService : VpnService() {
) )
) { ) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false) val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state") Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) { if (state == false) {
enableNotification() enableNotification()
} else { } else {
@ -442,7 +450,7 @@ open class AmneziaVpnService : VpnService() {
serviceNotification.isNotificationEnabled() && serviceNotification.isNotificationEnabled() &&
getSystemService<PowerManager>()?.isInteractive != false getSystemService<PowerManager>()?.isInteractive != false
) { ) {
Log.d(TAG, "Launch traffic stats update") Log.v(TAG, "Launch traffic stats update")
trafficStats.reset() trafficStats.reset()
startTrafficStatsUpdateJob() startTrafficStatsUpdateJob()
} }
@ -531,7 +539,7 @@ open class AmneziaVpnService : VpnService() {
protocolState.value = DISCONNECTING protocolState.value = DISCONNECTING
disconnectionJob = connectionScope.launch { disconnectionJob = connectionScope.launch {
connectionJob?.join() connectionJob?.cancelAndJoin()
connectionJob = null connectionJob = null
vpnProto?.protocol?.stopVpn() vpnProto?.protocol?.stopVpn()

View file

@ -0,0 +1,97 @@
package org.amnezia.vpn
import android.os.Build
import android.os.Bundle
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.AuthenticationResult
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
private const val TAG = "AuthActivity"
private const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL
class AuthActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val biometricManager = BiometricManager.from(applicationContext)
when (biometricManager.canAuthenticate(AUTHENTICATORS)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
showBiometricPrompt(biometricManager)
return
}
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
Log.w(TAG, "Unknown biometric status")
showBiometricPrompt(biometricManager)
return
}
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
Log.e(TAG, "The specified options are incompatible with the current Android " +
"version ${Build.VERSION.SDK_INT}")
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Log.w(TAG, "The hardware is unavailable")
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
Log.w(TAG, "No biometric or device credential is enrolled")
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Log.w(TAG, "There is no suitable hardware")
}
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
Log.w(TAG, "A security vulnerability has been discovered with one or " +
"more hardware sensors")
}
}
QtAndroidController.onAuthResult(true)
finish()
}
private fun showBiometricPrompt(biometricManager: BiometricManager) {
val executor = ContextCompat.getMainExecutor(applicationContext)
val biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.v(TAG, "Authentication succeeded")
QtAndroidController.onAuthResult(true)
finish()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "Authentication failed")
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.e(TAG, "Authentication error $errorCode: $errString")
QtAndroidController.onAuthResult(false)
finish()
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(AUTHENTICATORS)
.setTitle("AmneziaVPN")
.setSubtitle(biometricManager.getStrings(AUTHENTICATORS)?.promptMessage)
.build()
biometricPrompt.authenticate(promptInfo)
}
}

View file

@ -1,24 +0,0 @@
package org.amnezia.vpn;
import android.content.Context;
import android.app.KeyguardManager;
import android.content.Intent;
import org.qtproject.qt.android.bindings.QtActivity;
import static android.content.Context.KEYGUARD_SERVICE;
public class AuthHelper extends QtActivity {
static final String TAG = "AuthHelper";
public static Intent getAuthIntent(Context context) {
KeyguardManager mKeyguardManager = (KeyguardManager)context.getSystemService(KEYGUARD_SERVICE);
if (mKeyguardManager.isDeviceSecure()) {
return mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
} else {
return null;
}
}
}

View file

@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "Create Import Config Activity: $intent") Log.v(TAG, "Create Import Config Activity: $intent")
intent?.let(::readConfig) intent?.let(::readConfig)
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
Log.d(TAG, "onNewIntent: $intent") Log.v(TAG, "onNewIntent: $intent")
intent?.let(::readConfig) intent.let(::readConfig)
} }
private fun readConfig(intent: Intent) { private fun readConfig(intent: Intent) {
when (intent.action) { when (intent.action) {
ACTION_SEND -> { ACTION_SEND -> {
Log.d(TAG, "Process SEND action, type: ${intent.type}") Log.v(TAG, "Process SEND action, type: ${intent.type}")
when (intent.type) { when (intent.type) {
"application/octet-stream" -> { "application/octet-stream" -> {
intent.getUriCompat()?.let { uri -> intent.getUriCompat()?.let { uri ->
@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() {
} }
ACTION_VIEW -> { ACTION_VIEW -> {
Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}") Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}")
when (intent.scheme) { when (intent.scheme) {
"file", "content" -> { "file", "content" -> {
intent.data?.let { uri -> intent.data?.let { uri ->

View file

@ -62,7 +62,7 @@ class ServiceNotification(private val context: Context) {
fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification { fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification {
val speedString = if (state == CONNECTED) zeroSpeed else null val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state") Log.v(TAG, "Build notification: $serverName, $state")
return notificationBuilder return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round) .setSmallIcon(R.drawable.ic_amnezia_round)
@ -88,17 +88,15 @@ class ServiceNotification(private val context: Context) {
fun isNotificationEnabled(): Boolean { fun isNotificationEnabled(): Boolean {
if (!context.isNotificationPermissionGranted()) return false if (!context.isNotificationPermissionGranted()) return false
if (!notificationManager.areNotificationsEnabled()) return false if (!notificationManager.areNotificationsEnabled()) return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let {
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) it.importance != NotificationManager.IMPORTANCE_NONE
?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true } ?: true
}
return true
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) { fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) { if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state") Log.v(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state)) notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
} }
} }

View file

@ -25,5 +25,7 @@ object QtAndroidController {
external fun onConfigImported(data: String) external fun onConfigImported(data: String)
external fun onAuthResult(result: Boolean)
external fun decodeQrCode(data: String): Boolean external fun decodeQrCode(data: String): Boolean
} }

View file

@ -0,0 +1,9 @@
package org.amnezia.vpn.util
import org.json.JSONArray
import org.json.JSONObject
inline fun <reified T> JSONArray.asSequence(): Sequence<T> =
(0..<length()).asSequence().map { get(it) as T }
fun JSONObject.optStringOrNull(name: String) = optString(name).ifEmpty { null }

View file

@ -0,0 +1,66 @@
package org.amnezia.vpn.util
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipFile
private const val TAG = "LibraryLoader"
object LibraryLoader {
private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean {
Log.d(TAG, "Extracting library: $libraryName")
val apks = hashSetOf<String>()
context.applicationInfo.run {
sourceDir?.let { apks += it }
splitSourceDirs?.let { apks += it }
}
for (abi in Build.SUPPORTED_ABIS) {
for (apk in apks) {
ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile ->
val mappedName = System.mapLibraryName(libraryName)
val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator)
val zipEntry = zipFile.getEntry(libraryZipPath)
zipEntry?.let {
Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}")
FileOutputStream(destination).use { outStream ->
zipFile.getInputStream(zipEntry).use { inStream ->
inStream.copyTo(outStream, 32 * 1024)
outStream.fd.sync()
}
}
}
return true
}
}
}
return false
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
fun loadSharedLibrary(context: Context, libraryName: String) {
Log.d(TAG, "Loading library: $libraryName")
try {
System.loadLibrary(libraryName)
return
} catch (_: UnsatisfiedLinkError) {
Log.w(TAG, "Failed to load library, try to extract it from apk")
}
var tempFile: File? = null
try {
tempFile = File.createTempFile("lib", ".so", context.codeCacheDir)
if (extractLibrary(context, libraryName, tempFile)) {
System.load(tempFile.absolutePath)
return
}
} catch (e: Exception) {
throw LoadLibraryException("Failed to load library apk: $libraryName", e)
} finally {
tempFile?.delete()
}
}
}
class LoadLibraryException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)

View file

@ -1,8 +1,6 @@
package org.amnezia.vpn.util package org.amnezia.vpn.util
import android.content.Context import android.content.Context
import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.os.Build import android.os.Build
import android.os.Process import android.os.Process
import java.io.File import java.io.File
@ -12,8 +10,6 @@ import java.nio.channels.FileChannel
import java.nio.channels.FileLock import java.nio.channels.FileLock
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import org.amnezia.vpn.util.Log.Priority.D import org.amnezia.vpn.util.Log.Priority.D
import org.amnezia.vpn.util.Log.Priority.E import org.amnezia.vpn.util.Log.Priority.E
@ -41,11 +37,7 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024
* | | | create a report and/or terminate the process | * | | | create a report and/or terminate the process |
*/ */
object Log { object Log {
private val dateTimeFormat: Any = private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
else object : ThreadLocal<DateFormat>() {
override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US)
}
private lateinit var logDir: File private lateinit var logDir: File
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) } private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
@ -143,12 +135,7 @@ object Log {
} }
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String { private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val date = LocalDateTime.now().format(dateTimeFormat)
LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter)
} else {
@Suppress("UNCHECKED_CAST")
(dateTimeFormat as ThreadLocal<DateFormat>).get()?.format(Date())
}
return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " + return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
"$tag: $msg\n" "$tag: $msg\n"
} }

View file

@ -42,18 +42,12 @@ class NetworkState(
private val networkCallback: NetworkCallback by lazy(NONE) { private val networkCallback: NetworkCallback by lazy(NONE) {
object : NetworkCallback() { object : NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
Log.d(TAG, "onAvailable: $network") Log.v(TAG, "onAvailable: $network")
} }
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
checkNetworkState(network, networkCapabilities) checkNetworkState(network, networkCapabilities)
} else {
handler.post {
checkNetworkState(network, networkCapabilities)
}
}
} }
private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) { private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) {
@ -73,11 +67,11 @@ class NetworkState(
} }
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
Log.d(TAG, "onBlockedStatusChanged: $network, $blocked") Log.v(TAG, "onBlockedStatusChanged: $network, $blocked")
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
Log.d(TAG, "onLost: $network") Log.v(TAG, "onLost: $network")
} }
} }
} }
@ -87,8 +81,8 @@ class NetworkState(
Log.d(TAG, "Bind network listener") Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { } else {
val numberAttempts = 3 val numberAttempts = 300
var attemptCount = 0 var attemptCount = 0
while(true) { while(true) {
try { try {
@ -108,8 +102,6 @@ class NetworkState(
} }
} }
} }
} else {
connectivityManager.requestNetwork(networkRequest, networkCallback)
} }
isListenerBound = true isListenerBound = true
} }

View file

@ -35,7 +35,7 @@ fun getLocalNetworks(context: Context, ipv6: Boolean): List<InetNetwork> {
return emptyList() return emptyList()
} }
fun parseInetAddress(address: String): InetAddress = parseNumericAddressCompat(address) fun parseInetAddress(address: String): InetAddress = InetAddress.getByName(address)
private val parseNumericAddressCompat: (String) -> InetAddress = private val parseNumericAddressCompat: (String) -> InetAddress =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -60,7 +60,7 @@ private val parseNumericAddressCompat: (String) -> InetAddress =
internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6 internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6
.replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2") .replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2")
internal val InetAddress.ip: String val InetAddress.ip: String
get() = if (this is Inet4Address) { get() = if (this is Inet4Address) {
hostAddress!! hostAddress!!
} else { } else {

View file

@ -1,60 +1,35 @@
package org.amnezia.vpn.protocol.wireguard package org.amnezia.vpn.protocol.wireguard
import android.net.VpnService.Builder import android.net.VpnService.Builder
import java.util.TreeMap import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.amnezia.awg.GoBackend import org.amnezia.awg.GoBackend
import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.asSequence
import org.amnezia.vpn.util.net.InetEndpoint import org.amnezia.vpn.util.net.InetEndpoint
import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.parseInetAddress import org.amnezia.vpn.util.net.parseInetAddress
import org.amnezia.vpn.util.optStringOrNull
import org.json.JSONObject import org.json.JSONObject
/**
* Config example:
* {
* "protocol": "wireguard",
* "description": "Server 1",
* "dns1": "1.1.1.1",
* "dns2": "1.0.0.1",
* "hostName": "100.100.100.0",
* "splitTunnelSites": [
* ],
* "splitTunnelType": 0,
* "wireguard_config_data": {
* "client_ip": "10.8.1.1",
* "hostName": "100.100.100.0",
* "port": 12345,
* "client_pub_key": "clientPublicKeyBase64",
* "client_priv_key": "privateKeyBase64",
* "psk_key": "presharedKeyBase64",
* "server_pub_key": "publicKeyBase64",
* "config": "[Interface]
* Address = 10.8.1.1/32
* DNS = 1.1.1.1, 1.0.0.1
* PrivateKey = privateKeyBase64
*
* [Peer]
* PublicKey = publicKeyBase64
* PresharedKey = presharedKeyBase64
* AllowedIPs = 0.0.0.0/0, ::/0
* Endpoint = 100.100.100.0:12345
* PersistentKeepalive = 25
* "
* }
* }
*/
private const val TAG = "Wireguard" private const val TAG = "Wireguard"
open class Wireguard : Protocol() { open class Wireguard : Protocol() {
private var tunnelHandle: Int = -1 private var tunnelHandle: Int = -1
protected open val ifName: String = "amn0" protected open val ifName: String = "amn0"
private lateinit var scope: CoroutineScope
private var statusJob: Job? = null
override val statistics: Statistics override val statistics: Statistics
get() { get() {
@ -77,69 +52,78 @@ open class Wireguard : Protocol() {
override fun internalInit() { override fun internalInit() {
if (!isInitialized) loadSharedLibrary(context, "wg-go") if (!isInitialized) loadSharedLibrary(context, "wg-go")
if (this::scope.isInitialized) {
scope.cancel()
}
scope = CoroutineScope(Dispatchers.IO)
} }
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
val wireguardConfig = parseConfig(config) val wireguardConfig = parseConfig(config)
start(wireguardConfig, vpnBuilder, protect) start(wireguardConfig, vpnBuilder, protect)
state.value = CONNECTED
} }
protected open fun parseConfig(config: JSONObject): WireguardConfig { protected open fun parseConfig(config: JSONObject): WireguardConfig {
val configDataJson = config.getJSONObject("wireguard_config_data") val configData = config.getJSONObject("wireguard_config_data")
val configData = parseConfigData(configDataJson.getString("config"))
return WireguardConfig.build { return WireguardConfig.build {
configWireguard(configData, configDataJson) configWireguard(config, configData)
configSplitTunneling(config) configSplitTunneling(config)
configAppSplitTunneling(config) configAppSplitTunneling(config)
} }
} }
protected fun WireguardConfig.Builder.configWireguard(configData: Map<String, String>, configDataJson: JSONObject) { protected fun WireguardConfig.Builder.configWireguard(config: JSONObject, configData: JSONObject) {
configData["Address"]?.split(",")?.map { address -> configData.getString("client_ip").split(",").map { address ->
InetNetwork.parse(address.trim()) InetNetwork.parse(address.trim())
}?.forEach(::addAddress) }.forEach(::addAddress)
configData["DNS"]?.split(",")?.map { dns -> config.optStringOrNull("dns1")?.let { dns ->
parseInetAddress(dns.trim()) addDnsServer(parseInetAddress(dns.trim()))
}?.forEach(::addDnsServer) }
config.optStringOrNull("dns2")?.let { dns ->
addDnsServer(parseInetAddress(dns.trim()))
}
val defRoutes = hashSetOf( val defRoutes = hashSetOf(
InetNetwork("0.0.0.0", 0), InetNetwork("0.0.0.0", 0),
InetNetwork("::", 0) InetNetwork("::", 0)
) )
val routes = hashSetOf<InetNetwork>() val routes = hashSetOf<InetNetwork>()
configData["AllowedIPs"]?.split(",")?.map { route -> configData.getJSONArray("allowed_ips").asSequence<String>().map { route ->
InetNetwork.parse(route.trim()) InetNetwork.parse(route.trim())
}?.forEach(routes::add) }.forEach(routes::add)
// if the allowed IPs list contains at least one non-default route, disable global split tunneling // if the allowed IPs list contains at least one non-default route, disable global split tunneling
if (routes.any { it !in defRoutes }) disableSplitTunneling() if (routes.any { it !in defRoutes }) disableSplitTunneling()
addRoutes(routes) addRoutes(routes)
configDataJson.optString("mtu").let { mtu -> configData.optStringOrNull("mtu")?.let { setMtu(it.toInt()) }
if (mtu.isNotEmpty()) {
setMtu(mtu.toInt()) val host = configData.getString("hostName").let { parseInetAddress(it.trim()) }
} else { val port = configData.getInt("port")
configData["MTU"]?.let { setMtu(it.toInt()) } setEndpoint(InetEndpoint(host, port))
}
if (configData.optBoolean("isObfuscationEnabled")) {
setUseProtocolExtension(true)
configExtensionParameters(configData)
} }
configData["Endpoint"]?.let { setEndpoint(InetEndpoint.parse(it)) } configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
configData["PersistentKeepalive"]?.let { setPersistentKeepalive(it.toInt()) } configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
configData["PrivateKey"]?.let { setPrivateKeyHex(it.base64ToHex()) } configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
configData["PublicKey"]?.let { setPublicKeyHex(it.base64ToHex()) } configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
configData["PresharedKey"]?.let { setPreSharedKeyHex(it.base64ToHex()) }
} }
protected fun parseConfigData(data: String): Map<String, String> { protected fun WireguardConfig.Builder.configExtensionParameters(configData: JSONObject) {
val parsedData = TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER) configData.optStringOrNull("Jc")?.let { setJc(it.toInt()) }
data.lineSequence() configData.optStringOrNull("Jmin")?.let { setJmin(it.toInt()) }
.filter { it.isNotEmpty() && !it.startsWith('[') } configData.optStringOrNull("Jmax")?.let { setJmax(it.toInt()) }
.forEach { line -> configData.optStringOrNull("S1")?.let { setS1(it.toInt()) }
val attr = line.split("=", limit = 2) configData.optStringOrNull("S2")?.let { setS2(it.toInt()) }
parsedData[attr.first().trim()] = attr.last().trim() configData.optStringOrNull("H1")?.let { setH1(it.toLong()) }
} configData.optStringOrNull("H2")?.let { setH2(it.toLong()) }
return parsedData configData.optStringOrNull("H3")?.let { setH3(it.toLong()) }
configData.optStringOrNull("H4")?.let { setH4(it.toLong()) }
} }
private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) { private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) {
@ -168,6 +152,43 @@ open class Wireguard : Protocol() {
tunnelHandle = -1 tunnelHandle = -1
throw VpnStartException("Protect VPN interface: permission not granted or revoked") throw VpnStartException("Protect VPN interface: permission not granted or revoked")
} }
launchStatusJob()
}
private fun launchStatusJob() {
Log.d(TAG, "Launch status job")
statusJob = scope.launch {
while (true) {
val lastHandshake = getLastHandshake()
Log.v(TAG, "lastHandshake=$lastHandshake")
if (lastHandshake == 0L) {
delay(1000)
continue
}
if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED
else if (lastHandshake == -1L) state.value = DISCONNECTED
statusJob = null
break
}
}
}
private fun getLastHandshake(): Long {
if (tunnelHandle == -1) {
Log.e(TAG, "Trying to get config of a non-existent tunnel")
return -1
}
val config = GoBackend.awgGetConfig(tunnelHandle)
if (config == null) {
Log.e(TAG, "Failed to get tunnel config")
return -2
}
val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong()
if (lastHandshake == null) {
Log.e(TAG, "Failed to get last_handshake_time_sec")
return -2
}
return lastHandshake
} }
override fun stopVpn() { override fun stopVpn() {
@ -175,6 +196,8 @@ open class Wireguard : Protocol() {
Log.w(TAG, "Tunnel already down") Log.w(TAG, "Tunnel already down")
return return
} }
statusJob?.cancel()
statusJob = null
val handleToClose = tunnelHandle val handleToClose = tunnelHandle
tunnelHandle = -1 tunnelHandle = -1
GoBackend.awgTurnOff(handleToClose) GoBackend.awgTurnOff(handleToClose)

View file

@ -1,6 +1,7 @@
package org.amnezia.vpn.protocol.wireguard package org.amnezia.vpn.protocol.wireguard
import android.util.Base64 import android.util.Base64
import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.ProtocolConfig import org.amnezia.vpn.protocol.ProtocolConfig
import org.amnezia.vpn.util.net.InetEndpoint import org.amnezia.vpn.util.net.InetEndpoint
@ -12,7 +13,17 @@ open class WireguardConfig protected constructor(
val persistentKeepalive: Int, val persistentKeepalive: Int,
val publicKeyHex: String, val publicKeyHex: String,
val preSharedKeyHex: String?, val preSharedKeyHex: String?,
val privateKeyHex: String val privateKeyHex: String,
val useProtocolExtension: Boolean,
val jc: Int?,
val jmin: Int?,
val jmax: Int?,
val s1: Int?,
val s2: Int?,
val h1: Long?,
val h2: Long?,
val h3: Long?,
val h4: Long?
) : ProtocolConfig(protocolConfigBuilder) { ) : ProtocolConfig(protocolConfigBuilder) {
protected constructor(builder: Builder) : this( protected constructor(builder: Builder) : this(
@ -21,7 +32,17 @@ open class WireguardConfig protected constructor(
builder.persistentKeepalive, builder.persistentKeepalive,
builder.publicKeyHex, builder.publicKeyHex,
builder.preSharedKeyHex, builder.preSharedKeyHex,
builder.privateKeyHex builder.privateKeyHex,
builder.useProtocolExtension,
builder.jc,
builder.jmin,
builder.jmax,
builder.s1,
builder.s2,
builder.h1,
builder.h2,
builder.h3,
builder.h4
) )
fun toWgUserspaceString(): String = with(StringBuilder()) { fun toWgUserspaceString(): String = with(StringBuilder()) {
@ -33,6 +54,30 @@ open class WireguardConfig protected constructor(
open fun appendDeviceLine(sb: StringBuilder) = with(sb) { open fun appendDeviceLine(sb: StringBuilder) = with(sb) {
appendLine("private_key=$privateKeyHex") appendLine("private_key=$privateKeyHex")
if (useProtocolExtension) {
validateProtocolExtensionParameters()
appendLine("jc=$jc")
appendLine("jmin=$jmin")
appendLine("jmax=$jmax")
appendLine("s1=$s1")
appendLine("s2=$s2")
appendLine("h1=$h1")
appendLine("h2=$h2")
appendLine("h3=$h3")
appendLine("h4=$h4")
}
}
private fun validateProtocolExtensionParameters() {
if (jc == null) throw BadConfigException("Parameter jc is undefined")
if (jmin == null) throw BadConfigException("Parameter jmin is undefined")
if (jmax == null) throw BadConfigException("Parameter jmax is undefined")
if (s1 == null) throw BadConfigException("Parameter s1 is undefined")
if (s2 == null) throw BadConfigException("Parameter s2 is undefined")
if (h1 == null) throw BadConfigException("Parameter h1 is undefined")
if (h2 == null) throw BadConfigException("Parameter h2 is undefined")
if (h3 == null) throw BadConfigException("Parameter h3 is undefined")
if (h4 == null) throw BadConfigException("Parameter h4 is undefined")
} }
open fun appendPeerLine(sb: StringBuilder) = with(sb) { open fun appendPeerLine(sb: StringBuilder) = with(sb) {
@ -65,6 +110,18 @@ open class WireguardConfig protected constructor(
override var mtu: Int = WIREGUARD_DEFAULT_MTU override var mtu: Int = WIREGUARD_DEFAULT_MTU
internal var useProtocolExtension: Boolean = false
internal var jc: Int? = null
internal var jmin: Int? = null
internal var jmax: Int? = null
internal var s1: Int? = null
internal var s2: Int? = null
internal var h1: Long? = null
internal var h2: Long? = null
internal var h3: Long? = null
internal var h4: Long? = null
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint } fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive } fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
@ -75,6 +132,18 @@ open class WireguardConfig protected constructor(
fun setPrivateKeyHex(privateKeyHex: String) = apply { this.privateKeyHex = privateKeyHex } fun setPrivateKeyHex(privateKeyHex: String) = apply { this.privateKeyHex = privateKeyHex }
fun setUseProtocolExtension(useProtocolExtension: Boolean) = apply { this.useProtocolExtension = useProtocolExtension }
fun setJc(jc: Int) = apply { this.jc = jc }
fun setJmin(jmin: Int) = apply { this.jmin = jmin }
fun setJmax(jmax: Int) = apply { this.jmax = jmax }
fun setS1(s1: Int) = apply { this.s1 = s1 }
fun setS2(s2: Int) = apply { this.s2 = s2 }
fun setH1(h1: Long) = apply { this.h1 = h1 }
fun setH2(h2: Long) = apply { this.h2 = h2 }
fun setH3(h3: Long) = apply { this.h3 = h3 }
fun setH4(h4: Long) = apply { this.h4 = h4 }
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) } override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
} }

View file

@ -17,72 +17,10 @@ import org.amnezia.vpn.protocol.xray.libXray.Logger
import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.ip
import org.amnezia.vpn.util.net.parseInetAddress import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject import org.json.JSONObject
/**
* Config example:
* {
* "appSplitTunnelType": 0,
* "config_version": 0,
* "description": "Server 1",
* "dns1": "1.1.1.1",
* "dns2": "1.0.0.1",
* "hostName": "100.100.100.0",
* "protocol": "xray",
* "splitTunnelApps": [],
* "splitTunnelSites": [],
* "splitTunnelType": 0,
* "xray_config_data": {
* "inbounds": [
* {
* "listen": "127.0.0.1",
* "port": 8080,
* "protocol": "socks",
* "settings": {
* "udp": true
* }
* }
* ],
* "log": {
* "loglevel": "error"
* },
* "outbounds": [
* {
* "protocol": "vless",
* "settings": {
* "vnext": [
* {
* "address": "100.100.100.0",
* "port": 443,
* "users": [
* {
* "encryption": "none",
* "flow": "xtls-rprx-vision",
* "id": "id"
* }
* ]
* }
* ]
* },
* "streamSettings": {
* "network": "tcp",
* "realitySettings": {
* "fingerprint": "chrome",
* "publicKey": "publicKey",
* "serverName": "google.com",
* "shortId": "id",
* "spiderX": ""
* },
* "security": "reality"
* }
* }
* ]
* }
* }
*
*/
private const val TAG = "Xray" private const val TAG = "Xray"
private const val LIBXRAY_TAG = "libXray" private const val LIBXRAY_TAG = "libXray"
@ -109,7 +47,7 @@ class Xray : Protocol() {
} }
} }
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
if (isRunning) { if (isRunning) {
Log.w(TAG, "XRay already running") Log.w(TAG, "XRay already running")
return return
@ -124,7 +62,15 @@ class Xray : Protocol() {
.put("loglevel", "warning") .put("loglevel", "warning")
.put("access", "none") // disable access log .put("access", "none") // disable access log
start(xrayConfig, xrayJsonConfig.toString(), vpnBuilder, protect) var xrayJsonConfigString = xrayJsonConfig.toString()
config.getString("hostName").let { hostName ->
val ipAddress = parseInetAddress(hostName).ip
if (hostName != ipAddress) {
xrayJsonConfigString = xrayJsonConfigString.replace(hostName, ipAddress)
}
}
start(xrayConfig, xrayJsonConfigString, vpnBuilder, protect)
state.value = CONNECTED state.value = CONNECTED
isRunning = true isRunning = true
} }
@ -184,8 +130,8 @@ class Xray : Protocol() {
LibXray.initXray(assetsPath) LibXray.initXray(assetsPath)
val geoDir = File(assetsPath, "geo").absolutePath val geoDir = File(assetsPath, "geo").absolutePath
val configPath = File(context.cacheDir, "config.json") val configPath = File(context.cacheDir, "config.json")
Log.d(TAG, "xray.location.asset: $geoDir") Log.v(TAG, "xray.location.asset: $geoDir")
Log.d(TAG, "config: $configPath") Log.v(TAG, "config: $configPath")
try { try {
configPath.writeText(configJson) configPath.writeText(configJson)
} catch (e: IOException) { } catch (e: IOException) {

View file

@ -2,10 +2,6 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}") set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
if(NOT IOS AND NOT ANDROID)
include(${CLIENT_ROOT_DIR}/3rd/SingleApplication/singleapplication.cmake)
endif()
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel) add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
set(LIBS ${LIBS} SortFilterProxyModel) set(LIBS ${LIBS} SortFilterProxyModel)
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake) include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)

View file

@ -1,6 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build") message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 24) set(APP_ANDROID_MIN_SDK 26)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE) "The minimum API level supported by the application or library" FORCE)
@ -27,7 +27,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h
) )
@ -35,7 +34,6 @@ set(HEADERS ${HEADERS}
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp
) )

View file

@ -95,6 +95,18 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
stdOut.replace("/32", ""); stdOut.replace("/32", "");
QStringList ips = stdOut.split("\n", Qt::SkipEmptyParts); QStringList ips = stdOut.split("\n", Qt::SkipEmptyParts);
// remove extra IPs from each line for case when user manually edited the wg0.conf
// and added there more IPs for route his itnernal networks, like:
// ...
// AllowedIPs = 10.8.1.6/32, 192.168.1.0/24, 192.168.2.0/24, ...
// ...
// without this code - next IP would be 1 if last item in 'ips' has format above
QStringList vpnIps;
for (const auto &ip : ips) {
vpnIps.append(ip.split(",", Qt::SkipEmptyParts).first().trimmed());
}
ips = vpnIps;
// Calc next IP address // Calc next IP address
if (ips.isEmpty()) { if (ips.isEmpty()) {
nextIpNumber = "2"; nextIpNumber = "2";
@ -187,6 +199,10 @@ QString WireguardConfigurator::createConfig(const ServerCredentials &credentials
jConfig[config_key::server_pub_key] = connData.serverPubKey; jConfig[config_key::server_pub_key] = connData.serverPubKey;
jConfig[config_key::mtu] = wireguarConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); jConfig[config_key::mtu] = wireguarConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu);
jConfig[config_key::persistent_keep_alive] = "25";
QJsonArray allowedIps { "0.0.0.0/0", "::/0" };
jConfig[config_key::allowed_ips] = allowedIps;
jConfig[config_key::clientId] = connData.clientPubKey; jConfig[config_key::clientId] = connData.clientPubKey;
return QJsonDocument(jConfig).toJson(); return QJsonDocument(jConfig).toJson();

View file

@ -3,38 +3,169 @@
#include <QFile> #include <QFile>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QUuid>
#include "logger.h"
#include "containers/containers_defs.h" #include "containers/containers_defs.h"
#include "core/controllers/serverController.h" #include "core/controllers/serverController.h"
#include "core/scripts_registry.h" #include "core/scripts_registry.h"
namespace {
Logger logger("XrayConfigurator");
}
XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent) XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent)
: ConfiguratorBase(settings, serverController, parent) : ConfiguratorBase(settings, serverController, parent)
{ {
} }
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
ErrorCode &errorCode) const QJsonObject &containerConfig, ErrorCode &errorCode)
{ {
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), // Generate new UUID for client
m_serverController->genVarsForScript(credentials, container, containerConfig)); QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString xrayPublicKey = // Get current server config
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); QString currentConfig = m_serverController->getTextFileFromContainer(
xrayPublicKey.replace("\n", ""); container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode);
xrayUuid.replace("\n", "");
QString xrayShortId =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
xrayShortId.replace("\n", "");
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return ""; return "";
} }
config.replace("$XRAY_CLIENT_ID", xrayUuid); // Parse current config as JSON
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains("inbounds")) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray inbounds = serverConfig["inbounds"].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains("settings")) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject settings = inbound["settings"].toObject();
if (!settings.contains("clients")) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray clients = settings["clients"].toArray();
// Create configuration for new client
QJsonObject clientConfig {
{"id", clientId},
{"flow", "xtls-rprx-vision"}
};
clients.append(clientConfig);
// Update config
settings["clients"] = clients;
inbound["settings"] = settings;
inbounds[0] = inbound;
serverConfig["inbounds"] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_serverController->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_serverController->runScript(
credentials,
m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container))
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
}
return clientId;
}
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
// Get client ID from prepareServerConfig
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
errorCode = ErrorCode::InternalError;
return "";
}
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));
if (config.isEmpty()) {
logger.error() << "Failed to get config template";
errorCode = ErrorCode::InternalError;
return "";
}
QString xrayPublicKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
errorCode = ErrorCode::InternalError;
return "";
}
xrayPublicKey.replace("\n", "");
QString xrayShortId =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
errorCode = ErrorCode::InternalError;
return "";
}
xrayShortId.replace("\n", "");
// Validate all required variables are present
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
logger.error() << "Config template missing required variables:"
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
errorCode = ErrorCode::InternalError;
return "";
}
config.replace("$XRAY_CLIENT_ID", xrayClientId);
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
config.replace("$XRAY_SHORT_ID", xrayShortId); config.replace("$XRAY_SHORT_ID", xrayShortId);

View file

@ -14,6 +14,10 @@ public:
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode); ErrorCode &errorCode);
private:
QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode);
}; };
#endif // XRAY_CONFIGURATOR_H #endif // XRAY_CONFIGURATOR_H

View file

@ -1,5 +1,8 @@
#include "apiController.h" #include "apiController.h"
#include <algorithm>
#include <random>
#include <QEventLoop> #include <QEventLoop>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
@ -9,8 +12,9 @@
#include "QRsa.h" #include "QRsa.h"
#include "amnezia_application.h" #include "amnezia_application.h"
#include "core/enums/apiEnums.h"
#include "configurators/wireguard_configurator.h" #include "configurators/wireguard_configurator.h"
#include "core/enums/apiEnums.h"
#include "utilities.h"
#include "version.h" #include "version.h"
namespace namespace
@ -33,6 +37,7 @@ namespace
constexpr char userCountryCode[] = "user_country_code"; constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code"; constexpr char serverCountryCode[] = "server_country_code";
constexpr char serviceType[] = "service_type"; constexpr char serviceType[] = "service_type";
constexpr char serviceInfo[] = "service_info";
constexpr char aesKey[] = "aes_key"; constexpr char aesKey[] = "aes_key";
constexpr char aesIv[] = "aes_iv"; constexpr char aesIv[] = "aes_iv";
@ -40,9 +45,12 @@ namespace
constexpr char apiPayload[] = "api_payload"; constexpr char apiPayload[] = "api_payload";
constexpr char keyPayload[] = "key_payload"; constexpr char keyPayload[] = "key_payload";
constexpr char apiConfig[] = "api_config";
constexpr char authData[] = "auth_data";
} }
const QStringList proxyStorageUrl = {""}; const int requestTimeoutMsecs = 12 * 1000; // 12 secs
ErrorCode checkErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply) ErrorCode checkErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
{ {
@ -63,9 +71,32 @@ namespace
return ErrorCode::ApiConfigDownloadError; return ErrorCode::ApiConfigDownloadError;
} }
} }
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
const QByteArray &iv = "", const QByteArray &salt = "")
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "Timeout occurred";
return true;
} else if (responseBody.contains("html")) {
qDebug() << "The response contains an html tag";
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
} catch (...) {
qDebug() << "Failed to decrypt the data";
return true;
}
}
return false;
}
} }
ApiController::ApiController(const QString &gatewayEndpoint, QObject *parent) : QObject(parent), m_gatewayEndpoint(gatewayEndpoint) ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent)
: QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment)
{ {
} }
@ -93,8 +124,8 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey);
} else if (protocol == configKey::awg) { } else if (protocol == configKey::awg) {
configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey);
auto serverConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
auto containers = serverConfig.value(config_key::containers).toArray(); auto containers = newServerConfig.value(config_key::containers).toArray();
if (containers.isEmpty()) { if (containers.isEmpty()) {
return; // todo process error return; // todo process error
} }
@ -113,37 +144,56 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader);
container[containerName] = containerConfig; container[containerName] = containerConfig;
containers.replace(0, container); containers.replace(0, container);
serverConfig[config_key::containers] = containers; newServerConfig[config_key::containers] = containers;
configStr = QString(QJsonDocument(serverConfig).toJson()); configStr = QString(QJsonDocument(newServerConfig).toJson());
} }
QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1); serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1);
serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2); serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2);
serverConfig[config_key::containers] = apiConfig.value(config_key::containers); serverConfig[config_key::containers] = newServerConfig.value(config_key::containers);
serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName); serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName);
if (apiConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
serverConfig[config_key::configVersion] = apiConfig.value(config_key::configVersion); serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion);
serverConfig[config_key::description] = apiConfig.value(config_key::description); serverConfig[config_key::description] = newServerConfig.value(config_key::description);
serverConfig[config_key::name] = apiConfig.value(config_key::name); serverConfig[config_key::name] = newServerConfig.value(config_key::name);
} }
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString(); auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString();
serverConfig[config_key::defaultContainer] = defaultContainer; serverConfig[config_key::defaultContainer] = defaultContainer;
QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap();
map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap());
auto apiConfig = QJsonObject::fromVariantMap(map);
if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject());
}
serverConfig[configKey::apiConfig] = apiConfig;
return; return;
} }
QStringList ApiController::getProxyUrls() QStringList ApiController::getProxyUrls()
{ {
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(7000); request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait; QEventLoop wait;
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
QNetworkReply* reply; QNetworkReply *reply;
QStringList proxyStorageUrl;
if (m_isDevEnvironment) {
proxyStorageUrl = QStringList { DEV_S3_ENDPOINT };
} else {
proxyStorageUrl = QStringList { PROD_S3_ENDPOINT };
}
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
for (const auto &proxyStorageUrl : proxyStorageUrl) { for (const auto &proxyStorageUrl : proxyStorageUrl) {
request.setUrl(proxyStorageUrl); request.setUrl(proxyStorageUrl);
@ -165,11 +215,23 @@ QStringList ApiController::getProxyUrls()
EVP_PKEY *privateKey = nullptr; EVP_PKEY *privateKey = nullptr;
QByteArray responseBody; QByteArray responseBody;
try { try {
QByteArray key = PROD_PROXY_STORAGE_KEY; if (!m_isDevEnvironment) {
QSimpleCrypto::QRsa rsa; QCryptographicHash hash(QCryptographicHash::Sha512);
privateKey = rsa.getPrivateKeyFromByteArray(key, ""); hash.addData(key);
responseBody = rsa.decrypt(encryptedResponseBody, privateKey, RSA_PKCS1_PADDING); QByteArray hashResult = hash.result().toHex();
QByteArray key = QByteArray::fromHex(hashResult.left(64));
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
} else {
responseBody = encryptedResponseBody;
}
} catch (...) { } catch (...) {
Utils::logException();
qCritical() << "error loading private key from environment variables or decrypting payload"; qCritical() << "error loading private key from environment variables or decrypting payload";
return {}; return {};
} }
@ -220,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
if (serverConfig.value(config_key::configVersion).toInt()) { if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(7000); request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
@ -276,12 +338,12 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
#endif #endif
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(7000); request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint));
QNetworkReply* reply; QNetworkReply *reply;
reply = amnApp->manager()->get(request); reply = amnApp->manager()->get(request);
QEventLoop wait; QEventLoop wait;
@ -291,39 +353,53 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(); wait.exec();
if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) { responseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
m_proxyUrls = getProxyUrls(); m_proxyUrls = getProxyUrls();
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator);
for (const QString &proxyUrl : m_proxyUrls) { for (const QString &proxyUrl : m_proxyUrls) {
qDebug() << "Go to the next endpoint";
request.setUrl(QString("%1v1/services").arg(proxyUrl)); request.setUrl(QString("%1v1/services").arg(proxyUrl));
reply->deleteLater(); // delete the previous reply
reply = amnApp->manager()->get(request); reply = amnApp->manager()->get(request);
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(); wait.exec();
if (reply->error() != QNetworkReply::NetworkError::TimeoutError && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) {
responseBody = reply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, responseBody, false)) {
break; break;
} }
reply->deleteLater();
} }
} }
responseBody = reply->readAll();
auto errorCode = checkErrors(sslErrors, reply); auto errorCode = checkErrors(sslErrors, reply);
reply->deleteLater(); reply->deleteLater();
if (errorCode == ErrorCode::NoError) {
if (!responseBody.contains("services")) {
return ErrorCode::ApiServicesMissingError;
}
}
return errorCode; return errorCode;
} }
ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType,
const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig) const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData,
QJsonObject &serverConfig)
{ {
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess(); IosController::Instance()->requestInetAccess();
QThread::msleep(10); QThread::msleep(10);
#endif #endif
QNetworkAccessManager manager;
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(7000); request.setTransferTimeout(requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint)); request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint));
@ -337,6 +413,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
} }
apiPayload[configKey::serviceType] = serviceType; apiPayload[configKey::serviceType] = serviceType;
apiPayload[configKey::uuid] = installationUuid; apiPayload[configKey::uuid] = installationUuid;
if (!authData.isEmpty()) {
apiPayload[configKey::authData] = authData;
}
QSimpleCrypto::QBlockCipher blockCipher; QSimpleCrypto::QBlockCipher blockCipher;
QByteArray key = blockCipher.generatePrivateSalt(32); QByteArray key = blockCipher.generatePrivateSalt(32);
@ -355,10 +434,11 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
EVP_PKEY *publicKey = nullptr; EVP_PKEY *publicKey = nullptr;
try { try {
QByteArray key = PROD_AGW_PUBLIC_KEY; QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QSimpleCrypto::QRsa rsa; QSimpleCrypto::QRsa rsa;
publicKey = rsa.getPublicKeyFromByteArray(key); publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
} catch (...) { } catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables"; qCritical() << "error loading public key from environment variables";
return ErrorCode::ApiMissingAgwPublicKey; return ErrorCode::ApiMissingAgwPublicKey;
} }
@ -368,14 +448,16 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
} catch (...) { // todo change error handling in QSimpleCrypto? } catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
qCritical() << "error when encrypting the request body"; qCritical() << "error when encrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
} }
QJsonObject requestBody; QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
QNetworkReply* reply = manager.post(request, QJsonDocument(requestBody).toJson()); QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson());
QEventLoop wait; QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
@ -384,36 +466,43 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(); wait.exec();
if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) { auto encryptedResponseBody = reply->readAll();
if (m_proxyUrls.isEmpty()) {
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
m_proxyUrls = getProxyUrls(); m_proxyUrls = getProxyUrls();
} std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator);
for (const QString &proxyUrl : m_proxyUrls) { for (const QString &proxyUrl : m_proxyUrls) {
qDebug() << "Go to the next endpoint";
request.setUrl(QString("%1v1/config").arg(proxyUrl)); request.setUrl(QString("%1v1/config").arg(proxyUrl));
reply = manager.post(request, QJsonDocument(requestBody).toJson()); reply->deleteLater(); // delete the previous reply
reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson());
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(); wait.exec();
if (reply->error() != QNetworkReply::NetworkError::TimeoutError && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) {
encryptedResponseBody = reply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
break; break;
} }
reply->deleteLater();
} }
} }
auto errorCode = checkErrors(sslErrors, reply); auto errorCode = checkErrors(sslErrors, reply);
reply->deleteLater();
if (errorCode) { if (errorCode) {
return errorCode; return errorCode;
} }
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
try { try {
auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig); fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig);
} catch (...) { // todo change error handling in QSimpleCrypto? } catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
qCritical() << "error when decrypting the request body"; qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
} }
return errorCode; return errorCode;

View file

@ -14,14 +14,14 @@ class ApiController : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ApiController(const QString &gatewayEndpoint, QObject *parent = nullptr); explicit ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent = nullptr);
public slots: public slots:
void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig);
ErrorCode getServicesList(QByteArray &responseBody); ErrorCode getServicesList(QByteArray &responseBody);
ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType,
const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig); const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig);
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
@ -44,6 +44,7 @@ private:
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
QStringList m_proxyUrls; QStringList m_proxyUrls;
bool m_isDevEnvironment = false;
}; };
#endif // APICONTROLLER_H #endif // APICONTROLLER_H

View file

@ -83,7 +83,6 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr
} }
qDebug().noquote() << lineToExec; qDebug().noquote() << lineToExec;
Logger::appendSshLog("Run command:" + lineToExec);
error = m_sshClient.executeCommand(lineToExec, cbReadStdOut, cbReadStdErr); error = m_sshClient.executeCommand(lineToExec, cbReadStdOut, cbReadStdErr);
if (error != ErrorCode::NoError) { if (error != ErrorCode::NoError) {
@ -100,13 +99,13 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr) const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr)
{ {
QString fileName = "/opt/amnezia/" + Utils::getRandomString(16) + ".sh"; QString fileName = "/opt/amnezia/" + Utils::getRandomString(16) + ".sh";
Logger::appendSshLog("Run container script for " + ContainerProps::containerToString(container) + ":\n" + script);
ErrorCode e = uploadTextFileToContainer(container, credentials, script, fileName); ErrorCode e = uploadTextFileToContainer(container, credentials, script, fileName);
if (e) if (e)
return e; return e;
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash")); QString runner =
QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash"));
e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName); QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);
@ -426,7 +425,7 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
if (errorCode) if (errorCode)
return errorCode; return errorCode;
errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),dockerFilePath); errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(), dockerFilePath);
if (errorCode) if (errorCode)
return errorCode; return errorCode;
@ -437,7 +436,8 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
return ErrorCode::NoError; return ErrorCode::NoError;
}; };
errorCode = runScript(credentials, errorCode =
runScript(credentials,
replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)), replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)),
cbReadStdOut); cbReadStdOut);
if (errorCode) if (errorCode)
@ -621,13 +621,15 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
// Socks5 proxy vars // Socks5 proxy vars
vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } }); vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } });
auto username = socks5ProxyConfig.value(config_key:: userName).toString(); auto username = socks5ProxyConfig.value(config_key::userName).toString();
auto password = socks5ProxyConfig.value(config_key::password).toString(); auto password = socks5ProxyConfig.value(config_key::password).toString();
QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : ""; QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : "";
vars.append({ { "$SOCKS5_USER", socks5user } }); vars.append({ { "$SOCKS5_USER", socks5user } });
vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } }); vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } });
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); QString serverIp = (container != DockerContainer::Awg && container != DockerContainer::WireGuard && container != DockerContainer::Xray)
? NetworkUtilities::getIPAddress(credentials.hostName)
: credentials.hostName;
if (!serverIp.isEmpty()) { if (!serverIp.isEmpty()) {
vars.append({ { "$SERVER_IP_ADDRESS", serverIp } }); vars.append({ { "$SERVER_IP_ADDRESS", serverIp } });
} else { } else {
@ -713,7 +715,8 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
udpProtoScript.append("' | grep -i udp"); udpProtoScript.append("' | grep -i udp");
tcpProtoScript.append(" | grep LISTEN"); tcpProtoScript.append(" | grep LISTEN");
ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); ErrorCode errorCode =
runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
return errorCode; return errorCode;
} }

View file

@ -100,7 +100,13 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair<QStr
protocolConfigString = configurator->processConfigWithLocalSettings(dns, isApiConfig, protocolConfigString); protocolConfigString = configurator->processConfigWithLocalSettings(dns, isApiConfig, protocolConfigString);
QJsonObject vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); QJsonObject vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); if (container == DockerContainer::Awg || container == DockerContainer::WireGuard) {
// add mtu for old configs
if (vpnConfigData[config_key::mtu].toString().isEmpty()) {
vpnConfigData[config_key::mtu] = container == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
}
}
vpnConfiguration.insert(ProtocolProps::key_proto_config_data(proto), vpnConfigData); vpnConfiguration.insert(ProtocolProps::key_proto_config_data(proto), vpnConfigData);
} }

View file

@ -96,6 +96,7 @@ namespace amnezia
// import and install errors // import and install errors
ImportInvalidConfigError = 900, ImportInvalidConfigError = 900,
ImportOpenConfigError = 901,
// Android errors // Android errors
AndroidError = 1000, AndroidError = 1000,
@ -107,6 +108,8 @@ namespace amnezia
ApiConfigTimeoutError = 1103, ApiConfigTimeoutError = 1103,
ApiConfigSslError = 1104, ApiConfigSslError = 1104,
ApiMissingAgwPublicKey = 1105, ApiMissingAgwPublicKey = 1105,
ApiConfigDecryptionError = 1106,
ApiServicesMissingError = 1107,
// QFile errors // QFile errors
OpenError = 1200, OpenError = 1200,

View file

@ -50,6 +50,7 @@ QString errorString(ErrorCode code) {
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break; case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break; case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
// Android errors // Android errors
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
@ -61,6 +62,8 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break; case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break;
case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break;
case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break;
// QFile errors // QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;

View file

@ -29,6 +29,12 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
return Instance()->m_ipcClient; return Instance()->m_ipcClient;
} }
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
{
if (!Instance()) return nullptr;
return Instance()->m_Tun2SocksClient;
}
bool IpcClient::init(IpcClient *instance) bool IpcClient::init(IpcClient *instance)
{ {
m_instance = instance; m_instance = instance;
@ -44,6 +50,12 @@ bool IpcClient::init(IpcClient *instance)
qWarning() << "IpcClient replica is not connected!"; qWarning() << "IpcClient replica is not connected!";
} }
Instance()->m_Tun2SocksClient.reset(Instance()->m_ClientNode.acquire<IpcProcessTun2SocksReplica>());
Instance()->m_Tun2SocksClient->waitForSource(1000);
if (!Instance()->m_Tun2SocksClient->isReplicaValid()) {
qWarning() << "IpcClient::m_Tun2SocksClient replica is not connected!";
}
}); });
connect(Instance()->m_localSocket, &QLocalSocket::disconnected, [instance](){ connect(Instance()->m_localSocket, &QLocalSocket::disconnected, [instance](){
@ -51,16 +63,16 @@ bool IpcClient::init(IpcClient *instance)
}); });
Instance()->m_localSocket->connectToServer(amnezia::getIpcServiceUrl()); Instance()->m_localSocket->connectToServer(amnezia::getIpcServiceUrl());
Instance()->m_localSocket->waitForConnected(); Instance()->m_localSocket->waitForConnected();
if (!Instance()->m_ipcClient) { if (!Instance()->m_ipcClient) {
qDebug() << "IpcClient::init failed"; qDebug() << "IpcClient::init failed";
return false; return false;
} }
qDebug() << "IpcClient::init succeed"; qDebug() << "IpcClient::init succeed";
return Instance()->m_ipcClient->isReplicaValid(); return (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid());
} }
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess() QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()

View file

@ -6,6 +6,7 @@
#include "ipc.h" #include "ipc.h"
#include "rep_ipc_interface_replica.h" #include "rep_ipc_interface_replica.h"
#include "rep_ipc_process_tun2socks_replica.h"
#include "privileged_process.h" #include "privileged_process.h"
@ -18,6 +19,7 @@ public:
static IpcClient *Instance(); static IpcClient *Instance();
static bool init(IpcClient *instance); static bool init(IpcClient *instance);
static QSharedPointer<IpcInterfaceReplica> Interface(); static QSharedPointer<IpcInterfaceReplica> Interface();
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess(); static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
bool isSocketConnected() const; bool isSocketConnected() const;
@ -28,8 +30,11 @@ private:
~IpcClient() override; ~IpcClient() override;
QRemoteObjectNode m_ClientNode; QRemoteObjectNode m_ClientNode;
QRemoteObjectNode m_Tun2SocksNode;
QSharedPointer<IpcInterfaceReplica> m_ipcClient; QSharedPointer<IpcInterfaceReplica> m_ipcClient;
QPointer<QLocalSocket> m_localSocket; QPointer<QLocalSocket> m_localSocket;
QPointer<QLocalSocket> m_tun2socksSocket;
QSharedPointer<IpcProcessTun2SocksReplica> m_Tun2SocksClient;
struct ProcessDescriptor { struct ProcessDescriptor {
ProcessDescriptor () { ProcessDescriptor () {

View file

@ -116,7 +116,10 @@ QStringList NetworkUtilities::summarizeRoutes(const QStringList &ips, const QStr
QString NetworkUtilities::getIPAddress(const QString &host) QString NetworkUtilities::getIPAddress(const QString &host)
{ {
if (ipAddressRegExp().match(host).hasMatch()) { QHostAddress address(host);
if (QAbstractSocket::IPv4Protocol == address.protocol()) {
return host;
} else if (QAbstractSocket::IPv6Protocol == address.protocol()) {
return host; return host;
} }

View file

@ -78,7 +78,7 @@ bool Daemon::activate(const InterfaceConfig& config) {
return false; return false;
} }
if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { if (!dnsutils()->restoreResolvers()) {
return false; return false;
} }
@ -165,10 +165,6 @@ bool Daemon::activate(const InterfaceConfig& config) {
} }
bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) { bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) {
if (!supportDnsUtils()) {
return true;
}
if ((config.m_hopType == InterfaceConfig::MultiHopExit) || if ((config.m_hopType == InterfaceConfig::MultiHopExit) ||
(config.m_hopType == InterfaceConfig::SingleHop)) { (config.m_hopType == InterfaceConfig::SingleHop)) {
QList<QHostAddress> resolvers; QList<QHostAddress> resolvers;
@ -423,13 +419,8 @@ bool Daemon::deactivate(bool emitSignals) {
} }
// Cleanup DNS // Cleanup DNS
if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { if (!dnsutils()->restoreResolvers()) {
return false; logger.warning() << "Failed to restore DNS resolvers.";
}
if (!wgutils()->interfaceExists()) {
logger.warning() << "Wireguard interface does not exist.";
return false;
} }
// Cleanup peers and routing // Cleanup peers and routing
@ -449,13 +440,9 @@ bool Daemon::deactivate(bool emitSignals) {
} }
m_excludedAddrSet.clear(); m_excludedAddrSet.clear();
// Delete the interface
if (!wgutils()->deleteInterface()) {
return false;
}
m_connections.clear(); m_connections.clear();
return true; // Delete the interface
return wgutils()->deleteInterface();
} }
QString Daemon::logs() { QString Daemon::logs() {

View file

@ -69,7 +69,6 @@ class Daemon : public QObject {
virtual WireguardUtils* wgutils() const = 0; virtual WireguardUtils* wgutils() const = 0;
virtual bool supportIPUtils() const { return false; } virtual bool supportIPUtils() const { return false; }
virtual IPUtils* iputils() { return nullptr; } virtual IPUtils* iputils() { return nullptr; }
virtual bool supportDnsUtils() const { return false; }
virtual DnsUtils* dnsutils() { return nullptr; } virtual DnsUtils* dnsutils() { return nullptr; }
static bool parseStringList(const QJsonObject& obj, const QString& name, static bool parseStringList(const QJsonObject& obj, const QString& name,

View file

@ -92,6 +92,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
logger.debug() << "Command received:" << type; logger.debug() << "Command received:" << type;
// It is expected that sometimes the client will request backend logs
// before the first authentication. In these cases we just return empty
// logs.
if (type == "logs") {
QJsonObject obj;
obj.insert("type", "logs");
obj.insert("logs", "");
write(obj);
return;
}
if (type == "activate") { if (type == "activate") {
InterfaceConfig config; InterfaceConfig config;
if (!Daemon::parseConfig(obj, config)) { if (!Daemon::parseConfig(obj, config)) {
@ -115,8 +126,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
if (type == "status") { if (type == "status") {
QJsonObject obj = Daemon::instance()->getStatus(); QJsonObject obj = Daemon::instance()->getStatus();
obj.insert("type", "status"); obj.insert("type", "status");
m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); write(obj);
m_socket->write("\n");
return; return;
} }
@ -124,8 +134,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
QJsonObject obj; QJsonObject obj;
obj.insert("type", "logs"); obj.insert("type", "logs");
obj.insert("logs", Daemon::instance()->logs().replace("\n", "|")); obj.insert("logs", Daemon::instance()->logs().replace("\n", "|"));
m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); write(obj);
m_socket->write("\n");
return; return;
} }

View file

@ -1,107 +0,0 @@
#ifndef LOGGER_H
#define LOGGER_H
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QString>
#include <QTextStream>
#include "ui/property_helper.h"
#include "mozilla/shared/loglevel.h"
class Logger : public QObject
{
Q_OBJECT
AUTO_PROPERTY(QString, sshLog)
AUTO_PROPERTY(QString, allLog)
public:
static Logger& Instance();
static void appendSshLog(const QString &log);
static void appendAllLog(const QString &log);
static bool init();
static void deInit();
static bool setServiceLogsEnabled(bool enabled);
static bool openLogsFolder();
static bool openServiceLogsFolder();
static QString appLogFileNamePath();
static void clearLogs();
static void clearServiceLogs();
static void cleanUp();
static QString userLogsFilePath();
static QString getLogFile();
// compat with Mozilla logger
Logger(const QString &className) { m_className = className; }
const QString& className() const { return m_className; }
class Log {
public:
Log(Logger* logger, LogLevel level);
~Log();
Log& operator<<(uint64_t t);
Log& operator<<(const char* t);
Log& operator<<(const QString& t);
Log& operator<<(const QStringList& t);
Log& operator<<(const QByteArray& t);
Log& operator<<(const QJsonObject& t);
Log& operator<<(QTextStreamFunction t);
Log& operator<<(const void* t);
// Q_ENUM
template <typename T>
typename std::enable_if<QtPrivate::IsQEnumHelper<T>::Value, Log&>::type
operator<<(T t) {
const QMetaObject* meta = qt_getEnumMetaObject(t);
const char* name = qt_getEnumName(t);
addMetaEnum(typename QFlags<T>::Int(t), meta, name);
return *this;
}
private:
void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name);
Logger* m_logger;
LogLevel m_logLevel;
struct Data {
Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {}
QString m_buffer;
QTextStream m_ts;
};
Data* m_data;
};
Log error();
Log warning();
Log info();
Log debug();
QString sensitive(const QString& input);
private:
Logger() {}
Logger(Logger const &) = delete;
Logger& operator= (Logger const&) = delete;
static QString userLogsDir();
static QFile m_file;
static QTextStream m_textStream;
static QString m_logFileName;
friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg);
// compat with Mozilla logger
QString m_className;
};
#endif // LOGGER_H

View file

@ -15,13 +15,24 @@
#include "platforms/ios/QtAppDelegate-C-Interface.h" #include "platforms/ios/QtAppDelegate-C-Interface.h"
#endif #endif
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
bool isAnotherInstanceRunning()
{
QLocalSocket socket;
socket.connectToServer("AmneziaVPNInstance");
if (socket.waitForConnected(500)) {
qWarning() << "AmneziaVPN is already running";
return true;
}
return false;
}
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
Migrations migrationsManager; Migrations migrationsManager;
migrationsManager.doMigrations(); migrationsManager.doMigrations();
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
AllowSetForegroundWindow(ASFW_ANY); AllowSetForegroundWindow(ASFW_ANY);
#endif #endif
@ -32,16 +43,14 @@ int main(int argc, char *argv[])
qputenv("ANDROID_OPENSSL_SUFFIX", "_3"); qputenv("ANDROID_OPENSSL_SUFFIX", "_3");
#endif #endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
AmneziaApplication app(argc, argv); AmneziaApplication app(argc, argv);
#else
AmneziaApplication app(argc, argv, true,
SingleApplication::Mode::User | SingleApplication::Mode::SecondaryNotification);
if (!app.isPrimary()) { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (isAnotherInstanceRunning()) {
QTimer::singleShot(1000, &app, [&]() { app.quit(); }); QTimer::singleShot(1000, &app, [&]() { app.quit(); });
return app.exec(); return app.exec();
} }
app.startLocalServer();
#endif #endif
// Allow to raise app window if secondary instance launched // Allow to raise app window if secondary instance launched

View file

@ -35,7 +35,7 @@ LocalSocketController::LocalSocketController() {
connect(m_socket, &QLocalSocket::connected, this, connect(m_socket, &QLocalSocket::connected, this,
&LocalSocketController::daemonConnected); &LocalSocketController::daemonConnected);
connect(m_socket, &QLocalSocket::disconnected, this, connect(m_socket, &QLocalSocket::disconnected, this,
&LocalSocketController::disconnected); [&] { errorOccurred(QLocalSocket::PeerClosedError); });
connect(m_socket, &QLocalSocket::errorOccurred, this, connect(m_socket, &QLocalSocket::errorOccurred, this,
&LocalSocketController::errorOccurred); &LocalSocketController::errorOccurred);
connect(m_socket, &QLocalSocket::readyRead, this, connect(m_socket, &QLocalSocket::readyRead, this,
@ -149,7 +149,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
QJsonArray jsAllowedIPAddesses; QJsonArray jsAllowedIPAddesses;
QJsonArray plainAllowedIP = wgConfig.value(amnezia::config_key::allowed_ips).toArray(); QJsonArray plainAllowedIP = wgConfig.value(amnezia::config_key::allowed_ips).toArray();
QJsonArray defaultAllowedIP = QJsonArray::fromStringList(QString("0.0.0.0/0, ::/0").split(",")); QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" };
if (plainAllowedIP != defaultAllowedIP && !plainAllowedIP.isEmpty()) { if (plainAllowedIP != defaultAllowedIP && !plainAllowedIP.isEmpty()) {
// Use AllowedIP list from WG config because of higher priority // Use AllowedIP list from WG config because of higher priority

View file

@ -98,6 +98,7 @@ bool AndroidController::initialize()
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)}, {"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)} {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
}; };
@ -210,6 +211,11 @@ void AndroidController::setScreenshotsEnabled(bool enabled)
callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled); callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled);
} }
void AndroidController::setNavigationBarColor(unsigned int color)
{
callActivityMethod("setNavigationBarColor", "(I)V", color);
}
void AndroidController::minimizeApp() void AndroidController::minimizeApp()
{ {
callActivityMethod("minimizeApp", "()V"); callActivityMethod("minimizeApp", "()V");
@ -265,6 +271,22 @@ void AndroidController::requestNotificationPermission()
callActivityMethod("requestNotificationPermission", "()V"); callActivityMethod("requestNotificationPermission", "()V");
} }
bool AndroidController::requestAuthentication()
{
QEventLoop wait;
bool result;
connect(this, &AndroidController::authenticationResult, this,
[&result, &wait](const bool &authResult){
qDebug() << "Android authentication result:" << authResult;
result = authResult;
wait.quit();
},
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
callActivityMethod("requestAuthentication", "()V");
wait.exec();
return result;
}
// Moving log processing to the Android side // Moving log processing to the Android side
jclass AndroidController::log; jclass AndroidController::log;
jmethodID AndroidController::logDebug; jmethodID AndroidController::logDebug;
@ -462,6 +484,14 @@ void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data
emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data)); emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data));
} }
// static
void AndroidController::onAuthResult(JNIEnv *env, jobject thiz, jboolean result)
{
Q_UNUSED(thiz);
emit AndroidController::instance()->authenticationResult(result);
}
// static // static
bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data) bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
{ {

View file

@ -41,11 +41,13 @@ public:
void exportLogsFile(const QString &fileName); void exportLogsFile(const QString &fileName);
void clearLogs(); void clearLogs();
void setScreenshotsEnabled(bool enabled); void setScreenshotsEnabled(bool enabled);
void setNavigationBarColor(unsigned int color);
void minimizeApp(); void minimizeApp();
QJsonArray getAppList(); QJsonArray getAppList();
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted(); bool isNotificationPermissionGranted();
void requestNotificationPermission(); void requestNotificationPermission();
bool requestAuthentication();
static bool initLogging(); static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@ -63,6 +65,7 @@ signals:
void configImported(QString config); void configImported(QString config);
void importConfigFromOutside(QString config); void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state); void initConnectionState(Vpn::ConnectionState state);
void authenticationResult(bool result);
private: private:
bool isWaitingStatus = true; bool isWaitingStatus = true;
@ -89,6 +92,7 @@ private:
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
template <typename Ret, typename ...Args> template <typename Ret, typename ...Args>

View file

@ -1,16 +0,0 @@
#include "authResultReceiver.h"
AuthResultReceiver::AuthResultReceiver(QSharedPointer<AuthResultNotifier> &notifier) : m_notifier(notifier)
{
}
void AuthResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data)
{
qDebug() << "receiverRequestCode" << receiverRequestCode << "resultCode" << resultCode;
if (resultCode == -1) { // ResultOK
emit m_notifier->authSuccessful();
} else {
emit m_notifier->authFailed();
}
}

View file

@ -1,32 +0,0 @@
#ifndef AUTHRESULTRECEIVER_H
#define AUTHRESULTRECEIVER_H
#include <QJniObject>
#include <private/qandroidextras_p.h>
class AuthResultNotifier : public QObject
{
Q_OBJECT
public:
AuthResultNotifier(QObject *parent = nullptr) : QObject(parent) {};
signals:
void authFailed();
void authSuccessful();
};
/* Auth result handler for Android */
class AuthResultReceiver final : public QAndroidActivityResultReceiver
{
public:
AuthResultReceiver(QSharedPointer<AuthResultNotifier> &notifier);
void handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) override;
private:
QSharedPointer<AuthResultNotifier> m_notifier;
};
#endif // AUTHRESULTRECEIVER_H

View file

@ -351,8 +351,6 @@ void IosController::vpnStatusDidChange(void *pNotification)
} }
} }
} }
} else {
qDebug() << "Disconnect error is absent";
} }
}]; }];
} else { } else {
@ -501,6 +499,20 @@ bool IosController::setupWireGuard()
wgConfig.insert(config_key::persistent_keep_alive, "25"); wgConfig.insert(config_key::persistent_keep_alive, "25");
} }
if (config.contains(config_key::isObfuscationEnabled) && config.value(config_key::isObfuscationEnabled).toBool()) {
wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]);
wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]);
wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]);
wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]);
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
}
QJsonDocument wgConfigDoc(wgConfig); QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact)); QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
@ -835,7 +847,7 @@ QString IosController::openFile() {
void IosController::requestInetAccess() { void IosController::requestInetAccess() {
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"]; NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
if (url) { if (!url) {
qDebug() << "IosController::requestInetAccess URL error"; qDebug() << "IosController::requestInetAccess URL error";
return; return;
} }
@ -847,7 +859,6 @@ void IosController::requestInetAccess() {
} else { } else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length); QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length);
qDebug() << "IosController::requestInetAccess server response:" << httpResponse.statusCode << "\n\n" <<responseBody;
} }
}]; }];
[task resume]; [task resume];

View file

@ -22,7 +22,6 @@ class LinuxDaemon final : public Daemon {
protected: protected:
WireguardUtils* wgutils() const override { return m_wgutils; } WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; } DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; } bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; } IPUtils* iputils() override { return m_iputils; }

View file

@ -21,7 +21,6 @@ class MacOSDaemon final : public Daemon {
protected: protected:
WireguardUtils* wgutils() const override { return m_wgutils; } WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; } DnsUtils* dnsutils() override { return m_dnsutils; }
bool supportIPUtils() const override { return true; } bool supportIPUtils() const override { return true; }
IPUtils* iputils() override { return m_iputils; } IPUtils* iputils() override { return m_iputils; }

View file

@ -26,7 +26,6 @@ class WindowsDaemon final : public Daemon {
protected: protected:
bool run(Op op, const InterfaceConfig& config) override; bool run(Op op, const InterfaceConfig& config) override;
WireguardUtils* wgutils() const override { return m_wgutils; } WireguardUtils* wgutils() const override { return m_wgutils; }
bool supportDnsUtils() const override { return true; }
DnsUtils* dnsutils() override { return m_dnsutils; } DnsUtils* dnsutils() override { return m_dnsutils; }
private: private:

View file

@ -502,7 +502,7 @@ QString WindowsSplitTunnel::convertPath(const QString& path) {
// device should contain : for e.g C: // device should contain : for e.g C:
return ""; return "";
} }
QByteArray buffer(2048, 0xFF); QByteArray buffer(2048, 0xFFu);
auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter), auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter),
(wchar_t*)buffer.data(), buffer.size() / 2); (wchar_t*)buffer.data(), buffer.size() / 2);

View file

@ -248,7 +248,7 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) {
} }
if (result != NO_ERROR) { if (result != NO_ERROR) {
logger.error() << "Failed to create route to" logger.error() << "Failed to create route to"
<< logger.sensitive(prefix.toString()) << prefix.toString()
<< "result:" << result; << "result:" << result;
} }
return result == NO_ERROR; return result == NO_ERROR;
@ -265,7 +265,7 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) {
} }
if (result != NO_ERROR) { if (result != NO_ERROR) {
logger.error() << "Failed to delete route to" logger.error() << "Failed to delete route to"
<< logger.sensitive(prefix.toString()) << prefix.toString()
<< "result:" << result; << "result:" << result;
} }
return result == NO_ERROR; return result == NO_ERROR;

View file

@ -21,7 +21,7 @@
#include "platforms/windows/windowsutils.h" #include "platforms/windows/windowsutils.h"
constexpr const char* VPN_NAME = "AmneziaVPN"; constexpr const char* VPN_NAME = "AmneziaVPN";
constexpr const char* WIREGUARD_DIR = "WireGuard"; constexpr const char* WIREGUARD_DIR = "AmneziaWG";
constexpr const char* DATA_DIR = "Data"; constexpr const char* DATA_DIR = "Data";
namespace { namespace {

View file

@ -6,6 +6,7 @@
#include <QTcpSocket> #include <QTcpSocket>
#include <QNetworkInterface> #include <QNetworkInterface>
#include "core/networkUtilities.h"
#include "logger.h" #include "logger.h"
#include "openvpnprotocol.h" #include "openvpnprotocol.h"
#include "utilities.h" #include "utilities.h"
@ -127,7 +128,6 @@ void OpenVpnProtocol::sendManagementCommand(const QString &command)
uint OpenVpnProtocol::selectMgmtPort() uint OpenVpnProtocol::selectMgmtPort()
{ {
for (int i = 0; i < 100; ++i) { for (int i = 0; i < 100; ++i) {
quint32 port = QRandomGenerator::global()->generate(); quint32 port = QRandomGenerator::global()->generate();
port = (double)(65000 - 15001) * port / UINT32_MAX + 15001; port = (double)(65000 - 15001) * port / UINT32_MAX + 15001;
@ -137,7 +137,6 @@ uint OpenVpnProtocol::selectMgmtPort()
if (ok) if (ok)
return port; return port;
} }
return m_managementPort; return m_managementPort;
} }
@ -343,7 +342,8 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line)
} }
m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index()); m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index());
m_configData.insert("vpnGateway", m_vpnGateway); m_configData.insert("vpnGateway", m_vpnGateway);
m_configData.insert("vpnServer", m_configData.value(amnezia::config_key::hostName).toString()); m_configData.insert("vpnServer",
NetworkUtilities::getIPAddress(m_configData.value(amnezia::config_key::hostName).toString()));
IpcClient::Interface()->enablePeerTraffic(m_configData); IpcClient::Interface()->enablePeerTraffic(m_configData);
} }
} }
@ -352,6 +352,8 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line)
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
// killSwitch toggle // killSwitch toggle
if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) {
m_configData.insert("vpnServer",
NetworkUtilities::getIPAddress(m_configData.value(amnezia::config_key::hostName).toString()));
IpcClient::Interface()->enableKillSwitch(m_configData, 0); IpcClient::Interface()->enableKillSwitch(m_configData, 0);
} }
#endif #endif

View file

@ -66,6 +66,7 @@ namespace amnezia
constexpr char last_config[] = "last_config"; constexpr char last_config[] = "last_config";
constexpr char isThirdPartyConfig[] = "isThirdPartyConfig"; constexpr char isThirdPartyConfig[] = "isThirdPartyConfig";
constexpr char isObfuscationEnabled[] = "isObfuscationEnabled";
constexpr char junkPacketCount[] = "Jc"; constexpr char junkPacketCount[] = "Jc";
constexpr char junkPacketMinSize[] = "Jmin"; constexpr char junkPacketMinSize[] = "Jmin";

View file

@ -4,9 +4,8 @@
#include <QTcpSocket> #include <QTcpSocket>
#include <QThread> #include <QThread>
#include "logger.h"
#include "utilities.h"
#include "wireguardprotocol.h" #include "wireguardprotocol.h"
#include "core/networkUtilities.h"
#include "mozilla/localsocketcontroller.h" #include "mozilla/localsocketcontroller.h"
@ -37,6 +36,12 @@ void WireguardProtocol::stop()
ErrorCode WireguardProtocol::startMzImpl() ErrorCode WireguardProtocol::startMzImpl()
{ {
QString protocolName = m_rawConfig.value("protocol").toString();
QJsonObject vpnConfigData = m_rawConfig.value(protocolName + "_config_data").toObject();
vpnConfigData[config_key::hostName] = NetworkUtilities::getIPAddress(vpnConfigData.value(config_key::hostName).toString());
m_rawConfig.insert(protocolName + "_config_data", vpnConfigData);
m_rawConfig[config_key::hostName] = NetworkUtilities::getIPAddress(m_rawConfig[config_key::hostName].toString());
m_impl->activate(m_rawConfig); m_impl->activate(m_rawConfig);
return ErrorCode::NoError; return ErrorCode::NoError;
} }

78
client/protocols/xrayprotocol.cpp Normal file → Executable file
View file

@ -17,6 +17,7 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent):
m_routeGateway = NetworkUtilities::getGatewayAndIface(); m_routeGateway = NetworkUtilities::getGatewayAndIface();
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr; m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr; m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr;
m_t2sProcess = IpcClient::InterfaceTun2Socks();
} }
XrayProtocol::~XrayProtocol() XrayProtocol::~XrayProtocol()
@ -43,7 +44,9 @@ ErrorCode XrayProtocol::start()
m_xrayCfgFile.setAutoRemove(false); m_xrayCfgFile.setAutoRemove(false);
#endif #endif
m_xrayCfgFile.open(); m_xrayCfgFile.open();
m_xrayCfgFile.write(QJsonDocument(m_xrayConfig).toJson()); QString config = QJsonDocument(m_xrayConfig).toJson();
config.replace(m_remoteHost, m_remoteAddress);
m_xrayCfgFile.write(config.toUtf8());
m_xrayCfgFile.close(); m_xrayCfgFile.close();
QStringList args = QStringList() << "-c" << m_xrayCfgFile.fileName() << "-format=json"; QStringList args = QStringList() << "-c" << m_xrayCfgFile.fileName() << "-format=json";
@ -63,7 +66,7 @@ ErrorCode XrayProtocol::start()
}); });
connect(&m_xrayProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { connect(&m_xrayProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
qDebug().noquote() << "XrayProtocol finished, exitCode, exiStatus" << exitCode << exitStatus; qDebug().noquote() << "XrayProtocol finished, exitCode, exitStatus" << exitCode << exitStatus;
setConnectionState(Vpn::ConnectionState::Disconnected); setConnectionState(Vpn::ConnectionState::Disconnected);
if (exitStatus != QProcess::NormalExit) { if (exitStatus != QProcess::NormalExit) {
emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed);
@ -89,63 +92,32 @@ ErrorCode XrayProtocol::start()
ErrorCode XrayProtocol::startTun2Sock() ErrorCode XrayProtocol::startTun2Sock()
{ {
if (!QFileInfo::exists(Utils::tun2socksPath())) { m_t2sProcess->start();
setLastError(ErrorCode::Tun2SockExecutableMissing);
return lastError();
}
m_t2sProcess = IpcClient::CreatePrivilegedProcess();
if (!m_t2sProcess) {
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_t2sProcess->waitForSource(1000);
if (!m_t2sProcess->isInitialized()) {
qWarning() << "IpcProcess replica is not connected!";
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
return ErrorCode::AmneziaServiceConnectionFailed;
}
QString XrayConStr = "socks5://127.0.0.1:" + QString::number(m_localPort);
m_t2sProcess->setProgram(PermittedProcess::Tun2Socks);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress))); m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress)));
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up",
QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255").arg(amnezia::protocols::xray::defaultLocalAddr)});
#endif #endif
#ifdef Q_OS_LINUX
QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr});
#endif
#ifdef Q_OS_MAC
QStringList arguments({"-device", "utun22", "-proxy", XrayConStr});
#endif
m_t2sProcess->setArguments(arguments);
qDebug() << arguments.join(" "); connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this,
connect(m_t2sProcess.data(), &PrivilegedProcess::errorOccurred, [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
[&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; });
connect(m_t2sProcess.data(), &PrivilegedProcess::stateChanged, connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this,
[&](QProcess::ProcessState newState) { [&](int vpnState) {
qDebug() << "PrivilegedProcess stateChanged" << newState; qDebug() << "PrivilegedProcess setConnectionState " << vpnState;
if (newState == QProcess::Running) if (vpnState == Vpn::ConnectionState::Connected)
{ {
setConnectionState(Vpn::ConnectionState::Connecting); setConnectionState(Vpn::ConnectionState::Connecting);
QList<QHostAddress> dnsAddr; QList<QHostAddress> dnsAddr;
dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns1).toString())); dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns1).toString()));
dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns2).toString())); dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns2).toString()));
#ifdef Q_OS_WIN
QThread::msleep(8000);
#endif
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
QThread::msleep(5000); QThread::msleep(5000);
IpcClient::Interface()->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr); IpcClient::Interface()->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr);
IpcClient::Interface()->updateResolvers("utun22", dnsAddr); IpcClient::Interface()->updateResolvers("utun22", dnsAddr);
#endif #endif
#ifdef Q_OS_WINDOWS
QThread::msleep(15000);
#endif
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
QThread::msleep(1000); QThread::msleep(1000);
IpcClient::Interface()->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); IpcClient::Interface()->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr);
@ -154,6 +126,7 @@ ErrorCode XrayProtocol::startTun2Sock()
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
// killSwitch toggle // killSwitch toggle
if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) {
m_configData.insert("vpnServer", m_remoteAddress);
IpcClient::Interface()->enableKillSwitch(m_configData, 0); IpcClient::Interface()->enableKillSwitch(m_configData, 0);
} }
#endif #endif
@ -167,7 +140,7 @@ ErrorCode XrayProtocol::startTun2Sock()
IpcClient::Interface()->updateResolvers("tun2", dnsAddr); IpcClient::Interface()->updateResolvers("tun2", dnsAddr);
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces(); QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (int i = 0; i < netInterfaces.size(); i++) { for (int i = 0; i < netInterfaces.size(); i++) {
for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++) for (int j = 0; j < netInterfaces.at(i).addressEntries().size(); j++)
{ {
// killSwitch toggle // killSwitch toggle
if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) {
@ -184,21 +157,15 @@ ErrorCode XrayProtocol::startTun2Sock()
#endif #endif
setConnectionState(Vpn::ConnectionState::Connected); setConnectionState(Vpn::ConnectionState::Connected);
} }
});
#if !defined(Q_OS_MACOS) #if !defined(Q_OS_MACOS)
connect(m_t2sProcess.data(), &PrivilegedProcess::finished, this, if (vpnState == Vpn::ConnectionState::Disconnected) {
[&]() {
setConnectionState(Vpn::ConnectionState::Disconnected); setConnectionState(Vpn::ConnectionState::Disconnected);
IpcClient::Interface()->deleteTun("tun2"); IpcClient::Interface()->deleteTun("tun2");
IpcClient::Interface()->StartRoutingIpv6(); IpcClient::Interface()->StartRoutingIpv6();
IpcClient::Interface()->clearSavedRoutes(); IpcClient::Interface()->clearSavedRoutes();
}); }
#endif #endif
});
m_t2sProcess->start();
return ErrorCode::NoError; return ErrorCode::NoError;
} }
@ -212,7 +179,7 @@ void XrayProtocol::stop()
qDebug() << "XrayProtocol::stop()"; qDebug() << "XrayProtocol::stop()";
m_xrayProcess.terminate(); m_xrayProcess.terminate();
if (m_t2sProcess) { if (m_t2sProcess) {
m_t2sProcess->close(); m_t2sProcess->stop();
} }
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
@ -238,7 +205,8 @@ void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration)
} }
m_xrayConfig = xrayConfiguration; m_xrayConfig = xrayConfiguration;
m_localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort).toInt(); m_localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort).toInt();
m_remoteAddress = configuration.value(amnezia::config_key::hostName).toString(); m_remoteHost = configuration.value(amnezia::config_key::hostName).toString();
m_remoteAddress = NetworkUtilities::getIPAddress(m_remoteHost);
m_routeMode = configuration.value(amnezia::config_key::splitTunnelType).toInt(); m_routeMode = configuration.value(amnezia::config_key::splitTunnelType).toInt();
m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString(); m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString();
m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString(); m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString();

View file

@ -26,6 +26,7 @@ private:
static QString tun2SocksExecPath(); static QString tun2SocksExecPath();
private: private:
int m_localPort; int m_localPort;
QString m_remoteHost;
QString m_remoteAddress; QString m_remoteAddress;
int m_routeMode; int m_routeMode;
QJsonObject m_configData; QJsonObject m_configData;
@ -33,9 +34,10 @@ private:
QString m_secondaryDNS; QString m_secondaryDNS;
#ifndef Q_OS_IOS #ifndef Q_OS_IOS
QProcess m_xrayProcess; QProcess m_xrayProcess;
QSharedPointer<PrivilegedProcess> m_t2sProcess; QSharedPointer<IpcProcessTun2SocksReplica> m_t2sProcess;
#endif #endif
QTemporaryFile m_xrayCfgFile; QTemporaryFile m_xrayCfgFile;
}; };
#endif // XRAYPROTOCOL_H #endif // XRAYPROTOCOL_H

View file

@ -200,6 +200,8 @@
<file>server_scripts/socks5_proxy/configure_container.sh</file> <file>server_scripts/socks5_proxy/configure_container.sh</file>
<file>server_scripts/socks5_proxy/start.sh</file> <file>server_scripts/socks5_proxy/start.sh</file>
<file>server_scripts/ipsec/template.conf</file> <file>server_scripts/ipsec/template.conf</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file> <file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file> <file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
<file>ui/qml/Controls2/CardWithIconsType.qml</file> <file>ui/qml/Controls2/CardWithIconsType.qml</file>

View file

@ -174,13 +174,25 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json)
QByteArray SecureQSettings::encryptText(const QByteArray &value) const QByteArray SecureQSettings::encryptText(const QByteArray &value) const
{ {
QSimpleCrypto::QBlockCipher cipher; QSimpleCrypto::QBlockCipher cipher;
return cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); QByteArray result;
try {
result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv());
} catch (...) { // todo change error handling in QSimpleCrypto?
qCritical() << "error when encrypting the settings value";
}
return result;
} }
QByteArray SecureQSettings::decryptText(const QByteArray &ba) const QByteArray SecureQSettings::decryptText(const QByteArray &ba) const
{ {
QSimpleCrypto::QBlockCipher cipher; QSimpleCrypto::QBlockCipher cipher;
return cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); QByteArray result;
try {
result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv());
} catch (...) { // todo change error handling in QSimpleCrypto?
qCritical() << "error when decrypting the settings value";
}
return result;
} }
bool SecureQSettings::encryptionRequired() const bool SecureQSettings::encryptionRequired() const

View file

@ -13,5 +13,5 @@ sudo docker network connect amnezia-dns-net $CONTAINER_NAME
sudo docker exec -i $CONTAINER_NAME bash -c 'mkdir -p /dev/net; if [ ! -c /dev/net/tun ]; then mknod /dev/net/tun c 10 200; fi' sudo docker exec -i $CONTAINER_NAME bash -c 'mkdir -p /dev/net; if [ ! -c /dev/net/tun ]; then mknod /dev/net/tun c 10 200; fi'
# Prevent to route packets outside of the container in case if server behind of the NAT # Prevent to route packets outside of the container in case if server behind of the NAT
sudo docker exec -i $CONTAINER_NAME sh -c "ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up" #sudo docker exec -i $CONTAINER_NAME sh -c "ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up"

View file

@ -3,7 +3,7 @@
# This scripts copied from Amnezia client to Docker container to /opt/amnezia and launched every time container starts # This scripts copied from Amnezia client to Docker container to /opt/amnezia and launched every time container starts
echo "Container startup" echo "Container startup"
ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up #ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up
iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

View file

@ -227,7 +227,7 @@ void Settings::setSaveLogs(bool enabled)
if (!isSaveLogs()) { if (!isSaveLogs()) {
Logger::deInit(); Logger::deInit();
} else { } else {
if (!Logger::init()) { if (!Logger::init(false)) {
qWarning() << "Initialization of debug subsystem failed"; qWarning() << "Initialization of debug subsystem failed";
} }
} }
@ -519,7 +519,22 @@ void Settings::setGatewayEndpoint(const QString &endpoint)
m_gatewayEndpoint = endpoint; m_gatewayEndpoint = endpoint;
} }
void Settings::setDevGatewayEndpoint()
{
m_gatewayEndpoint = DEV_AGW_ENDPOINT;
}
QString Settings::getGatewayEndpoint() QString Settings::getGatewayEndpoint()
{ {
return m_gatewayEndpoint; return m_gatewayEndpoint;
} }
bool Settings::isDevGatewayEnv()
{
return m_isDevGatewayEnv;
}
void Settings::toggleDevGatewayEnv(bool enabled)
{
m_isDevGatewayEnv = enabled;
}

View file

@ -183,7 +183,7 @@ public:
bool isScreenshotsEnabled() const bool isScreenshotsEnabled() const
{ {
return value("Conf/screenshotsEnabled", false).toBool(); return value("Conf/screenshotsEnabled", true).toBool();
} }
void setScreenshotsEnabled(bool enabled) void setScreenshotsEnabled(bool enabled)
{ {
@ -217,7 +217,10 @@ public:
void resetGatewayEndpoint(); void resetGatewayEndpoint();
void setGatewayEndpoint(const QString &endpoint); void setGatewayEndpoint(const QString &endpoint);
void setDevGatewayEndpoint();
QString getGatewayEndpoint(); QString getGatewayEndpoint();
bool isDevGatewayEnv();
void toggleDevGatewayEnv(bool enabled);
signals: signals:
void saveLogsChanged(bool enabled); void saveLogsChanged(bool enabled);
@ -234,6 +237,7 @@ private:
mutable SecureQSettings m_settings; mutable SecureQSettings m_settings;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
bool m_isDevGatewayEnv = false;
}; };
#endif // SETTINGS_H #endif // SETTINGS_H

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,47 +4,52 @@
<context> <context>
<name>ApiServicesModel</name> <name>ApiServicesModel</name>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="63"/> <location filename="../ui/models/apiServicesModel.cpp" line="65"/>
<source>Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s</source> <source>Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="67"/> <location filename="../ui/models/apiServicesModel.cpp" line="69"/>
<source>VPN to access blocked sites in regions with high levels of Internet censorship. </source> <source>VPN to access blocked sites in regions with high levels of Internet censorship. </source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="72"/> <location filename="../ui/models/apiServicesModel.cpp" line="71"/>
<source>&lt;p&gt;&lt;a style=&quot;color: #EB5757;&quot;&gt;Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/models/apiServicesModel.cpp" line="78"/>
<source>Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship.</source> <source>Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="75"/> <location filename="../ui/models/apiServicesModel.cpp" line="81"/>
<source>Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship</source> <source>Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="80"/> <location filename="../ui/models/apiServicesModel.cpp" line="94"/>
<source>%1 MBit/s</source> <source>%1 MBit/s</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="87"/> <location filename="../ui/models/apiServicesModel.cpp" line="101"/>
<source>%1 days</source> <source>%1 days</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="96"/> <location filename="../ui/models/apiServicesModel.cpp" line="110"/>
<source>VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, &lt;a href=&quot;%1/free&quot; style=&quot;color: #FBB26A;&quot;&gt;more details on the website.&lt;/a&gt;</source> <source>VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, &lt;a href=&quot;%1/free&quot; style=&quot;color: #FBB26A;&quot;&gt;more details on the website.&lt;/a&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="104"/> <location filename="../ui/models/apiServicesModel.cpp" line="118"/>
<source>Free</source> <source>Free</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="106"/> <location filename="../ui/models/apiServicesModel.cpp" line="120"/>
<source>%1 $/month</source> <source>%1 $/month</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -75,7 +80,7 @@
<context> <context>
<name>ConnectButton</name> <name>ConnectButton</name>
<message> <message>
<location filename="../ui/qml/Components/ConnectButton.qml" line="27"/> <location filename="../ui/qml/Components/ConnectButton.qml" line="28"/>
<source>Unable to disconnect during configuration preparation</source> <source>Unable to disconnect during configuration preparation</source>
<translation>ि ि </translation> <translation>ि ि </translation>
</message> </message>
@ -187,9 +192,8 @@
<context> <context>
<name>ExportController</name> <name>ExportController</name>
<message> <message>
<location filename="../ui/controllers/exportController.cpp" line="30"/>
<source>Access error!</source> <source>Access error!</source>
<translation> ि!</translation> <translation type="vanished"> ि!</translation>
</message> </message>
</context> </context>
<context> <context>
@ -255,18 +259,18 @@ Can&apos;t be disabled for current server</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/importController.cpp" line="186"/> <location filename="../ui/controllers/importController.cpp" line="187"/>
<location filename="../ui/controllers/importController.cpp" line="191"/> <location filename="../ui/controllers/importController.cpp" line="192"/>
<source>Invalid configuration file</source> <source>Invalid configuration file</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/importController.cpp" line="606"/> <location filename="../ui/controllers/importController.cpp" line="617"/>
<source>Scanned %1 of %2.</source> <source>Scanned %1 of %2.</source>
<translation>%2 %1 ि .</translation> <translation>%2 %1 ि .</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/importController.cpp" line="641"/> <location filename="../ui/controllers/importController.cpp" line="652"/>
<source>In the imported configuration, potentially dangerous lines were found:</source> <source>In the imported configuration, potentially dangerous lines were found:</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -443,6 +447,11 @@ Already installed containers were found on the server. All installed containers
<source>Gateway endpoint</source> <source>Gateway endpoint</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageDevMenu.qml" line="101"/>
<source>Dev gateway environment</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>PageHome</name> <name>PageHome</name>
@ -477,10 +486,63 @@ Already installed containers were found on the server. All installed containers
<translation>ि </translation> <translation>ि </translation>
</message> </message>
</context> </context>
<context>
<name>PageProtocolAwgClientSettings</name>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="96"/>
<source>AmneziaWG settings</source>
<translation type="unfinished">Amneziaडब्ल्यूज ि</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="104"/>
<source>MTU</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="184"/>
<source>Server settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="194"/>
<source>Port</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="284"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="290"/>
<source>Save settings?</source>
<translation type="unfinished">ि ?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="291"/>
<source>Only the settings for this device will be changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="292"/>
<source>Continue</source>
<translation type="unfinished"> </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="293"/>
<source>Cancel</source>
<translation type="unfinished"> </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="297"/>
<source>Unable change settings while there is an active connection</source>
<translation type="unfinished">ि ि </translation>
</message>
</context>
<context> <context>
<name>PageProtocolAwgSettings</name> <name>PageProtocolAwgSettings</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="96"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="94"/>
<source>AmneziaWG settings</source> <source>AmneziaWG settings</source>
<translation>Amneziaडब्ल्यूज ि</translation> <translation>Amneziaडब्ल्यूज ि</translation>
</message> </message>
@ -490,92 +552,91 @@ Already installed containers were found on the server. All installed containers
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="126"/>
<source>MTU</source> <source>MTU</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="147"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="126"/>
<source>Jc - Junk packet count</source> <source>Jc - Junk packet count</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="172"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="151"/>
<source>Jmin - Junk packet minimum size</source> <source>Jmin - Junk packet minimum size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="193"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="172"/>
<source>Jmax - Junk packet maximum size</source> <source>Jmax - Junk packet maximum size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="214"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="193"/>
<source>S1 - Init packet junk size</source> <source>S1 - Init packet junk size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="235"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="214"/>
<source>S2 - Response packet junk size</source> <source>S2 - Response packet junk size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="256"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="235"/>
<source>H1 - Init packet magic header</source> <source>H1 - Init packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="277"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="256"/>
<source>H2 - Response packet magic header</source> <source>H2 - Response packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="298"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="277"/>
<source>H4 - Transport packet magic header</source> <source>H4 - Transport packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="320"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="299"/>
<source>H3 - Underload packet magic header</source> <source>H3 - Underload packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="354"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="333"/>
<source>Save</source> <source>Save</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="363"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="345"/>
<source>The values of the H1-H4 fields must be unique</source> <source>The values of the H1-H4 fields must be unique</source>
<translation>H1-H4 ि ि</translation> <translation>H1-H4 ि ि</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="369"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="351"/>
<source>The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)</source> <source>The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)</source>
<translation> S1 + (148) S2 + िि (92) ि</translation> <translation> S1 + (148) S2 + िि (92) ि</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="373"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="356"/>
<source>Save settings?</source> <source>Save settings?</source>
<translation>ि ?</translation> <translation>ि ?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="374"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="357"/>
<source>All users with whom you shared a connection with will no longer be able to connect to it.</source> <source>All users with whom you shared a connection with will no longer be able to connect to it.</source>
<translation> ि ि , .</translation> <translation> ि ि , .</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="382"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="363"/>
<source>Unable change settings while there is an active connection</source> <source>Unable change settings while there is an active connection</source>
<translation>ि ि </translation> <translation>ि ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="375"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="358"/>
<source>Continue</source> <source>Continue</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="376"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="359"/>
<source>Cancel</source> <source>Cancel</source>
<translation> </translation> <translation> </translation>
</message> </message>
@ -862,30 +923,102 @@ Already installed containers were found on the server. All installed containers
<translation>ि ि </translation> <translation>ि ि </translation>
</message> </message>
</context> </context>
<context>
<name>PageProtocolWireGuardClientSettings</name>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="93"/>
<source>WG settings</source>
<translation type="unfinished"> ि</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="101"/>
<source>MTU</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="118"/>
<source>Server settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="128"/>
<source>Port</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="151"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="157"/>
<source>Save settings?</source>
<translation type="unfinished">ि ?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="158"/>
<source>Only the settings for this device will be changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="159"/>
<source>Continue</source>
<translation type="unfinished"> </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="160"/>
<source>Cancel</source>
<translation type="unfinished"> </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="164"/>
<source>Unable change settings while there is an active connection</source>
<translation type="unfinished">ि ि </translation>
</message>
</context>
<context> <context>
<name>PageProtocolWireGuardSettings</name> <name>PageProtocolWireGuardSettings</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="94"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="97"/>
<source>WG settings</source> <source>WG settings</source>
<translation> ि</translation> <translation> ि</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="102"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="107"/>
<source>Port</source> <source>Port</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="123"/>
<source>MTU</source> <source>MTU</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="149"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="131"/>
<source>Save</source> <source>Save</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="157"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="138"/>
<source>Save settings?</source>
<translation type="unfinished">ि ?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="139"/>
<source>All users with whom you shared a connection with will no longer be able to connect to it.</source>
<translation type="unfinished"> ि ि , .</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="140"/>
<source>Continue</source>
<translation type="unfinished"> </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="141"/>
<source>Cancel</source>
<translation type="unfinished"> </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="145"/>
<source>Unable change settings while there is an active connection</source> <source>Unable change settings while there is an active connection</source>
<translation>ि ि </translation> <translation>ि ि </translation>
</message> </message>
@ -1237,9 +1370,13 @@ Already installed containers were found on the server. All installed containers
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="123"/>
<source>Mail</source> <source>Mail</source>
<translation></translation> <translation type="vanished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="123"/>
<source>support@amnezia.org</source>
<translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="124"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="124"/>
@ -1247,32 +1384,37 @@ Already installed containers were found on the server. All installed containers
<translation> ि ि</translation> <translation> ि ि</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="141"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="132"/>
<source>Copied</source>
<translation type="unfinished"> ि </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="143"/>
<source>GitHub</source> <source>GitHub</source>
<translation>GitHub</translation> <translation>GitHub</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="148"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="150"/>
<source>https://github.com/amnezia-vpn/amnezia-client</source> <source>https://github.com/amnezia-vpn/amnezia-client</source>
<translation>https://github.com/amnezia-vpn/amnezia-client</translation> <translation>https://github.com/amnezia-vpn/amnezia-client</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="159"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="161"/>
<source>Website</source> <source>Website</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="179"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="181"/>
<source>Software version: %1</source> <source>Software version: %1</source>
<translation> : %1</translation> <translation> : %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="208"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="210"/>
<source>Check for updates</source> <source>Check for updates</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="231"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="233"/>
<source>Privacy Policy</source> <source>Privacy Policy</source>
<translation> ि</translation> <translation> ि</translation>
</message> </message>
@ -1729,72 +1871,108 @@ Already installed containers were found on the server. All installed containers
<context> <context>
<name>PageSettingsLogging</name> <name>PageSettingsLogging</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="24"/>
<source>Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted.</source> <source>Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted.</source>
<translation>ि . ि 14 ि ि , .</translation> <translation type="vanished">ि . ि 14 ि ि , .</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="69"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="56"/>
<source>Logging</source> <source>Logging</source>
<translation>ि</translation> <translation>ि</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="70"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="57"/>
<source>Enabling this function will save application&apos;s logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction.</source> <source>Enabling this function will save application&apos;s logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction.</source>
<translation> ि ि , ि , ि ि िि ि .</translation> <translation> ि ि , ि , ि ि िि ि .</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="79"/>
<source>Save logs</source> <source>Save logs</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="117"/>
<source>Open folder with logs</source> <source>Open folder with logs</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="143"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="171"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="255"/>
<source>Save</source> <source>Save</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="144"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="172"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="256"/>
<source>Logs files (*.log)</source> <source>Logs files (*.log)</source>
<translation> (*.log)</translation> <translation> (*.log)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="153"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="181"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="265"/>
<source>Logs file saved</source> <source>Logs file saved</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="162"/>
<source>Save logs to file</source> <source>Save logs to file</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="184"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="68"/>
<source>Enable logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="93"/>
<source>Clear logs?</source> <source>Clear logs?</source>
<translation> ?</translation> <translation> ?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="185"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="94"/>
<source>Continue</source> <source>Continue</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="186"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="95"/>
<source>Cancel</source> <source>Cancel</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="192"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="101"/>
<source>Logs have been cleaned up</source> <source>Logs have been cleaned up</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="211"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="122"/>
<source>Client logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="132"/>
<source>AmneziaVPN logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="141"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="220"/>
<source>Open logs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="160"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="244"/>
<source>Export logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="196"/>
<source>Service logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="208"/>
<source>AmneziaVPN-service logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="86"/>
<source>Clear logs</source> <source>Clear logs</source>
<translation> </translation> <translation> </translation>
</message> </message>
@ -1954,12 +2132,11 @@ Already installed containers were found on the server. All installed containers
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="135"/>
<source>Clear %1 profile</source> <source>Clear %1 profile</source>
<translation>%1 </translation> <translation type="vanished">%1 </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="138"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="176"/>
<source>Clear %1 profile?</source> <source>Clear %1 profile?</source>
<translation>%1 ?</translation> <translation>%1 ?</translation>
</message> </message>
@ -1969,39 +2146,64 @@ Already installed containers were found on the server. All installed containers
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="145"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="183"/>
<source>Unable to clear %1 profile while there is an active connection</source> <source>Unable to clear %1 profile while there is an active connection</source>
<translation>ि %1 </translation> <translation>ि %1 </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="186"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="224"/>
<source>Remove </source> <source>Remove </source>
<translation>ि </translation> <translation>ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="191"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="229"/>
<source>All users with whom you shared a connection will no longer be able to connect to it.</source> <source>All users with whom you shared a connection will no longer be able to connect to it.</source>
<translation> ि ि , .</translation> <translation> ि ि , .</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="198"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="236"/>
<source>Cannot remove active container</source> <source>Cannot remove active container</source>
<translation>ि </translation> <translation>ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="190"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="228"/>
<source>Remove %1 from server?</source> <source>Remove %1 from server?</source>
<translation> %1 ?</translation> <translation> %1 ?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="140"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="100"/>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="192"/> <source> connection settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="112"/>
<source>Click the &quot;connect&quot; button to create a connection configuration</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="132"/>
<source> server settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="173"/>
<source>Clear profile</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="177"/>
<source>The connection configuration will be deleted for this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="178"/>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="230"/>
<source>Continue</source> <source>Continue</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="141"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="179"/>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="193"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="231"/>
<source>Cancel</source> <source>Cancel</source>
<translation> </translation> <translation> </translation>
</message> </message>
@ -2185,82 +2387,92 @@ Already installed containers were found on the server. All installed containers
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="67"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="83"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="91"/>
<source>Enable logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="112"/>
<source>Insert the key, add a configuration file or scan the QR-code</source> <source>Insert the key, add a configuration file or scan the QR-code</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="77"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="122"/>
<source>Insert key</source> <source>Insert key</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="78"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="123"/>
<source>Insert</source> <source>Insert</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="98"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="143"/>
<source>Continue</source> <source>Continue</source>
<translation type="unfinished"> </translation> <translation type="unfinished"> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="116"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="161"/>
<source>Other connection options</source> <source>Other connection options</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="129"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="172"/>
<source>VPN by Amnezia</source> <source>VPN by Amnezia</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="130"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="173"/>
<source>Connect to classic paid and free VPN services from Amnezia</source> <source>Connect to classic paid and free VPN services from Amnezia</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="153"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="196"/>
<source>Self-hosted VPN</source> <source>Self-hosted VPN</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="154"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="197"/>
<source>Configure Amnezia VPN on your own server</source> <source>Configure Amnezia VPN on your own server</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="174"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="217"/>
<source>Restore from backup</source> <source>Restore from backup</source>
<translation type="unfinished"> </translation> <translation type="unfinished"> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="180"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="223"/>
<source>Open backup file</source> <source>Open backup file</source>
<translation type="unfinished"> </translation> <translation type="unfinished"> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="181"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="224"/>
<source>Backup files (*.backup)</source> <source>Backup files (*.backup)</source>
<translation type="unfinished"> (*.backup)</translation> <translation type="unfinished"> (*.backup)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="198"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="241"/>
<source>File with connection settings</source> <source>File with connection settings</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="206"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="249"/>
<source>Open config file</source> <source>Open config file</source>
<translation>ि </translation> <translation>ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="225"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="268"/>
<source>QR code</source> <source>QR code</source>
<translation> ि</translation> <translation> ि</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="248"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="291"/>
<source>I have nothing</source> <source>I have nothing</source>
<translation type="unfinished"> </translation> <translation type="unfinished"> </translation>
</message> </message>
@ -2432,7 +2644,7 @@ Already installed containers were found on the server. All installed containers
<translation>ि </translation> <translation>ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardProtocolSettings.qml" line="267"/> <location filename="../ui/qml/Pages2/PageSetupWizardProtocolSettings.qml" line="268"/>
<source>The port must be in the range of 1 to 65535</source> <source>The port must be in the range of 1 to 65535</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -2804,12 +3016,17 @@ Already installed containers were found on the server. All installed containers
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="143"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="147"/>
<source>Access error!</source>
<translation type="unfinished"> ि!</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="153"/>
<source>Connection to </source> <source>Connection to </source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="144"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="154"/>
<source>File with connection settings to </source> <source>File with connection settings to </source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
@ -2826,6 +3043,11 @@ Already installed containers were found on the server. All installed containers
<source>Settings restored from backup file</source> <source>Settings restored from backup file</source>
<translation type="unfinished"> ि ि </translation> <translation type="unfinished"> ि ि </translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageStart.qml" line="208"/>
<source>Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>PopupType</name> <name>PopupType</name>
@ -2864,12 +3086,12 @@ Already installed containers were found on the server. All installed containers
<translation> ि</translation> <translation> ि</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="173"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="175"/>
<source>Could not open keystore</source> <source>Could not open keystore</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="179"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="181"/>
<source>Could not remove private key from keystore</source> <source>Could not remove private key from keystore</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
@ -3045,27 +3267,27 @@ Already installed containers were found on the server. All installed containers
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="124"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="126"/>
<source>Could not create private key generator</source> <source>Could not create private key generator</source>
<translation>ि </translation> <translation>ि </translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="131"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="133"/>
<source>Could not generate new private key</source> <source>Could not generate new private key</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="139"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="141"/>
<source>Could not retrieve private key from keystore</source> <source>Could not retrieve private key from keystore</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="147"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="149"/>
<source>Could not create encryption cipher</source> <source>Could not create encryption cipher</source>
<translation>ि ि </translation> <translation>ि ि </translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="155"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="157"/>
<source>Could not encrypt data</source> <source>Could not encrypt data</source>
<translation> ि ि </translation> <translation> ि ि </translation>
</message> </message>
@ -3763,12 +3985,12 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
<context> <context>
<name>SettingsController</name> <name>SettingsController</name>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="138"/> <location filename="../ui/controllers/settingsController.cpp" line="152"/>
<source>Backup file is corrupted</source> <source>Backup file is corrupted</source>
<translation> ि </translation> <translation> ि </translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="160"/> <location filename="../ui/controllers/settingsController.cpp" line="174"/>
<source>All settings have been reset to default values</source> <source>All settings have been reset to default values</source>
<translation> ि ि ि </translation> <translation> ि ि ि </translation>
</message> </message>
@ -3900,7 +4122,7 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
<context> <context>
<name>VpnConnection</name> <name>VpnConnection</name>
<message> <message>
<location filename="../vpnconnection.cpp" line="375"/> <location filename="../vpnconnection.cpp" line="408"/>
<source>Mbps</source> <source>Mbps</source>
<translation></translation> <translation></translation>
</message> </message>

File diff suppressed because it is too large Load diff

View file

@ -4,47 +4,52 @@
<context> <context>
<name>ApiServicesModel</name> <name>ApiServicesModel</name>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="63"/> <location filename="../ui/models/apiServicesModel.cpp" line="65"/>
<source>Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s</source> <source>Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s</source>
<translation>Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с</translation> <translation>Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="67"/> <location filename="../ui/models/apiServicesModel.cpp" line="69"/>
<source>VPN to access blocked sites in regions with high levels of Internet censorship. </source> <source>VPN to access blocked sites in regions with high levels of Internet censorship. </source>
<translation>VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. </translation> <translation>VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. </translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="72"/> <location filename="../ui/models/apiServicesModel.cpp" line="71"/>
<source>&lt;p&gt;&lt;a style=&quot;color: #EB5757;&quot;&gt;Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/models/apiServicesModel.cpp" line="78"/>
<source>Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship.</source> <source>Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship.</source>
<translation>Amnezia Premium классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры.</translation> <translation>Amnezia Premium классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры.</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="75"/> <location filename="../ui/models/apiServicesModel.cpp" line="81"/>
<source>Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship</source> <source>Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship</source>
<translation>Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры</translation> <translation>Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="80"/> <location filename="../ui/models/apiServicesModel.cpp" line="94"/>
<source>%1 MBit/s</source> <source>%1 MBit/s</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="87"/> <location filename="../ui/models/apiServicesModel.cpp" line="101"/>
<source>%1 days</source> <source>%1 days</source>
<translation>%1 дней</translation> <translation>%1 дней</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="96"/> <location filename="../ui/models/apiServicesModel.cpp" line="110"/>
<source>VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, &lt;a href=&quot;%1/free&quot; style=&quot;color: #FBB26A;&quot;&gt;more details on the website.&lt;/a&gt;</source> <source>VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, &lt;a href=&quot;%1/free&quot; style=&quot;color: #FBB26A;&quot;&gt;more details on the website.&lt;/a&gt;</source>
<translation>Через VPN будут открываться только популярные сайты, заблокированные в вашем регионе, такие как Instagram, Facebook, Twitter и другие. Остальные сайты будут открываться с вашего реального IP-адреса, &lt;a href=&quot;%1/free&quot; style=&quot;color: #FBB26A;&quot;&gt;подробности на сайте.&lt;/a&gt;</translation> <translation>Через VPN будут открываться только популярные сайты, заблокированные в вашем регионе, такие как Instagram, Facebook, Twitter и другие. Остальные сайты будут открываться с вашего реального IP-адреса, &lt;a href=&quot;%1/free&quot; style=&quot;color: #FBB26A;&quot;&gt;подробности на сайте.&lt;/a&gt;</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="104"/> <location filename="../ui/models/apiServicesModel.cpp" line="118"/>
<source>Free</source> <source>Free</source>
<translation>Бесплатно</translation> <translation>Бесплатно</translation>
</message> </message>
<message> <message>
<location filename="../ui/models/apiServicesModel.cpp" line="106"/> <location filename="../ui/models/apiServicesModel.cpp" line="120"/>
<source>%1 $/month</source> <source>%1 $/month</source>
<translation>%1 $/месяц</translation> <translation>%1 $/месяц</translation>
</message> </message>
@ -75,7 +80,7 @@
<context> <context>
<name>ConnectButton</name> <name>ConnectButton</name>
<message> <message>
<location filename="../ui/qml/Components/ConnectButton.qml" line="27"/> <location filename="../ui/qml/Components/ConnectButton.qml" line="28"/>
<source>Unable to disconnect during configuration preparation</source> <source>Unable to disconnect during configuration preparation</source>
<translation>Невозможно отключиться во время подготовки конфигурации</translation> <translation>Невозможно отключиться во время подготовки конфигурации</translation>
</message> </message>
@ -187,9 +192,8 @@
<context> <context>
<name>ExportController</name> <name>ExportController</name>
<message> <message>
<location filename="../ui/controllers/exportController.cpp" line="30"/>
<source>Access error!</source> <source>Access error!</source>
<translation>Ошибка доступа!</translation> <translation type="vanished">Ошибка доступа!</translation>
</message> </message>
</context> </context>
<context> <context>
@ -259,18 +263,18 @@ Can&apos;t be disabled for current server</source>
<translation>Невозможно открыть файл</translation> <translation>Невозможно открыть файл</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/importController.cpp" line="186"/> <location filename="../ui/controllers/importController.cpp" line="187"/>
<location filename="../ui/controllers/importController.cpp" line="191"/> <location filename="../ui/controllers/importController.cpp" line="192"/>
<source>Invalid configuration file</source> <source>Invalid configuration file</source>
<translation>Неверный файл конфигурации</translation> <translation>Неверный файл конфигурации</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/importController.cpp" line="606"/> <location filename="../ui/controllers/importController.cpp" line="617"/>
<source>Scanned %1 of %2.</source> <source>Scanned %1 of %2.</source>
<translation>Отсканировано %1 из %2.</translation> <translation>Отсканировано %1 из %2.</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/importController.cpp" line="641"/> <location filename="../ui/controllers/importController.cpp" line="652"/>
<source>In the imported configuration, potentially dangerous lines were found:</source> <source>In the imported configuration, potentially dangerous lines were found:</source>
<translation>В импортированной конфигурации были обнаружены потенциально опасные строки:</translation> <translation>В импортированной конфигурации были обнаружены потенциально опасные строки:</translation>
</message> </message>
@ -447,6 +451,11 @@ Already installed containers were found on the server. All installed containers
<source>Gateway endpoint</source> <source>Gateway endpoint</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageDevMenu.qml" line="101"/>
<source>Dev gateway environment</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>PageHome</name> <name>PageHome</name>
@ -481,10 +490,63 @@ Already installed containers were found on the server. All installed containers
<translation>Невозможно изменить сервер во время активного соединения</translation> <translation>Невозможно изменить сервер во время активного соединения</translation>
</message> </message>
</context> </context>
<context>
<name>PageProtocolAwgClientSettings</name>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="96"/>
<source>AmneziaWG settings</source>
<translation type="unfinished">Настройки AmneziaWG</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="104"/>
<source>MTU</source>
<translation type="unfinished">MTU</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="184"/>
<source>Server settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="194"/>
<source>Port</source>
<translation type="unfinished">Порт</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="284"/>
<source>Save</source>
<translation type="unfinished">Сохранить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="290"/>
<source>Save settings?</source>
<translation type="unfinished">Сохранить настройки?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="291"/>
<source>Only the settings for this device will be changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="292"/>
<source>Continue</source>
<translation type="unfinished">Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="293"/>
<source>Cancel</source>
<translation type="unfinished">Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="297"/>
<source>Unable change settings while there is an active connection</source>
<translation type="unfinished">Невозможно изменить настройки во время активного соединения</translation>
</message>
</context>
<context> <context>
<name>PageProtocolAwgSettings</name> <name>PageProtocolAwgSettings</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="96"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="94"/>
<source>AmneziaWG settings</source> <source>AmneziaWG settings</source>
<translation>Настройки AmneziaWG</translation> <translation>Настройки AmneziaWG</translation>
</message> </message>
@ -494,9 +556,8 @@ Already installed containers were found on the server. All installed containers
<translation>Порт</translation> <translation>Порт</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="126"/>
<source>MTU</source> <source>MTU</source>
<translation>MTU</translation> <translation type="vanished">MTU</translation>
</message> </message>
<message> <message>
<source>Remove AmneziaWG</source> <source>Remove AmneziaWG</source>
@ -507,87 +568,87 @@ Already installed containers were found on the server. All installed containers
<translation type="vanished">Удалить AmneziaWG с сервера?</translation> <translation type="vanished">Удалить AmneziaWG с сервера?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="374"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="357"/>
<source>All users with whom you shared a connection with will no longer be able to connect to it.</source> <source>All users with whom you shared a connection with will no longer be able to connect to it.</source>
<translation>Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation> <translation>Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="354"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="333"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="147"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="126"/>
<source>Jc - Junk packet count</source> <source>Jc - Junk packet count</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="172"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="151"/>
<source>Jmin - Junk packet minimum size</source> <source>Jmin - Junk packet minimum size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="193"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="172"/>
<source>Jmax - Junk packet maximum size</source> <source>Jmax - Junk packet maximum size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="214"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="193"/>
<source>S1 - Init packet junk size</source> <source>S1 - Init packet junk size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="235"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="214"/>
<source>S2 - Response packet junk size</source> <source>S2 - Response packet junk size</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="256"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="235"/>
<source>H1 - Init packet magic header</source> <source>H1 - Init packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="277"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="256"/>
<source>H2 - Response packet magic header</source> <source>H2 - Response packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="298"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="277"/>
<source>H4 - Transport packet magic header</source> <source>H4 - Transport packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="320"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="299"/>
<source>H3 - Underload packet magic header</source> <source>H3 - Underload packet magic header</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="363"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="345"/>
<source>The values of the H1-H4 fields must be unique</source> <source>The values of the H1-H4 fields must be unique</source>
<translation>Значения в полях H1-H4 должны быть уникальными</translation> <translation>Значения в полях H1-H4 должны быть уникальными</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="369"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="351"/>
<source>The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)</source> <source>The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)</source>
<translation>Значение в поле S1 + размер инициации сообщения (148) не должно равняться значению в поле S2 + размер ответа на сообщение (92)</translation> <translation>Значение в поле S1 + размер инициации сообщения (148) не должно равняться значению в поле S2 + размер ответа на сообщение (92)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="373"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="356"/>
<source>Save settings?</source> <source>Save settings?</source>
<translation>Сохранить настройки?</translation> <translation>Сохранить настройки?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="375"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="358"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="376"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="359"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="382"/> <location filename="../ui/qml/Pages2/PageProtocolAwgSettings.qml" line="363"/>
<source>Unable change settings while there is an active connection</source> <source>Unable change settings while there is an active connection</source>
<translation>Невозможно изменить настройки во время активного соединения</translation> <translation>Невозможно изменить настройки во время активного соединения</translation>
</message> </message>
@ -898,25 +959,87 @@ Already installed containers were found on the server. All installed containers
<translation>Невозможно изменить настройки во время активного соединения</translation> <translation>Невозможно изменить настройки во время активного соединения</translation>
</message> </message>
</context> </context>
<context>
<name>PageProtocolWireGuardClientSettings</name>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="93"/>
<source>WG settings</source>
<translation type="unfinished">Настройки WG</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="101"/>
<source>MTU</source>
<translation type="unfinished">MTU</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="118"/>
<source>Server settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="128"/>
<source>Port</source>
<translation type="unfinished">Порт</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="151"/>
<source>Save</source>
<translation type="unfinished">Сохранить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="157"/>
<source>Save settings?</source>
<translation type="unfinished">Сохранить настройки?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="158"/>
<source>Only the settings for this device will be changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="159"/>
<source>Continue</source>
<translation type="unfinished">Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="160"/>
<source>Cancel</source>
<translation type="unfinished">Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml" line="164"/>
<source>Unable change settings while there is an active connection</source>
<translation type="unfinished">Невозможно изменить настройки во время активного соединения</translation>
</message>
</context>
<context> <context>
<name>PageProtocolWireGuardSettings</name> <name>PageProtocolWireGuardSettings</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="94"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="97"/>
<source>WG settings</source> <source>WG settings</source>
<translation>Настройки WG</translation> <translation>Настройки WG</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="102"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="107"/>
<source>Port</source> <source>Port</source>
<translation>Порт</translation> <translation>Порт</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="123"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="138"/>
<source>MTU</source> <source>Save settings?</source>
<translation>MTU</translation> <translation type="unfinished">Сохранить настройки?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="157"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="139"/>
<source>All users with whom you shared a connection with will no longer be able to connect to it.</source>
<translation type="unfinished">Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation>
</message>
<message>
<source>MTU</source>
<translation type="vanished">MTU</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="145"/>
<source>Unable change settings while there is an active connection</source> <source>Unable change settings while there is an active connection</source>
<translation>Невозможно изменить настройки во время активного соединения</translation> <translation>Невозможно изменить настройки во время активного соединения</translation>
</message> </message>
@ -933,15 +1056,17 @@ Already installed containers were found on the server. All installed containers
<translation type="vanished">Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation> <translation type="vanished">Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="140"/>
<source>Continue</source> <source>Continue</source>
<translation type="vanished">Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="141"/>
<source>Cancel</source> <source>Cancel</source>
<translation type="vanished">Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="149"/> <location filename="../ui/qml/Pages2/PageProtocolWireGuardSettings.qml" line="131"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
@ -1305,8 +1430,12 @@ Already installed containers were found on the server. All installed containers
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="123"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="123"/>
<source>support@amnezia.org</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Mail</source> <source>Mail</source>
<translation>Почта</translation> <translation type="vanished">Почта</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="124"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="124"/>
@ -1314,17 +1443,22 @@ Already installed containers were found on the server. All installed containers
<translation>Для отзывов и сообщений об ошибках</translation> <translation>Для отзывов и сообщений об ошибках</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="141"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="132"/>
<source>Copied</source>
<translation type="unfinished">Скопировано</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="143"/>
<source>GitHub</source> <source>GitHub</source>
<translation>GitHub</translation> <translation>GitHub</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="148"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="150"/>
<source>https://github.com/amnezia-vpn/amnezia-client</source> <source>https://github.com/amnezia-vpn/amnezia-client</source>
<translation>https://github.com/amnezia-vpn/amnezia-client</translation> <translation>https://github.com/amnezia-vpn/amnezia-client</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="159"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="161"/>
<source>Website</source> <source>Website</source>
<translation>Веб-сайт</translation> <translation>Веб-сайт</translation>
</message> </message>
@ -1333,17 +1467,17 @@ Already installed containers were found on the server. All installed containers
<translation type="vanished">https://amnezia.org</translation> <translation type="vanished">https://amnezia.org</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="179"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="181"/>
<source>Software version: %1</source> <source>Software version: %1</source>
<translation>Версия ПО: %1</translation> <translation>Версия ПО: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="208"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="210"/>
<source>Check for updates</source> <source>Check for updates</source>
<translation>Проверить обновления</translation> <translation>Проверить обновления</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="231"/> <location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="233"/>
<source>Privacy Policy</source> <source>Privacy Policy</source>
<translation>Политика конфиденциальности</translation> <translation>Политика конфиденциальности</translation>
</message> </message>
@ -1816,72 +1950,108 @@ Already installed containers were found on the server. All installed containers
<context> <context>
<name>PageSettingsLogging</name> <name>PageSettingsLogging</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="24"/>
<source>Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted.</source> <source>Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted.</source>
<translation>Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены.</translation> <translation type="vanished">Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="69"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="56"/>
<source>Logging</source> <source>Logging</source>
<translation>Логирование</translation> <translation>Логирование</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="70"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="57"/>
<source>Enabling this function will save application&apos;s logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction.</source> <source>Enabling this function will save application&apos;s logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction.</source>
<translation>Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения.</translation> <translation>Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="79"/>
<source>Save logs</source> <source>Save logs</source>
<translation>Сохранять логи</translation> <translation type="vanished">Сохранять логи</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="117"/>
<source>Open folder with logs</source> <source>Open folder with logs</source>
<translation>Открыть папку с логами</translation> <translation type="vanished">Открыть папку с логами</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="143"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="171"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="255"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="144"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="172"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="256"/>
<source>Logs files (*.log)</source> <source>Logs files (*.log)</source>
<translation>Файлы логов (*.log)</translation> <translation>Файлы логов (*.log)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="153"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="181"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="265"/>
<source>Logs file saved</source> <source>Logs file saved</source>
<translation>Файл с логами сохранен</translation> <translation>Файл с логами сохранен</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="162"/>
<source>Save logs to file</source> <source>Save logs to file</source>
<translation>Сохранить логи в файл</translation> <translation type="vanished">Сохранить логи в файл</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="184"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="68"/>
<source>Enable logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="93"/>
<source>Clear logs?</source> <source>Clear logs?</source>
<translation>Очистить логи?</translation> <translation>Очистить логи?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="185"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="94"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="186"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="95"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="192"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="101"/>
<source>Logs have been cleaned up</source> <source>Logs have been cleaned up</source>
<translation>Логи очищены</translation> <translation>Логи очищены</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="211"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="122"/>
<source>Client logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="132"/>
<source>AmneziaVPN logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="141"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="220"/>
<source>Open logs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="160"/>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="244"/>
<source>Export logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="196"/>
<source>Service logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="208"/>
<source>AmneziaVPN-service logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="86"/>
<source>Clear logs</source> <source>Clear logs</source>
<translation>Очистить логи</translation> <translation>Очистить логи</translation>
</message> </message>
@ -2069,12 +2239,11 @@ Already installed containers were found on the server. All installed containers
<translation> настройки</translation> <translation> настройки</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="135"/>
<source>Clear %1 profile</source> <source>Clear %1 profile</source>
<translation>Очистить профиль %1</translation> <translation type="vanished">Очистить профиль %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="138"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="176"/>
<source>Clear %1 profile?</source> <source>Clear %1 profile?</source>
<translation>Очистить профиль %1?</translation> <translation>Очистить профиль %1?</translation>
</message> </message>
@ -2084,27 +2253,52 @@ Already installed containers were found on the server. All installed containers
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="145"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="100"/>
<source> connection settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="112"/>
<source>Click the &quot;connect&quot; button to create a connection configuration</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="132"/>
<source> server settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="173"/>
<source>Clear profile</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="177"/>
<source>The connection configuration will be deleted for this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="183"/>
<source>Unable to clear %1 profile while there is an active connection</source> <source>Unable to clear %1 profile while there is an active connection</source>
<translation>Невозможно очистить профиль %1 во время активного соединения</translation> <translation>Невозможно очистить профиль %1 во время активного соединения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="186"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="224"/>
<source>Remove </source> <source>Remove </source>
<translation>Удалить </translation> <translation>Удалить </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="190"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="228"/>
<source>Remove %1 from server?</source> <source>Remove %1 from server?</source>
<translation>Удалить %1 с сервера?</translation> <translation>Удалить %1 с сервера?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="191"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="229"/>
<source>All users with whom you shared a connection will no longer be able to connect to it.</source> <source>All users with whom you shared a connection will no longer be able to connect to it.</source>
<translation>Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation> <translation>Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="198"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="236"/>
<source>Cannot remove active container</source> <source>Cannot remove active container</source>
<translation>Невозможно удалить активный контейнер</translation> <translation>Невозможно удалить активный контейнер</translation>
</message> </message>
@ -2113,14 +2307,14 @@ Already installed containers were found on the server. All installed containers
<translation type="vanished">Все пользователи, с которыми вы поделились VPN, больше не смогут к нему подключаться.</translation> <translation type="vanished">Все пользователи, с которыми вы поделились VPN, больше не смогут к нему подключаться.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="140"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="178"/>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="192"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="230"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="141"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="179"/>
<location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="193"/> <location filename="../ui/qml/Pages2/PageSettingsServerProtocol.qml" line="231"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
@ -2311,7 +2505,7 @@ It&apos;s okay as long as it&apos;s from someone you trust.</source>
<translation type="vanished">Что у вас есть?</translation> <translation type="vanished">Что у вас есть?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="198"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="241"/>
<source>File with connection settings</source> <source>File with connection settings</source>
<translation>Файл с настройками подключения</translation> <translation>Файл с настройками подключения</translation>
</message> </message>
@ -2325,77 +2519,87 @@ It&apos;s okay as long as it&apos;s from someone you trust.</source>
<translation>Соединение</translation> <translation>Соединение</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="67"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="83"/>
<source>Settings</source>
<translation type="unfinished">Настройки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="91"/>
<source>Enable logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="112"/>
<source>Insert the key, add a configuration file or scan the QR-code</source> <source>Insert the key, add a configuration file or scan the QR-code</source>
<translation>Вставьте ключ, добавьте файл конфигурации или отсканируйте QR-код</translation> <translation>Вставьте ключ, добавьте файл конфигурации или отсканируйте QR-код</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="77"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="122"/>
<source>Insert key</source> <source>Insert key</source>
<translation>Вставьте ключ</translation> <translation>Вставьте ключ</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="78"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="123"/>
<source>Insert</source> <source>Insert</source>
<translation>Вставить</translation> <translation>Вставить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="98"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="143"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="116"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="161"/>
<source>Other connection options</source> <source>Other connection options</source>
<translation>Другие варианты подключения</translation> <translation>Другие варианты подключения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="129"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="172"/>
<source>VPN by Amnezia</source> <source>VPN by Amnezia</source>
<translation>VPN от Amnezia</translation> <translation>VPN от Amnezia</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="130"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="173"/>
<source>Connect to classic paid and free VPN services from Amnezia</source> <source>Connect to classic paid and free VPN services from Amnezia</source>
<translation>Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia</translation> <translation>Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="153"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="196"/>
<source>Self-hosted VPN</source> <source>Self-hosted VPN</source>
<translation>Self-hosted VPN</translation> <translation>Self-hosted VPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="154"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="197"/>
<source>Configure Amnezia VPN on your own server</source> <source>Configure Amnezia VPN on your own server</source>
<translation>Настроить VPN на собственном сервере</translation> <translation>Настроить VPN на собственном сервере</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="174"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="217"/>
<source>Restore from backup</source> <source>Restore from backup</source>
<translation>Восстановить из резервной копии</translation> <translation>Восстановить из резервной копии</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="180"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="223"/>
<source>Open backup file</source> <source>Open backup file</source>
<translation>Открыть резервную копию</translation> <translation>Открыть резервную копию</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="181"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="224"/>
<source>Backup files (*.backup)</source> <source>Backup files (*.backup)</source>
<translation>Файлы резервных копий (*.backup)</translation> <translation>Файлы резервных копий (*.backup)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="206"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="249"/>
<source>Open config file</source> <source>Open config file</source>
<translation>Открыть файл с конфигурацией</translation> <translation>Открыть файл с конфигурацией</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="225"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="268"/>
<source>QR code</source> <source>QR code</source>
<translation>QR-код</translation> <translation>QR-код</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="248"/> <location filename="../ui/qml/Pages2/PageSetupWizardConfigSource.qml" line="291"/>
<source>I have nothing</source> <source>I have nothing</source>
<translation>У меня ничего нет</translation> <translation>У меня ничего нет</translation>
</message> </message>
@ -2600,7 +2804,7 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation>Установить</translation> <translation>Установить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardProtocolSettings.qml" line="267"/> <location filename="../ui/qml/Pages2/PageSetupWizardProtocolSettings.qml" line="268"/>
<source>The port must be in the range of 1 to 65535</source> <source>The port must be in the range of 1 to 65535</source>
<translation>Порт должен быть в диапазоне от 1 до 65535</translation> <translation>Порт должен быть в диапазоне от 1 до 65535</translation>
</message> </message>
@ -2996,12 +3200,17 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation>Поделиться</translation> <translation>Поделиться</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="143"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="147"/>
<source>Access error!</source>
<translation type="unfinished">Ошибка доступа!</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="153"/>
<source>Connection to </source> <source>Connection to </source>
<translation>Подключение к </translation> <translation>Подключение к </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="144"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="154"/>
<source>File with connection settings to </source> <source>File with connection settings to </source>
<translation>Файл с настройками подключения к </translation> <translation>Файл с настройками подключения к </translation>
</message> </message>
@ -3018,6 +3227,11 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<source>Settings restored from backup file</source> <source>Settings restored from backup file</source>
<translation>Настройки восстановлены из бэкап файла</translation> <translation>Настройки восстановлены из бэкап файла</translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageStart.qml" line="208"/>
<source>Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>PopupType</name> <name>PopupType</name>
@ -3056,12 +3270,12 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation>Пароль не найден</translation> <translation>Пароль не найден</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="173"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="175"/>
<source>Could not open keystore</source> <source>Could not open keystore</source>
<translation>Не удалось открыть хранилище ключей</translation> <translation>Не удалось открыть хранилище ключей</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="179"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="181"/>
<source>Could not remove private key from keystore</source> <source>Could not remove private key from keystore</source>
<translation>Не удалось удалить закрытый ключ из хранилища ключей</translation> <translation>Не удалось удалить закрытый ключ из хранилища ключей</translation>
</message> </message>
@ -3237,27 +3451,27 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
<translation>Не удалось открыть хранилище ключей</translation> <translation>Не удалось открыть хранилище ключей</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="124"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="126"/>
<source>Could not create private key generator</source> <source>Could not create private key generator</source>
<translation>Не удалось создать генератор закрытых ключей</translation> <translation>Не удалось создать генератор закрытых ключей</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="131"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="133"/>
<source>Could not generate new private key</source> <source>Could not generate new private key</source>
<translation>Не удалось сгенерировать новый закрытый ключ</translation> <translation>Не удалось сгенерировать новый закрытый ключ</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="139"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="141"/>
<source>Could not retrieve private key from keystore</source> <source>Could not retrieve private key from keystore</source>
<translation>Не удалось получить закрытый ключ из хранилища ключей</translation> <translation>Не удалось получить закрытый ключ из хранилища ключей</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="147"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="149"/>
<source>Could not create encryption cipher</source> <source>Could not create encryption cipher</source>
<translation>Не удалось создать шифр шифрования</translation> <translation>Не удалось создать шифр шифрования</translation>
</message> </message>
<message> <message>
<location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="155"/> <location filename="../3rd/qtkeychain/qtkeychain/keychain_android.cpp" line="157"/>
<source>Could not encrypt data</source> <source>Could not encrypt data</source>
<translation>Не удалось зашифровать данные</translation> <translation>Не удалось зашифровать данные</translation>
</message> </message>
@ -4094,7 +4308,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin
<context> <context>
<name>SettingsController</name> <name>SettingsController</name>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="160"/> <location filename="../ui/controllers/settingsController.cpp" line="174"/>
<source>All settings have been reset to default values</source> <source>All settings have been reset to default values</source>
<translation>Все настройки сброшены до значений по умолчанию</translation> <translation>Все настройки сброшены до значений по умолчанию</translation>
</message> </message>
@ -4103,7 +4317,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin
<translation type="vanished">Закэшированные профили очищены</translation> <translation type="vanished">Закэшированные профили очищены</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="138"/> <location filename="../ui/controllers/settingsController.cpp" line="152"/>
<source>Backup file is corrupted</source> <source>Backup file is corrupted</source>
<translation>Файл резервной копии поврежден</translation> <translation>Файл резервной копии поврежден</translation>
</message> </message>
@ -4235,7 +4449,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin
<context> <context>
<name>VpnConnection</name> <name>VpnConnection</name>
<message> <message>
<location filename="../vpnconnection.cpp" line="375"/> <location filename="../vpnconnection.cpp" line="408"/>
<source>Mbps</source> <source>Mbps</source>
<translation>Мбит/с</translation> <translation>Мбит/с</translation>
</message> </message>

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