Compare commits
20 Commits
feature/er
...
sudo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c6daf1e83 | ||
|
|
f4c06f952e | ||
|
|
ab13e98ad2 | ||
|
|
4e29ce7139 | ||
|
|
60d58915c0 | ||
|
|
c0302b7a01 | ||
|
|
fecdd60787 | ||
|
|
75987b8b45 | ||
|
|
4109e9db58 | ||
|
|
8e10144717 | ||
|
|
2410bf6838 | ||
|
|
bd595f2c94 | ||
|
|
7d7a4c154d | ||
|
|
d66304f814 | ||
|
|
e7a1a93d84 | ||
|
|
9f29bc9afe | ||
|
|
a6ffd2a675 | ||
|
|
795737bbc3 | ||
|
|
61f55af76b | ||
|
|
6c5a12e626 |
1
.gitattributes
vendored
@@ -3,4 +3,3 @@ deploy/data/windows/x64/tap/windows_10/OemVista.inf eol=crlf
|
||||
deploy/data/windows/x32/tap/windows_7/OemVista.inf eol=crlf
|
||||
deploy/data/windows/x32/tap/windows_10/OemVista.inf eol=crlf
|
||||
client/3rd/* linguist-vendored
|
||||
client/android/gradlew.bat eol=crlf
|
||||
|
||||
143
.github/workflows/deploy.yml
vendored
@@ -48,18 +48,14 @@ jobs:
|
||||
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
|
||||
bash deploy/build_linux.sh
|
||||
|
||||
- name: 'Pack installer'
|
||||
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN_Linux_installer.tar
|
||||
path: deploy/AmneziaVPN_Linux_Installer.tar
|
||||
name: AmneziaVPN_Linux_installer
|
||||
path: deploy/AmneziaVPN_Linux_Installer
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN_Linux_unpacked
|
||||
path: deploy/AppDir
|
||||
@@ -114,14 +110,13 @@ jobs:
|
||||
call deploy\\build_windows.bat
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN_Windows_installer
|
||||
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.exe
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN_Windows_unpacked
|
||||
path: deploy\\build_${{ env.BUILD_ARCH }}\\client\\Release
|
||||
@@ -205,7 +200,7 @@ jobs:
|
||||
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
|
||||
|
||||
# - name: 'Upload appstore .ipa and dSYMs to artifacts'
|
||||
# uses: actions/upload-artifact@v4
|
||||
# uses: actions/upload-artifact@v3
|
||||
# with:
|
||||
# name: app-store ipa & dsyms
|
||||
# path: |
|
||||
@@ -260,14 +255,13 @@ jobs:
|
||||
bash deploy/build_macos.sh
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN_MacOS_installer
|
||||
path: AmneziaVPN.dmg
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN_MacOS_unpacked
|
||||
path: deploy/build/client/AmneziaVPN.app
|
||||
@@ -279,10 +273,21 @@ jobs:
|
||||
name: 'Build-Android'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- abi: 'x86_64'
|
||||
qt_arch: 'android_x86_64'
|
||||
- abi: 'x86'
|
||||
qt_arch: 'android_x86'
|
||||
- abi: 'armeabi-v7a'
|
||||
qt_arch: 'android_armv7'
|
||||
- abi: 'arm64-v8a'
|
||||
qt_arch: 'android_arm64_v8a'
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-34
|
||||
QT_VERSION: 6.6.1
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
QT_VERSION: 6.5.2
|
||||
ANDROID_BUILD_PLATFORM: android-33
|
||||
|
||||
steps:
|
||||
- name: 'Install desktop Qt'
|
||||
@@ -292,58 +297,29 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'gcc_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86_64 Qt'
|
||||
- name: 'Install android Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android_x86_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86 Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android_x86'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_armv7 Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android_armv7'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_arm64_v8a Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android_arm64_v8a'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
arch: ${{ matrix.qt_arch }}
|
||||
modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Grant execute permission for qt-cmake'
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_x86_64/bin/qt-cmake
|
||||
chmod +x ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/${{ matrix.qt_arch }}/bin/qt-cmake
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v3
|
||||
@@ -357,14 +333,15 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '11'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: 'Setup Android NDK'
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: 'r26b'
|
||||
ndk-version: 'r25c'
|
||||
local-cache: 'true'
|
||||
|
||||
- name: 'Decode keystore secret to file'
|
||||
env:
|
||||
@@ -377,48 +354,16 @@ jobs:
|
||||
env:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
QT_HOST_PATH: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
|
||||
ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
||||
run: ./deploy/build_android.sh --apk ${{ matrix.abi }} --platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
||||
|
||||
- name: 'Upload x86_64 apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: 'Upload apk'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AmneziaVPN-android-x86_64
|
||||
path: deploy/build/AmneziaVPN-x86_64-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload x86 apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-x86
|
||||
path: deploy/build/AmneziaVPN-x86-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload arm64-v8a apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-arm64-v8a
|
||||
path: deploy/build/AmneziaVPN-arm64-v8a-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload armeabi-v7a apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-armeabi-v7a
|
||||
path: deploy/build/AmneziaVPN-armeabi-v7a-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload aab'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android
|
||||
path: deploy/build/AmneziaVPN-release.aab
|
||||
compression-level: 0
|
||||
name: AmneziaVPN-android-${{ matrix.abi }}
|
||||
path: deploy/build/AmneziaVPN-${{ matrix.abi }}-release-signed.apk
|
||||
retention-days: 7
|
||||
|
||||
6
.gitmodules
vendored
@@ -22,6 +22,6 @@
|
||||
[submodule "client/3rd-prebuilt"]
|
||||
path = client/3rd-prebuilt
|
||||
url = https://github.com/amnezia-vpn/3rd-prebuilt
|
||||
[submodule "client/3rd/amneziawg-apple"]
|
||||
path = client/3rd/amneziawg-apple
|
||||
url = https://github.com/amnezia-vpn/amneziawg-apple
|
||||
[submodule "client/3rd/awg-apple"]
|
||||
path = client/3rd/awg-apple
|
||||
url = https://github.com/amnezia-vpn/awg-apple
|
||||
|
||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
|
||||
project(${PROJECT} VERSION 4.2.1.1
|
||||
project(${PROJECT} VERSION 4.1.0.1
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
@@ -11,7 +11,6 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 43)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
16
README.md
@@ -18,6 +18,7 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
|
||||
[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](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
[https://signal.group/...](https://signal.group/#CjQKIB2gUf8QH_IXnOJMGQWMDjYz9cNfmRQipGWLFiIgc4MwEhAKBONrSiWHvoUFbbD0xwdh) - Signal channel
|
||||
|
||||
## Tech
|
||||
|
||||
@@ -35,7 +36,7 @@ AmneziaVPN uses a number of open source projects to work:
|
||||
Make sure to pull all submodules after checking out the repo.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
## Development
|
||||
@@ -49,15 +50,7 @@ Look deploy folder for build scripts.
|
||||
|
||||
1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher.
|
||||
|
||||
2. We use QT to generate the XCode project. we need QT version 6.6.1. Install QT for macos in [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules:
|
||||
- macOS
|
||||
- iOS
|
||||
- Qt 5 Compatibility Module
|
||||
- Qt Shader Tools
|
||||
- Additional Libraries:
|
||||
- Qt Image Formats
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
2. We use QT to generate the XCode project. we need QT version 6.4. Install QT for macos in [here](https://doc.qt.io/qt-6/macos.html)
|
||||
|
||||
3. Install cmake is require. We recommend cmake version 3.25. You can install cmake in [here](https://cmake.org/download/)
|
||||
|
||||
@@ -73,11 +66,10 @@ gomobile init
|
||||
5. Build project
|
||||
```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
|
||||
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_BIN_DIR
|
||||
```
|
||||
Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment
|
||||
|
||||
|
||||
1
client/3rd/amneziawg-apple
vendored
1
client/3rd/awg-apple
vendored
Submodule
2
client/3rd/qtkeychain
vendored
@@ -56,7 +56,6 @@ set(CMAKE_AUTORCC ON)
|
||||
set(AMNEZIAVPN_TS_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
||||
@@ -108,7 +107,7 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/server_defs.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/servercontroller.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h
|
||||
@@ -147,7 +146,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/server_defs.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/servercontroller.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp
|
||||
|
||||
@@ -1,389 +1,366 @@
|
||||
#include "amnezia_application.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFontDatabase>
|
||||
#include <QMimeData>
|
||||
#include <QQuickStyle>
|
||||
#include <QResource>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "logger.h"
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#include "protocols/qml_register_protocols.h"
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv)
|
||||
#else
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options,
|
||||
int timeout, const QString &userData)
|
||||
: SingleApplication(argc, argv, allowSecondary, options, timeout, userData)
|
||||
#endif
|
||||
{
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
// Fix config file permissions
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
{
|
||||
QSettings s(ORGANIZATION_NAME, APPLICATION_NAME);
|
||||
s.setValue("permFixed", true);
|
||||
}
|
||||
|
||||
QString configLoc1 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/"
|
||||
+ ORGANIZATION_NAME + "/" + APPLICATION_NAME + ".conf";
|
||||
QFile::setPermissions(configLoc1, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
|
||||
QString configLoc2 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/"
|
||||
+ ORGANIZATION_NAME + "/" + APPLICATION_NAME + "/" + APPLICATION_NAME + ".conf";
|
||||
QFile::setPermissions(configLoc2, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
#endif
|
||||
|
||||
m_settings = std::shared_ptr<Settings>(new Settings);
|
||||
}
|
||||
|
||||
AmneziaApplication::~AmneziaApplication()
|
||||
{
|
||||
m_vpnConnectionThread.quit();
|
||||
m_vpnConnectionThread.wait(3000);
|
||||
|
||||
if (m_engine) {
|
||||
QObject::disconnect(m_engine, 0, 0, 0);
|
||||
delete m_engine;
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaApplication::init()
|
||||
{
|
||||
m_engine = new QQmlApplicationEngine;
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
|
||||
QObject::connect(
|
||||
m_engine, &QQmlApplicationEngine::objectCreated, this,
|
||||
[url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl)
|
||||
QCoreApplication::exit(-1);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_engine->rootContext()->setContextProperty("Debug", &Logger::Instance());
|
||||
|
||||
m_configurator = std::shared_ptr<VpnConfigurator>(new VpnConfigurator(m_settings, this));
|
||||
m_vpnConnection.reset(new VpnConnection(m_settings, m_configurator));
|
||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
||||
m_vpnConnectionThread.start();
|
||||
|
||||
initModels();
|
||||
loadTranslator();
|
||||
initControllers();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if(!AndroidController::initLogging()) {
|
||||
qFatal("Android logging initialization failed");
|
||||
}
|
||||
AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs());
|
||||
connect(m_settings.get(), &Settings::saveLogsChanged,
|
||||
AndroidController::instance(), &AndroidController::setSaveLogs);
|
||||
|
||||
connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
|
||||
[this](Vpn::ConnectionState state) {
|
||||
m_connectionController->onConnectionStateChanged(state);
|
||||
if (m_vpnConnection)
|
||||
m_vpnConnection->restoreConnection();
|
||||
});
|
||||
if (!AndroidController::instance()->initialize()) {
|
||||
qFatal("Android controller initialization failed");
|
||||
}
|
||||
|
||||
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) {
|
||||
m_pageController->replaceStartPage();
|
||||
m_importController->extractConfigFromData(data);
|
||||
m_pageController->goToPageViewConfig();
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->initialize();
|
||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) {
|
||||
m_pageController->replaceStartPage();
|
||||
m_importController->extractConfigFromData(data);
|
||||
m_pageController->goToPageViewConfig();
|
||||
});
|
||||
|
||||
connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) {
|
||||
m_pageController->replaceStartPage();
|
||||
m_pageController->goToPageSettingsBackup();
|
||||
m_settingsController->importBackupFromOutside(filePath);
|
||||
});
|
||||
#endif
|
||||
|
||||
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
||||
|
||||
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
|
||||
&NotificationHandler::setConnectionState);
|
||||
|
||||
connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(),
|
||||
&PageController::raiseMainWindow);
|
||||
connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(),
|
||||
&ConnectionController::openConnection);
|
||||
connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(),
|
||||
&ConnectionController::closeConnection);
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(),
|
||||
&NotificationHandler::onTranslationsUpdated);
|
||||
|
||||
m_engine->load(url);
|
||||
m_systemController->setQmlRoot(m_engine->rootObjects().value(0));
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
if (m_settings->isSaveLogs()) {
|
||||
if (!Logger::init()) {
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (m_parser.isSet("a"))
|
||||
m_pageController->showOnStartup();
|
||||
else
|
||||
emit m_pageController->raiseMainWindow();
|
||||
#else
|
||||
m_pageController->showOnStartup();
|
||||
#endif
|
||||
|
||||
// TODO - fix
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (isPrimary()) {
|
||||
QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() {
|
||||
qDebug() << "Secondary instance started, showing this window instead";
|
||||
emit m_pageController->raiseMainWindow();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// Android TextArea clipboard workaround
|
||||
// Text from TextArea always has "text/html" mime-type:
|
||||
// /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1865
|
||||
// Next, html is created for this mime-type:
|
||||
// /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1885
|
||||
// And this html goes to the Androids clipboard, i.e. text from TextArea is always copied as richText:
|
||||
// /qt/6.6.1/Src/qtbase/src/plugins/platforms/android/androidjniclipboard.cpp:46
|
||||
// So we catch all the copies to the clipboard and clear them from "text/html"
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, []() {
|
||||
auto clipboard = QGuiApplication::clipboard();
|
||||
if (clipboard->mimeData()->hasHtml()) {
|
||||
clipboard->setText(clipboard->text());
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void AmneziaApplication::registerTypes()
|
||||
{
|
||||
qRegisterMetaType<ServerCredentials>("ServerCredentials");
|
||||
|
||||
qRegisterMetaType<DockerContainer>("DockerContainer");
|
||||
qRegisterMetaType<TransportProto>("TransportProto");
|
||||
qRegisterMetaType<Proto>("Proto");
|
||||
qRegisterMetaType<ServiceType>("ServiceType");
|
||||
|
||||
declareQmlProtocolEnum();
|
||||
declareQmlContainerEnum();
|
||||
|
||||
qmlRegisterType<QRCodeReader>("QRCodeReader", 1, 0, "QRCodeReader");
|
||||
|
||||
m_containerProps.reset(new ContainerProps());
|
||||
qmlRegisterSingletonInstance("ContainerProps", 1, 0, "ContainerProps", m_containerProps.get());
|
||||
|
||||
m_protocolProps.reset(new ProtocolProps());
|
||||
qmlRegisterSingletonInstance("ProtocolProps", 1, 0, "ProtocolProps", m_protocolProps.get());
|
||||
|
||||
qmlRegisterSingletonType(QUrl("qrc:/ui/qml/Filters/ContainersModelFilters.qml"), "ContainersModelFilters", 1, 0,
|
||||
"ContainersModelFilters");
|
||||
|
||||
//
|
||||
Vpn::declareQmlVpnConnectionStateEnum();
|
||||
PageLoader::declareQmlPageEnum();
|
||||
}
|
||||
|
||||
void AmneziaApplication::loadFonts()
|
||||
{
|
||||
QQuickStyle::setStyle("Basic");
|
||||
|
||||
QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf");
|
||||
}
|
||||
|
||||
void AmneziaApplication::loadTranslator()
|
||||
{
|
||||
auto locale = m_settings->getAppLanguage();
|
||||
m_translator.reset(new QTranslator());
|
||||
updateTranslator(locale);
|
||||
}
|
||||
|
||||
void AmneziaApplication::updateTranslator(const QLocale &locale)
|
||||
{
|
||||
if (!m_translator->isEmpty()) {
|
||||
QCoreApplication::removeTranslator(m_translator.get());
|
||||
}
|
||||
|
||||
QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm";
|
||||
if (m_translator->load(strFileName)) {
|
||||
if (QCoreApplication::installTranslator(m_translator.get())) {
|
||||
m_settings->setAppLanguage(locale);
|
||||
}
|
||||
} else {
|
||||
m_settings->setAppLanguage(QLocale::English);
|
||||
}
|
||||
|
||||
m_engine->retranslate();
|
||||
|
||||
emit translationsUpdated();
|
||||
}
|
||||
|
||||
bool AmneziaApplication::parseCommands()
|
||||
{
|
||||
m_parser.setApplicationDescription(APPLICATION_NAME);
|
||||
m_parser.addHelpOption();
|
||||
m_parser.addVersionOption();
|
||||
|
||||
QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" };
|
||||
m_parser.addOption(c_autostart);
|
||||
|
||||
QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" };
|
||||
m_parser.addOption(c_cleanup);
|
||||
|
||||
m_parser.process(*this);
|
||||
|
||||
if (m_parser.isSet(c_cleanup)) {
|
||||
Logger::cleanUp();
|
||||
QTimer::singleShot(100, this, [this] { quit(); });
|
||||
exec();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
||||
{
|
||||
return m_engine;
|
||||
}
|
||||
|
||||
void AmneziaApplication::initModels()
|
||||
{
|
||||
m_containersModel.reset(new ContainersModel(this));
|
||||
m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get());
|
||||
|
||||
m_serversModel.reset(new ServersModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get());
|
||||
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(),
|
||||
&ContainersModel::updateModel);
|
||||
connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(),
|
||||
&ContainersModel::setDefaultContainer);
|
||||
m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better?
|
||||
|
||||
m_languageModel.reset(new LanguageModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get());
|
||||
connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &AmneziaApplication::updateTranslator);
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated);
|
||||
|
||||
m_sitesModel.reset(new SitesModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get());
|
||||
|
||||
m_protocolsModel.reset(new ProtocolsModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get());
|
||||
|
||||
m_openVpnConfigModel.reset(new OpenVpnConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get());
|
||||
|
||||
m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get());
|
||||
|
||||
m_cloakConfigModel.reset(new CloakConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get());
|
||||
|
||||
m_wireGuardConfigModel.reset(new WireGuardConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get());
|
||||
|
||||
m_awgConfigModel.reset(new AwgConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get());
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get());
|
||||
#endif
|
||||
|
||||
m_sftpConfigModel.reset(new SftpConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get());
|
||||
|
||||
m_clientManagementModel.reset(new ClientManagementModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get());
|
||||
connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked,
|
||||
m_serversModel.get(), &ServersModel::clearCachedProfile);
|
||||
|
||||
connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this,
|
||||
[this](const QString &clientId, const QString &clientName, const DockerContainer container,
|
||||
ServerCredentials credentials) {
|
||||
m_serversModel->reloadContainerConfig();
|
||||
m_clientManagementModel->appendClient(clientId, clientName, container, credentials);
|
||||
emit m_configurator->clientModelUpdated();
|
||||
});
|
||||
}
|
||||
|
||||
void AmneziaApplication::initControllers()
|
||||
{
|
||||
m_connectionController.reset(new ConnectionController(m_serversModel, m_containersModel, m_vpnConnection));
|
||||
m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get());
|
||||
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(),
|
||||
&ConnectionController::onTranslationsUpdated);
|
||||
|
||||
m_pageController.reset(new PageController(m_serversModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
|
||||
|
||||
m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
|
||||
connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(),
|
||||
&PageController::showPassphraseRequestDrawer);
|
||||
connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(),
|
||||
&InstallController::setEncryptedPassphrase);
|
||||
connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(),
|
||||
&ConnectionController::onCurrentContainerUpdated);
|
||||
|
||||
m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("ImportController", m_importController.get());
|
||||
|
||||
m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel,
|
||||
m_settings, m_configurator));
|
||||
m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get());
|
||||
|
||||
m_settingsController.reset(new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
||||
if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) {
|
||||
QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); });
|
||||
}
|
||||
connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled , m_serversModel.get(),
|
||||
&ServersModel::toggleAmneziaDns);
|
||||
|
||||
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
||||
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
||||
|
||||
m_systemController.reset(new SystemController(m_settings));
|
||||
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
|
||||
|
||||
m_cloudController.reset(new ApiController(m_serversModel, m_containersModel));
|
||||
m_engine->rootContext()->setContextProperty("ApiController", m_cloudController.get());
|
||||
}
|
||||
#include "amnezia_application.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFontDatabase>
|
||||
#include <QMimeData>
|
||||
#include <QQuickStyle>
|
||||
#include <QResource>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "logger.h"
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#include "protocols/qml_register_protocols.h"
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv)
|
||||
#else
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options,
|
||||
int timeout, const QString &userData)
|
||||
: SingleApplication(argc, argv, allowSecondary, options, timeout, userData)
|
||||
#endif
|
||||
{
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
// Fix config file permissions
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
{
|
||||
QSettings s(ORGANIZATION_NAME, APPLICATION_NAME);
|
||||
s.setValue("permFixed", true);
|
||||
}
|
||||
|
||||
QString configLoc1 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/"
|
||||
+ ORGANIZATION_NAME + "/" + APPLICATION_NAME + ".conf";
|
||||
QFile::setPermissions(configLoc1, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
|
||||
QString configLoc2 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/"
|
||||
+ ORGANIZATION_NAME + "/" + APPLICATION_NAME + "/" + APPLICATION_NAME + ".conf";
|
||||
QFile::setPermissions(configLoc2, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
#endif
|
||||
|
||||
m_settings = std::shared_ptr<Settings>(new Settings);
|
||||
}
|
||||
|
||||
AmneziaApplication::~AmneziaApplication()
|
||||
{
|
||||
m_vpnConnectionThread.quit();
|
||||
m_vpnConnectionThread.wait(3000);
|
||||
|
||||
if (m_engine) {
|
||||
QObject::disconnect(m_engine, 0, 0, 0);
|
||||
delete m_engine;
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaApplication::init()
|
||||
{
|
||||
m_engine = new QQmlApplicationEngine;
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
|
||||
QObject::connect(
|
||||
m_engine, &QQmlApplicationEngine::objectCreated, this,
|
||||
[url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl)
|
||||
QCoreApplication::exit(-1);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_engine->rootContext()->setContextProperty("Debug", &Logger::Instance());
|
||||
|
||||
m_configurator = std::shared_ptr<VpnConfigurator>(new VpnConfigurator(m_settings, this));
|
||||
m_vpnConnection.reset(new VpnConnection(m_settings, m_configurator));
|
||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
||||
m_vpnConnectionThread.start();
|
||||
|
||||
initModels();
|
||||
loadTranslator();
|
||||
initControllers();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(AndroidController::instance(), &AndroidController::initialized, this,
|
||||
[this](bool status, bool connected, const QDateTime &connectionDate) {
|
||||
if (connected) {
|
||||
m_connectionController->onConnectionStateChanged(Vpn::ConnectionState::Connected);
|
||||
if (m_vpnConnection)
|
||||
m_vpnConnection->restoreConnection();
|
||||
}
|
||||
});
|
||||
if (!AndroidController::instance()->initialize()) {
|
||||
qCritical() << QString("Init failed");
|
||||
if (m_vpnConnection)
|
||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) {
|
||||
m_pageController->replaceStartPage();
|
||||
m_importController->extractConfigFromData(data);
|
||||
m_pageController->goToPageViewConfig();
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->initialize();
|
||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) {
|
||||
m_pageController->replaceStartPage();
|
||||
m_importController->extractConfigFromData(data);
|
||||
m_pageController->goToPageViewConfig();
|
||||
});
|
||||
|
||||
connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) {
|
||||
m_pageController->replaceStartPage();
|
||||
m_pageController->goToPageSettingsBackup();
|
||||
m_settingsController->importBackupFromOutside(filePath);
|
||||
});
|
||||
#endif
|
||||
|
||||
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
||||
|
||||
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
|
||||
&NotificationHandler::setConnectionState);
|
||||
|
||||
connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(),
|
||||
&PageController::raiseMainWindow);
|
||||
connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(),
|
||||
&ConnectionController::openConnection);
|
||||
connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(),
|
||||
&ConnectionController::closeConnection);
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(),
|
||||
&NotificationHandler::onTranslationsUpdated);
|
||||
|
||||
m_engine->load(url);
|
||||
m_systemController->setQmlRoot(m_engine->rootObjects().value(0));
|
||||
|
||||
if (m_settings->isSaveLogs()) {
|
||||
if (!Logger::init()) {
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (m_parser.isSet("a"))
|
||||
m_pageController->showOnStartup();
|
||||
else
|
||||
emit m_pageController->raiseMainWindow();
|
||||
#else
|
||||
m_pageController->showOnStartup();
|
||||
#endif
|
||||
|
||||
// TODO - fix
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (isPrimary()) {
|
||||
QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() {
|
||||
qDebug() << "Secondary instance started, showing this window instead";
|
||||
emit m_pageController->raiseMainWindow();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// Android TextField clipboard workaround
|
||||
// https://bugreports.qt.io/browse/QTBUG-113461
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(qApp, &QGuiApplication::applicationStateChanged, [](Qt::ApplicationState state) {
|
||||
if (state == Qt::ApplicationActive) {
|
||||
if (qApp->clipboard()->mimeData()->formats().contains("text/html")) {
|
||||
QTextDocument doc;
|
||||
doc.setHtml(qApp->clipboard()->mimeData()->html());
|
||||
qApp->clipboard()->setText(doc.toPlainText());
|
||||
}
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void AmneziaApplication::registerTypes()
|
||||
{
|
||||
qRegisterMetaType<ServerCredentials>("ServerCredentials");
|
||||
|
||||
qRegisterMetaType<DockerContainer>("DockerContainer");
|
||||
qRegisterMetaType<TransportProto>("TransportProto");
|
||||
qRegisterMetaType<Proto>("Proto");
|
||||
qRegisterMetaType<ServiceType>("ServiceType");
|
||||
|
||||
declareQmlProtocolEnum();
|
||||
declareQmlContainerEnum();
|
||||
|
||||
qmlRegisterType<QRCodeReader>("QRCodeReader", 1, 0, "QRCodeReader");
|
||||
|
||||
m_containerProps.reset(new ContainerProps());
|
||||
qmlRegisterSingletonInstance("ContainerProps", 1, 0, "ContainerProps", m_containerProps.get());
|
||||
|
||||
m_protocolProps.reset(new ProtocolProps());
|
||||
qmlRegisterSingletonInstance("ProtocolProps", 1, 0, "ProtocolProps", m_protocolProps.get());
|
||||
|
||||
qmlRegisterSingletonType(QUrl("qrc:/ui/qml/Filters/ContainersModelFilters.qml"), "ContainersModelFilters", 1, 0,
|
||||
"ContainersModelFilters");
|
||||
|
||||
//
|
||||
Vpn::declareQmlVpnConnectionStateEnum();
|
||||
PageLoader::declareQmlPageEnum();
|
||||
}
|
||||
|
||||
void AmneziaApplication::loadFonts()
|
||||
{
|
||||
QQuickStyle::setStyle("Basic");
|
||||
|
||||
QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf");
|
||||
}
|
||||
|
||||
void AmneziaApplication::loadTranslator()
|
||||
{
|
||||
auto locale = m_settings->getAppLanguage();
|
||||
m_translator.reset(new QTranslator());
|
||||
updateTranslator(locale);
|
||||
}
|
||||
|
||||
void AmneziaApplication::updateTranslator(const QLocale &locale)
|
||||
{
|
||||
if (!m_translator->isEmpty()) {
|
||||
QCoreApplication::removeTranslator(m_translator.get());
|
||||
}
|
||||
|
||||
QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm";
|
||||
if (m_translator->load(strFileName)) {
|
||||
if (QCoreApplication::installTranslator(m_translator.get())) {
|
||||
m_settings->setAppLanguage(locale);
|
||||
}
|
||||
} else {
|
||||
m_settings->setAppLanguage(QLocale::English);
|
||||
}
|
||||
|
||||
m_engine->retranslate();
|
||||
|
||||
emit translationsUpdated();
|
||||
}
|
||||
|
||||
bool AmneziaApplication::parseCommands()
|
||||
{
|
||||
m_parser.setApplicationDescription(APPLICATION_NAME);
|
||||
m_parser.addHelpOption();
|
||||
m_parser.addVersionOption();
|
||||
|
||||
QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" };
|
||||
m_parser.addOption(c_autostart);
|
||||
|
||||
QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" };
|
||||
m_parser.addOption(c_cleanup);
|
||||
|
||||
m_parser.process(*this);
|
||||
|
||||
if (m_parser.isSet(c_cleanup)) {
|
||||
Logger::cleanUp();
|
||||
QTimer::singleShot(100, this, [this] { quit(); });
|
||||
exec();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
||||
{
|
||||
return m_engine;
|
||||
}
|
||||
|
||||
void AmneziaApplication::initModels()
|
||||
{
|
||||
m_containersModel.reset(new ContainersModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get());
|
||||
connect(m_vpnConnection.get(), &VpnConnection::newVpnConfigurationCreated, m_containersModel.get(),
|
||||
&ContainersModel::updateContainersConfig);
|
||||
|
||||
m_serversModel.reset(new ServersModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get());
|
||||
connect(m_serversModel.get(), &ServersModel::currentlyProcessedServerIndexChanged, m_containersModel.get(),
|
||||
&ContainersModel::setCurrentlyProcessedServerIndex);
|
||||
connect(m_serversModel.get(), &ServersModel::defaultServerIndexChanged, m_containersModel.get(),
|
||||
&ContainersModel::setCurrentlyProcessedServerIndex);
|
||||
connect(m_containersModel.get(), &ContainersModel::containersModelUpdated, m_serversModel.get(),
|
||||
&ServersModel::updateContainersConfig);
|
||||
|
||||
m_languageModel.reset(new LanguageModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get());
|
||||
connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &AmneziaApplication::updateTranslator);
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated);
|
||||
|
||||
m_sitesModel.reset(new SitesModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get());
|
||||
|
||||
m_protocolsModel.reset(new ProtocolsModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get());
|
||||
|
||||
m_openVpnConfigModel.reset(new OpenVpnConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get());
|
||||
|
||||
m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get());
|
||||
|
||||
m_cloakConfigModel.reset(new CloakConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get());
|
||||
|
||||
m_wireGuardConfigModel.reset(new WireGuardConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get());
|
||||
|
||||
m_awgConfigModel.reset(new AwgConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get());
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get());
|
||||
#endif
|
||||
|
||||
m_sftpConfigModel.reset(new SftpConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get());
|
||||
}
|
||||
|
||||
void AmneziaApplication::initControllers()
|
||||
{
|
||||
m_connectionController.reset(new ConnectionController(m_serversModel, m_containersModel, m_vpnConnection));
|
||||
m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get());
|
||||
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(),
|
||||
&ConnectionController::onTranslationsUpdated);
|
||||
|
||||
m_pageController.reset(new PageController(m_serversModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
|
||||
|
||||
m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
|
||||
connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(),
|
||||
&PageController::showPassphraseRequestDrawer);
|
||||
connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(),
|
||||
&InstallController::setEncryptedPassphrase);
|
||||
connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(),
|
||||
&ConnectionController::onCurrentContainerUpdated);
|
||||
|
||||
m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("ImportController", m_importController.get());
|
||||
|
||||
m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_settings, m_configurator));
|
||||
m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get());
|
||||
|
||||
m_settingsController.reset(new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
||||
if (m_settingsController->isAutoStartEnabled() && m_serversModel->getDefaultServerIndex() >= 0) {
|
||||
QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); });
|
||||
}
|
||||
|
||||
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
||||
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
||||
|
||||
m_systemController.reset(new SystemController(m_settings));
|
||||
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
|
||||
}
|
||||
|
||||
@@ -1,127 +1,123 @@
|
||||
#ifndef AMNEZIA_APPLICATION_H
|
||||
#define AMNEZIA_APPLICATION_H
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#include <QGuiApplication>
|
||||
#else
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
#include "settings.h"
|
||||
#include "vpnconnection.h"
|
||||
|
||||
#include "configurators/vpn_configurator.h"
|
||||
|
||||
#include "ui/controllers/connectionController.h"
|
||||
#include "ui/controllers/exportController.h"
|
||||
#include "ui/controllers/importController.h"
|
||||
#include "ui/controllers/installController.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
#include "ui/controllers/settingsController.h"
|
||||
#include "ui/controllers/sitesController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
#include "ui/controllers/apiController.h"
|
||||
#include "ui/models/containers_model.h"
|
||||
#include "ui/models/languageModel.h"
|
||||
#include "ui/models/protocols/cloakConfigModel.h"
|
||||
#include "ui/notificationhandler.h"
|
||||
#ifdef Q_OS_WINDOWS
|
||||
#include "ui/models/protocols/ikev2ConfigModel.h"
|
||||
#endif
|
||||
#include "ui/models/protocols/awgConfigModel.h"
|
||||
#include "ui/models/protocols/openvpnConfigModel.h"
|
||||
#include "ui/models/protocols/shadowsocksConfigModel.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "ui/models/protocols_model.h"
|
||||
#include "ui/models/servers_model.h"
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/sites_model.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
|
||||
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#define AMNEZIA_BASE_CLASS QGuiApplication
|
||||
#else
|
||||
#define AMNEZIA_BASE_CLASS SingleApplication
|
||||
#define QAPPLICATION_CLASS QApplication
|
||||
#include "singleapplication.h"
|
||||
#endif
|
||||
|
||||
class AmneziaApplication : public AMNEZIA_BASE_CLASS
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication(int &argc, char *argv[]);
|
||||
#else
|
||||
AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false,
|
||||
SingleApplication::Options options = SingleApplication::User, int timeout = 1000,
|
||||
const QString &userData = {});
|
||||
#endif
|
||||
virtual ~AmneziaApplication();
|
||||
|
||||
void init();
|
||||
void registerTypes();
|
||||
void loadFonts();
|
||||
void loadTranslator();
|
||||
void updateTranslator(const QLocale &locale);
|
||||
bool parseCommands();
|
||||
|
||||
QQmlApplicationEngine *qmlEngine() const;
|
||||
|
||||
signals:
|
||||
void translationsUpdated();
|
||||
|
||||
private:
|
||||
void initModels();
|
||||
void initControllers();
|
||||
|
||||
QQmlApplicationEngine *m_engine {};
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
std::shared_ptr<VpnConfigurator> m_configurator;
|
||||
|
||||
QSharedPointer<ContainerProps> m_containerProps;
|
||||
QSharedPointer<ProtocolProps> m_protocolProps;
|
||||
|
||||
QSharedPointer<QTranslator> m_translator;
|
||||
QCommandLineParser m_parser;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
QSharedPointer<LanguageModel> m_languageModel;
|
||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||
QSharedPointer<SitesModel> m_sitesModel;
|
||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||
|
||||
QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel;
|
||||
QScopedPointer<ShadowSocksConfigModel> m_shadowSocksConfigModel;
|
||||
QScopedPointer<CloakConfigModel> m_cloakConfigModel;
|
||||
QScopedPointer<WireGuardConfigModel> m_wireGuardConfigModel;
|
||||
QScopedPointer<AwgConfigModel> m_awgConfigModel;
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QScopedPointer<Ikev2ConfigModel> m_ikev2ConfigModel;
|
||||
#endif
|
||||
|
||||
QScopedPointer<SftpConfigModel> m_sftpConfigModel;
|
||||
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QThread m_vpnConnectionThread;
|
||||
QScopedPointer<NotificationHandler> m_notificationHandler;
|
||||
|
||||
QScopedPointer<ConnectionController> m_connectionController;
|
||||
QScopedPointer<PageController> m_pageController;
|
||||
QScopedPointer<InstallController> m_installController;
|
||||
QScopedPointer<ImportController> m_importController;
|
||||
QScopedPointer<ExportController> m_exportController;
|
||||
QScopedPointer<SettingsController> m_settingsController;
|
||||
QScopedPointer<SitesController> m_sitesController;
|
||||
QScopedPointer<SystemController> m_systemController;
|
||||
QScopedPointer<ApiController> m_cloudController;
|
||||
};
|
||||
|
||||
#endif // AMNEZIA_APPLICATION_H
|
||||
#ifndef AMNEZIA_APPLICATION_H
|
||||
#define AMNEZIA_APPLICATION_H
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#include <QGuiApplication>
|
||||
#else
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
#include "settings.h"
|
||||
#include "vpnconnection.h"
|
||||
|
||||
#include "configurators/vpn_configurator.h"
|
||||
|
||||
#include "ui/controllers/connectionController.h"
|
||||
#include "ui/controllers/exportController.h"
|
||||
#include "ui/controllers/importController.h"
|
||||
#include "ui/controllers/installController.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
#include "ui/controllers/settingsController.h"
|
||||
#include "ui/controllers/sitesController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
#include "ui/models/containers_model.h"
|
||||
#include "ui/models/languageModel.h"
|
||||
#include "ui/models/protocols/cloakConfigModel.h"
|
||||
#include "ui/notificationhandler.h"
|
||||
#ifdef Q_OS_WINDOWS
|
||||
#include "ui/models/protocols/ikev2ConfigModel.h"
|
||||
#endif
|
||||
#include "ui/models/protocols/awgConfigModel.h"
|
||||
#include "ui/models/protocols/openvpnConfigModel.h"
|
||||
#include "ui/models/protocols/shadowsocksConfigModel.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "ui/models/protocols_model.h"
|
||||
#include "ui/models/servers_model.h"
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/sites_model.h"
|
||||
|
||||
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#define AMNEZIA_BASE_CLASS QGuiApplication
|
||||
#else
|
||||
#define AMNEZIA_BASE_CLASS SingleApplication
|
||||
#define QAPPLICATION_CLASS QApplication
|
||||
#include "singleapplication.h"
|
||||
#endif
|
||||
|
||||
class AmneziaApplication : public AMNEZIA_BASE_CLASS
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication(int &argc, char *argv[]);
|
||||
#else
|
||||
AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false,
|
||||
SingleApplication::Options options = SingleApplication::User, int timeout = 1000,
|
||||
const QString &userData = {});
|
||||
#endif
|
||||
virtual ~AmneziaApplication();
|
||||
|
||||
void init();
|
||||
void registerTypes();
|
||||
void loadFonts();
|
||||
void loadTranslator();
|
||||
void updateTranslator(const QLocale &locale);
|
||||
bool parseCommands();
|
||||
|
||||
QQmlApplicationEngine *qmlEngine() const;
|
||||
|
||||
signals:
|
||||
void translationsUpdated();
|
||||
|
||||
private:
|
||||
void initModels();
|
||||
void initControllers();
|
||||
|
||||
QQmlApplicationEngine *m_engine {};
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
std::shared_ptr<VpnConfigurator> m_configurator;
|
||||
|
||||
QSharedPointer<ContainerProps> m_containerProps;
|
||||
QSharedPointer<ProtocolProps> m_protocolProps;
|
||||
|
||||
QSharedPointer<QTranslator> m_translator;
|
||||
QCommandLineParser m_parser;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
QSharedPointer<LanguageModel> m_languageModel;
|
||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||
QSharedPointer<SitesModel> m_sitesModel;
|
||||
|
||||
QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel;
|
||||
QScopedPointer<ShadowSocksConfigModel> m_shadowSocksConfigModel;
|
||||
QScopedPointer<CloakConfigModel> m_cloakConfigModel;
|
||||
QScopedPointer<WireGuardConfigModel> m_wireGuardConfigModel;
|
||||
QScopedPointer<AwgConfigModel> m_awgConfigModel;
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QScopedPointer<Ikev2ConfigModel> m_ikev2ConfigModel;
|
||||
#endif
|
||||
|
||||
QScopedPointer<SftpConfigModel> m_sftpConfigModel;
|
||||
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QThread m_vpnConnectionThread;
|
||||
QScopedPointer<NotificationHandler> m_notificationHandler;
|
||||
|
||||
QScopedPointer<ConnectionController> m_connectionController;
|
||||
QScopedPointer<PageController> m_pageController;
|
||||
QScopedPointer<InstallController> m_installController;
|
||||
QScopedPointer<ImportController> m_importController;
|
||||
QScopedPointer<ExportController> m_exportController;
|
||||
QScopedPointer<SettingsController> m_settingsController;
|
||||
QScopedPointer<SitesController> m_sitesController;
|
||||
QScopedPointer<SystemController> m_systemController;
|
||||
};
|
||||
|
||||
#endif // AMNEZIA_APPLICATION_H
|
||||
|
||||
@@ -1,158 +1,166 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- Leave package attribute for androiddeployqt -->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.amnezia.vpn"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionName="-- %%INSERT_VERSION_NAME%% --"
|
||||
android:versionCode="-- %%INSERT_VERSION_CODE%% --"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
|
||||
<!-- The following comment will be replaced upon deployment with default features based on the dependencies
|
||||
of the application. Remove the comment if you do not require these default features. -->
|
||||
<!-- Enable when VPN-per-app mode will be implemented -->
|
||||
<!-- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default features. -->
|
||||
<!-- %%INSERT_FEATURES -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- To request network state -->
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" android:maxSdkVersion="30" />
|
||||
<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.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Enable when VPN-per-app mode will be implemented -->
|
||||
<!-- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> -->
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:anyDensity="true"
|
||||
android:smallScreens="true"/>
|
||||
|
||||
<application
|
||||
android:name=".AmneziaApplication"
|
||||
android:name=".qt.AmneziaApp"
|
||||
android:hardwareAccelerated="true"
|
||||
android:label="-- %%INSERT_APP_NAME%% --"
|
||||
android:icon="@mipmap/icon"
|
||||
android:roundIcon="@mipmap/icon_round"
|
||||
android:theme="@style/NoActionBar"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
tools:targetApi="s">
|
||||
|
||||
android:extractNativeLibs="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||
android:icon="@drawable/icon"
|
||||
android:roundIcon="@drawable/icon_round">
|
||||
|
||||
<activity
|
||||
android:name=".AmneziaActivity"
|
||||
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|
||||
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
|
||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
|
||||
android:name=".qt.VPNActivity"
|
||||
android:label="-- %%INSERT_APP_NAME%% --"
|
||||
android:screenOrientation="unspecified"
|
||||
android:launchMode="singleInstance"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
|
||||
|
||||
<!-- android:theme="@style/splashScreenTheme"-->
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.amnezia.vpn.IMPORT_CONFIG" />
|
||||
<action android:name="org.amnezia.vpn.qt.IMPORT_CONFIG" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
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" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.background_running"
|
||||
android:value="false"/>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.arguments"
|
||||
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
|
||||
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".CameraActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:name=".qt.CameraActivity"
|
||||
android:exported="false" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".VpnRequestActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="false"
|
||||
android:theme="@style/Translucent" />
|
||||
|
||||
<activity
|
||||
android:name=".ImportConfigActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="true"
|
||||
android:theme="@style/Translucent">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="text/plain" />
|
||||
android:name=".qt.ImportConfigActivity"
|
||||
android:exported="true" >
|
||||
|
||||
<intent-filter android:label="AmneziaVPN">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.vpn"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="vpn" android:host="*" />
|
||||
|
||||
<intent-filter android:label="AmneziaVPN">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.cfg"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
|
||||
<data android:pathPattern=".*\\.vpn" />
|
||||
<data android:pathPattern=".*\\..*\\.vpn" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.vpn" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.vpn" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.vpn" />
|
||||
|
||||
<data android:pathPattern=".*\\.cfg" />
|
||||
<data android:pathPattern=".*\\..*\\.cfg" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.cfg" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.cfg" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.cfg" />
|
||||
|
||||
<data android:pathPattern=".*\\.conf" />
|
||||
<data android:pathPattern=".*\\..*\\.conf" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.conf" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf" />
|
||||
|
||||
<intent-filter android:label="AmneziaVPN">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".AmneziaVpnService"
|
||||
android:process=":amneziaVpnService"
|
||||
android:name=".VPNService"
|
||||
android:process=":QtOnlyProcess"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="vpn" />
|
||||
android:foregroundServiceType="connectedDevice"
|
||||
android:exported="true">
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
||||
<service
|
||||
android:name=".qt.VPNPermissionHelper"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="connectedDevice"
|
||||
android:exported="true">
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
</service>
|
||||
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.amnezia.vpn.qtprovider"
|
||||
android:authorities="org.amnezia.vpn.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider"/>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
10377
client/android/assets/acl/bypass-china.acl
Normal file
10391
client/android/assets/acl/bypass-lan-china.acl
Normal file
17
client/android/assets/acl/bypass-lan.acl
Normal file
@@ -0,0 +1,17 @@
|
||||
[proxy_all]
|
||||
|
||||
[bypass_list]
|
||||
0.0.0.0/8
|
||||
10.0.0.0/8
|
||||
100.64.0.0/10
|
||||
127.0.0.0/8
|
||||
169.254.0.0/16
|
||||
172.16.0.0/12
|
||||
192.0.0.0/29
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.18.0.0/15
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/3
|
||||
5245
client/android/assets/acl/china-list.acl
Normal file
5492
client/android/assets/acl/gfwlist.acl
Normal file
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.protocol.awg"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
compileOnly(project(":protocolApi"))
|
||||
implementation(project(":wireguard"))
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.amnezia.vpn.protocol.awg
|
||||
|
||||
import org.amnezia.vpn.protocol.wireguard.Wireguard
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Config example:
|
||||
* {
|
||||
* "protocol": "awg",
|
||||
* "description": "Server 1",
|
||||
* "dns1": "1.1.1.1",
|
||||
* "dns2": "1.0.0.1",
|
||||
* "hostName": "100.100.100.0",
|
||||
* "splitTunnelSites": [
|
||||
* ],
|
||||
* "splitTunnelType": 0,
|
||||
* "awg_config_data": {
|
||||
* "H1": "969537490",
|
||||
* "H2": "481688153",
|
||||
* "H3": "2049399200",
|
||||
* "H4": "52029755",
|
||||
* "Jc": "3",
|
||||
* "Jmax": "1000",
|
||||
* "Jmin": "50",
|
||||
* "S1": "49",
|
||||
* "S2": "60",
|
||||
* "client_ip": "10.8.1.1",
|
||||
* "hostName": "100.100.100.0",
|
||||
* "port": 12345,
|
||||
* "client_pub_key": "clientPublicKeyBase64",
|
||||
* "client_priv_key": "privateKeyBase64",
|
||||
* "psk_key": "presharedKeyBase64",
|
||||
* "server_pub_key": "publicKeyBase64",
|
||||
* "config": "[Interface]
|
||||
* Address = 10.8.1.1/32
|
||||
* DNS = 1.1.1.1, 1.0.0.1
|
||||
* PrivateKey = privateKeyBase64
|
||||
* Jc = 3
|
||||
* Jmin = 50
|
||||
* Jmax = 1000
|
||||
* S1 = 49
|
||||
* S2 = 60
|
||||
* H1 = 969537490
|
||||
* H2 = 481688153
|
||||
* H3 = 2049399200
|
||||
* H4 = 52029755
|
||||
*
|
||||
* [Peer]
|
||||
* PublicKey = publicKeyBase64
|
||||
* PresharedKey = presharedKeyBase64
|
||||
* AllowedIPs = 0.0.0.0/0, ::/0
|
||||
* Endpoint = 100.100.100.0:12345
|
||||
* PersistentKeepalive = 25
|
||||
* "
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
class Awg : Wireguard() {
|
||||
|
||||
override val ifName: String = "awg0"
|
||||
|
||||
override fun parseConfig(config: JSONObject): AwgConfig {
|
||||
val configDataJson = config.getJSONObject("awg_config_data")
|
||||
val configData = parseConfigData(configDataJson.getString("config"))
|
||||
return AwgConfig.build {
|
||||
configWireguard(configData)
|
||||
configSplitTunneling(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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,153 @@
|
||||
// dummy file for androiddeployqt
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
buildscript {
|
||||
ext{
|
||||
kotlin_version = "1.7.22"
|
||||
// for libwg
|
||||
appcompatVersion = '1.1.0'
|
||||
annotationsVersion = '1.0.1'
|
||||
databindingVersion = '3.3.1'
|
||||
jsr305Version = '3.0.2'
|
||||
streamsupportVersion = '1.7.0'
|
||||
threetenabpVersion = '1.1.1'
|
||||
groupName = 'org.amnezia.vpn'
|
||||
minSdkVer = '24'
|
||||
cmakeMinVersion = "3.25.0+"
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
||||
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation group: 'org.json', name: 'json', version: '20220924'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
||||
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||
|
||||
implementation project(path: ':shadowsocks')
|
||||
|
||||
// CameraX core library using the camera2 implementation
|
||||
def camerax_version = "1.2.1"
|
||||
implementation("androidx.camera:camera-core:${camerax_version}")
|
||||
implementation("androidx.camera:camera-camera2:${camerax_version}")
|
||||
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
|
||||
implementation("androidx.camera:camera-view:${camerax_version}")
|
||||
implementation("androidx.camera:camera-extensions:${camerax_version}")
|
||||
|
||||
def camerax_ml_version = "1.2.0-beta02"
|
||||
def ml_kit_version = "17.0.3"
|
||||
implementation("androidx.camera:camera-mlkit-vision:${camerax_ml_version}")
|
||||
implementation("com.google.mlkit:barcode-scanning:${ml_kit_version}")
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
android {
|
||||
/*******************************************************
|
||||
* The following variables:
|
||||
* - androidBuildToolsVersion,
|
||||
* - androidCompileSdkVersion
|
||||
* - qtAndroidDir - holds the path to qt android files
|
||||
* needed to build any Qt application
|
||||
* on Android.
|
||||
*
|
||||
* are defined in gradle.properties file. This file is
|
||||
* updated by QtCreator and androiddeployqt tools.
|
||||
* Changing them manually might break the compilation!
|
||||
*******************************************************/
|
||||
|
||||
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||
|
||||
buildToolsVersion androidBuildToolsVersion
|
||||
ndkVersion androidNdkVersion
|
||||
|
||||
// Extract native libraries from the APK
|
||||
packagingOptions.jniLibs.useLegacyPackaging true
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "3g"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
|
||||
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
|
||||
res.srcDirs = [qtAndroidDir + '/res', 'res']
|
||||
resources.srcDirs = ['resources']
|
||||
renderscript.srcDirs = ['src']
|
||||
assets.srcDirs = ['assets']
|
||||
jniLibs.srcDirs = ['libs']
|
||||
androidTest.assets.srcDirs += files("${qtAndroidDir}/schemas".toString())
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.incremental = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
// Do not compress Qt binary resources file
|
||||
aaptOptions {
|
||||
noCompress 'rcc'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
resConfig "en"
|
||||
minSdkVersion = 24
|
||||
targetSdkVersion = 34
|
||||
versionCode 39 // Change to a higher number
|
||||
versionName "4.1.0" // Change to a higher number
|
||||
|
||||
javaCompileOptions.annotationProcessorOptions.arguments = [
|
||||
"room.schemaLocation": "${qtAndroidDir}/schemas".toString()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// android.bundle.enableUncompressedNativeLibs is deprecated
|
||||
// disable adding gradle property android.bundle.enableUncompressedNativeLibs by androiddeployqt
|
||||
useLegacyPackaging
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
id("property-delegate")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
// get values from gradle or local properties
|
||||
val qtTargetSdkVersion: String by gradleProperties
|
||||
val qtTargetAbiList: String by gradleProperties
|
||||
val outputBaseName: String by gradleProperties
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn"
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
androidResources {
|
||||
// don't compress Qt binary resources file
|
||||
noCompress += "rcc"
|
||||
}
|
||||
|
||||
packaging {
|
||||
// compress .so binary libraries
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.amnezia.vpn"
|
||||
targetSdk = qtTargetSdkVersion.toInt()
|
||||
|
||||
// keeps language resources for only the locales specified below
|
||||
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
res.setSrcDirs(listOf("res"))
|
||||
// androyddeployqt creates the folders below
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
storeFile = providers.environmentVariable("ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
storePassword = providers.environmentVariable("ANDROID_KEYSTORE_KEY_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("ANDROID_KEYSTORE_KEY_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("ANDROID_KEYSTORE_KEY_PASS").orNull
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// exclude coroutine debug resource from release build
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*qtTargetAbiList.split(',').toTypedArray())
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
// fix for Qt Creator to allow deploying the application to a device
|
||||
// to enable this fix, add the line outputBaseName=android-build to local.properties
|
||||
if (outputBaseName.isNotEmpty()) {
|
||||
applicationVariants.all {
|
||||
outputs.map { it as BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.outputFileName.endsWith(".apk")) {
|
||||
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += "InvalidFragmentVersionForActivityResult"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
|
||||
implementation(project(":qt"))
|
||||
implementation(project(":utils"))
|
||||
implementation(project(":protocolApi"))
|
||||
implementation(project(":wireguard"))
|
||||
implementation(project(":awg"))
|
||||
implementation(project(":openvpn"))
|
||||
implementation(project(":cloak"))
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.bundles.androidx.camera)
|
||||
implementation(libs.google.mlkit)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.protocol.cloak"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
compileOnly(project(":protocolApi"))
|
||||
implementation(project(":openvpn"))
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.amnezia.vpn.protocol.cloak
|
||||
|
||||
import android.util.Base64
|
||||
import net.openvpn.ovpn3.ClientAPI_Config
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpn
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpnConfig
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
import org.amnezia.vpn.util.net.parseInetAddress
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Config Example:
|
||||
* {
|
||||
* "protocol": "cloak",
|
||||
* "description": "Server 1",
|
||||
* "dns1": "1.1.1.1",
|
||||
* "dns2": "1.0.0.1",
|
||||
* "hostName": "100.100.100.0",
|
||||
* "splitTunnelSites": [
|
||||
* ],
|
||||
* "splitTunnelType": 0,
|
||||
* "openvpn_config_data": {
|
||||
* "config": "openVpnConfig"
|
||||
* }
|
||||
* "cloak_config_data": {
|
||||
* "BrowserSig": "chrome",
|
||||
* "EncryptionMethod": "aes-gcm",
|
||||
* "NumConn": 1,
|
||||
* "ProxyMethod": "openvpn",
|
||||
* "PublicKey": "PublicKey=",
|
||||
* "RemoteHost": "100.100.100.0",
|
||||
* "RemotePort": "443",
|
||||
* "ServerName": "servername",
|
||||
* "StreamTimeout": 300,
|
||||
* "Transport": "direct",
|
||||
* "UID": "UID="
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
class Cloak : OpenVpn() {
|
||||
|
||||
override fun parseConfig(config: JSONObject): ClientAPI_Config {
|
||||
val openVpnConfig = ClientAPI_Config()
|
||||
|
||||
val openVpnConfigStr = config.getJSONObject("openvpn_config_data").getString("config")
|
||||
val cloakConfigJson = checkCloakJson(config.getJSONObject("cloak_config_data"))
|
||||
val cloakConfigStr = Base64.encodeToString(cloakConfigJson.toString().toByteArray(), Base64.DEFAULT)
|
||||
|
||||
val configStr = "$openVpnConfigStr\n<cloak>\n$cloakConfigStr\n</cloak>\n"
|
||||
|
||||
openVpnConfig.usePluggableTransports = true
|
||||
openVpnConfig.content = configStr
|
||||
return openVpnConfig
|
||||
}
|
||||
|
||||
override fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) {
|
||||
// exclude remote server ip from vpn routes
|
||||
val remoteServer = config.getString("hostName")
|
||||
val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer))
|
||||
configBuilder.excludeRoute(remoteServerAddress)
|
||||
}
|
||||
|
||||
private fun checkCloakJson(cloakConfigJson: JSONObject): JSONObject {
|
||||
cloakConfigJson.put("NumConn", 1)
|
||||
cloakConfigJson.put("ProxyMethod", "openvpn")
|
||||
if (cloakConfigJson.has("port")) {
|
||||
val port = cloakConfigJson["port"]
|
||||
cloakConfigJson.remove("port")
|
||||
cloakConfigJson.put("RemotePort", port)
|
||||
}
|
||||
if (cloakConfigJson.has("remote")) {
|
||||
val remote = cloakConfigJson["remote"]
|
||||
cloakConfigJson.remove("remote")
|
||||
cloakConfigJson.put("RemoteHost", remote)
|
||||
}
|
||||
return cloakConfigJson
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,27 @@
|
||||
# Specifies the JVM arguments used for the daemon process
|
||||
org.gradle.jvmargs=-Xms512m -Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError \
|
||||
-Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configureondemand=true
|
||||
# Project-wide Gradle settings.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# Gradle caching allows reusing the build artifacts from a previous
|
||||
# build with the same inputs. However, over time, the cache size will
|
||||
# grow. Uncomment the following line to enable it.
|
||||
#org.gradle.caching=true
|
||||
|
||||
# Use AndroidX library instead of a Support Library
|
||||
android.useAndroidX=true
|
||||
# Disable adding android:testOnly attribute to the manifest
|
||||
android.injected.testOnly=false
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
|
||||
# Disable providing custom values to resources from buildscript by default
|
||||
android.defaults.buildfeatures.resvalues=false
|
||||
# Disable compileShaders tasks by default
|
||||
android.defaults.buildfeatures.shaders=false
|
||||
# Disable Android resource processing for libraries by default
|
||||
android.library.defaults.buildfeatures.androidresources=false
|
||||
|
||||
# Qt variables
|
||||
# At build time androiddeployqt replaces these values with:
|
||||
# androidCompileSdkVersion - androiddeployqt --android-platform parameter
|
||||
# androidBuildToolsVersion - QT_ANDROID_SDK_BUILD_TOOLS_REVISION cmake target parameter
|
||||
# qtMinSdkVersion - QT_ANDROID_MIN_SDK_VERSION cmake target parameter
|
||||
# qtTargetSdkVersion - QT_ANDROID_TARGET_SDK_VERSION cmake target parameter
|
||||
# androidNdkVersion - version from ANDROID_NDK_ROOT environment variable
|
||||
# qtTargetAbiList - qt-cmake QT_ANDROID_ABIS parameter
|
||||
# qtAndroidDir - path to qt binding java source code
|
||||
# buildDir - hardcoded "build" value in androiddeployqt
|
||||
|
||||
# For development copy and set local values for these parameters in local.properties
|
||||
#androidCompileSdkVersion=android-34
|
||||
#androidBuildToolsVersion=34.0.0
|
||||
#qtMinSdkVersion=24
|
||||
#qtTargetSdkVersion=34
|
||||
#androidNdkVersion=26.1.10909125
|
||||
#qtTargetAbiList=x86_64
|
||||
#qtAndroidDir=/QT_BASE/android_ABI/src/android/java
|
||||
#buildDir=build
|
||||
|
||||
# Note about qtAndroidDir:
|
||||
# Some IDEs (for example, IntelliJ IDEA) may index all data from a common root of the project and qtAndroidDir.
|
||||
# Therefore, it's recommended to copy qt android files to a directory inside the project
|
||||
# and specify the path to that directory in qtAndroidDir.
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
androidBuildToolsVersion=30.0.2
|
||||
androidCompileSdkVersion=30
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
android.enableJetifier=true
|
||||
android.injected.testOnly=false
|
||||
kapt.use.worker.api=false
|
||||
kapt.incremental.apt=false
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
[versions]
|
||||
agp = "8.2.0"
|
||||
kotlin = "1.9.20"
|
||||
androidx-core = "1.12.0"
|
||||
androidx-activity = "1.8.1"
|
||||
androidx-annotation = "1.7.0"
|
||||
androidx-camera = "1.3.0"
|
||||
androidx-security-crypto = "1.1.0-alpha06"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
google-mlkit = "17.2.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
|
||||
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
|
||||
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-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
|
||||
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
|
||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
|
||||
|
||||
[bundles]
|
||||
androidx-camera = [
|
||||
"androidx-camera-core",
|
||||
"androidx-camera-lifecycle",
|
||||
"androidx-camera-view",
|
||||
"androidx-camera-camera2"
|
||||
]
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
@@ -1,25 +0,0 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("settingsGradlePropertyDelegate") {
|
||||
id = "settings-property-delegate"
|
||||
implementationClass = "SettingsPropertyDelegate"
|
||||
}
|
||||
|
||||
register("projectGradlePropertyDelegate") {
|
||||
id = "property-delegate"
|
||||
implementationClass = "ProjectPropertyDelegate"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.util.Properties
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.initialization.Settings
|
||||
import org.gradle.api.provider.ProviderFactory
|
||||
|
||||
private fun localProperties(rootDir: File) = Properties().apply {
|
||||
val localProperties = File(rootDir, "local.properties")
|
||||
if (localProperties.isFile) {
|
||||
InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use {
|
||||
load(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PropertyDelegate(
|
||||
rootDir: File,
|
||||
private val providers: ProviderFactory,
|
||||
private val localProperties: Properties = localProperties(rootDir)
|
||||
) : ReadOnlyProperty<Any?, String> {
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): String =
|
||||
providers.gradleProperty(property.name).orNull ?: localProperties.getProperty(property.name).orEmpty()
|
||||
}
|
||||
|
||||
private lateinit var settingsPropertyDelegate: ReadOnlyProperty<Any?, String>
|
||||
private lateinit var projectPropertyDelegate: ReadOnlyProperty<Any?, String>
|
||||
|
||||
class SettingsPropertyDelegate : Plugin<Settings> {
|
||||
override fun apply(settings: Settings) {
|
||||
settingsPropertyDelegate = PropertyDelegate(settings.rootDir, settings.providers)
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectPropertyDelegate : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
projectPropertyDelegate = PropertyDelegate(project.rootDir, project.providers)
|
||||
}
|
||||
}
|
||||
|
||||
val Settings.gradleProperties: ReadOnlyProperty<Any?, String>
|
||||
get() = settingsPropertyDelegate
|
||||
|
||||
val Project.gradleProperties: ReadOnlyProperty<Any?, String>
|
||||
get() = projectPropertyDelegate
|
||||
BIN
client/android/gradle/wrapper/gradle-wrapper.jar
vendored
@@ -1,7 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
307
client/android/gradlew
vendored
@@ -1,127 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -130,120 +81,92 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
56
client/android/gradlew.bat
vendored
@@ -1,20 +1,4 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,23 +9,19 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -55,7 +35,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -65,26 +45,38 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.protocol.openvpn"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
compileOnly(project(":protocolApi"))
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package org.amnezia.vpn.protocol.openvpn
|
||||
|
||||
import android.content.Context
|
||||
import android.net.VpnService.Builder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openvpn.ovpn3.ClientAPI_Config
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
import org.amnezia.vpn.util.net.getLocalNetworks
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Config Example:
|
||||
* {
|
||||
* "protocol": "openvpn",
|
||||
* "description": "Server 1",
|
||||
* "dns1": "1.1.1.1",
|
||||
* "dns2": "1.0.0.1",
|
||||
* "hostName": "100.100.100.0",
|
||||
* "splitTunnelSites": [
|
||||
* ],
|
||||
* "splitTunnelType": 0,
|
||||
* "openvpn_config_data": {
|
||||
* "config": "openVpnConfig"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
open class OpenVpn : Protocol() {
|
||||
|
||||
private lateinit var context: Context
|
||||
private var openVpnClient: OpenVpnClient? = null
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
override val statistics: Statistics
|
||||
get() {
|
||||
openVpnClient?.let { client ->
|
||||
val stats = client.transport_stats()
|
||||
return Statistics.build {
|
||||
setRxBytes(stats.bytesIn)
|
||||
setTxBytes(stats.bytesOut)
|
||||
}
|
||||
}
|
||||
return Statistics.EMPTY_STATISTICS
|
||||
}
|
||||
|
||||
override fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
|
||||
super.initialize(context, state, onError)
|
||||
loadSharedLibrary(context, "ovpn3")
|
||||
this.context = context
|
||||
scope = CoroutineScope(Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
val configBuilder = OpenVpnConfig.Builder()
|
||||
|
||||
openVpnClient = OpenVpnClient(
|
||||
configBuilder = configBuilder,
|
||||
state = state,
|
||||
getLocalNetworks = { ipv6 -> getLocalNetworks(context, ipv6) },
|
||||
establish = makeEstablish(vpnBuilder),
|
||||
protect = protect,
|
||||
onError = onError
|
||||
)
|
||||
|
||||
try {
|
||||
openVpnClient?.let { client ->
|
||||
val openVpnConfig = parseConfig(config)
|
||||
val evalConfig = client.eval_config(openVpnConfig)
|
||||
if (evalConfig.error) {
|
||||
throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}")
|
||||
}
|
||||
configPluggableTransport(configBuilder, config)
|
||||
configBuilder.configSplitTunneling(config)
|
||||
|
||||
scope.launch {
|
||||
val status = client.connect()
|
||||
if (status.error) {
|
||||
state.value = DISCONNECTED
|
||||
onError("OpenVpn connect() error: ${status.status}: ${status.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
openVpnClient = null
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopVpn() {
|
||||
openVpnClient?.stop()
|
||||
openVpnClient = null
|
||||
}
|
||||
|
||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
||||
openVpnClient?.let {
|
||||
it.establish = makeEstablish(vpnBuilder)
|
||||
it.reconnect(0)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun parseConfig(config: JSONObject): ClientAPI_Config {
|
||||
val openVpnConfig = ClientAPI_Config()
|
||||
openVpnConfig.content = config.getJSONObject("openvpn_config_data").getString("config")
|
||||
return openVpnConfig
|
||||
}
|
||||
|
||||
protected open fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) {}
|
||||
|
||||
private fun makeEstablish(vpnBuilder: Builder): (OpenVpnConfig.Builder) -> Int = { configBuilder ->
|
||||
val openVpnConfig = configBuilder.build()
|
||||
buildVpnInterface(openVpnConfig, vpnBuilder)
|
||||
|
||||
vpnBuilder.establish().use { tunFd ->
|
||||
if (tunFd == null) {
|
||||
throw VpnStartException("Create VPN interface: permission not granted or revoked")
|
||||
}
|
||||
return@use tunFd.detachFd()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
package org.amnezia.vpn.protocol.openvpn
|
||||
|
||||
import android.net.ProxyInfo
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import net.openvpn.ovpn3.ClientAPI_Config
|
||||
import net.openvpn.ovpn3.ClientAPI_EvalConfig
|
||||
import net.openvpn.ovpn3.ClientAPI_Event
|
||||
import net.openvpn.ovpn3.ClientAPI_LogInfo
|
||||
import net.openvpn.ovpn3.ClientAPI_OpenVPNClient
|
||||
import net.openvpn.ovpn3.ClientAPI_Status
|
||||
import net.openvpn.ovpn3.ClientAPI_StringVec
|
||||
import net.openvpn.ovpn3.ClientAPI_TransportStats
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
import org.amnezia.vpn.util.net.parseInetAddress
|
||||
|
||||
private const val TAG = "OpenVpnClient"
|
||||
private const val EMULATED_EXCLUDE_ROUTES = (1 shl 16)
|
||||
|
||||
class OpenVpnClient(
|
||||
private val configBuilder: OpenVpnConfig.Builder,
|
||||
private val state: MutableStateFlow<ProtocolState>,
|
||||
private val getLocalNetworks: (Boolean) -> List<InetNetwork>,
|
||||
internal var establish: (OpenVpnConfig.Builder) -> Int,
|
||||
private val protect: (Int) -> Boolean,
|
||||
private val onError: (String) -> Unit
|
||||
) : ClientAPI_OpenVPNClient() {
|
||||
|
||||
/**************************************************************************
|
||||
* Tun builder callbacks
|
||||
**************************************************************************/
|
||||
|
||||
// Tun builder methods, loosely based on the Android VpnService.Builder
|
||||
// abstraction. These methods comprise an abstraction layer that
|
||||
// allows the OpenVPN C++ core to call out to external methods for
|
||||
// establishing the tunnel, adding routes, etc.
|
||||
|
||||
// All methods returning bool use the return
|
||||
// value to indicate success (true) or fail (false).
|
||||
// tun_builder_new() should be called first, then arbitrary setter methods,
|
||||
// and finally tun_builder_establish to return the socket descriptor
|
||||
// for the session. IP addresses are pre-validated before being passed to
|
||||
// these methods.
|
||||
// This interface is based on Android's VpnService.Builder.
|
||||
|
||||
// Callback to construct a new tun builder
|
||||
// Should be called first.
|
||||
override fun tun_builder_new(): Boolean {
|
||||
Log.d(TAG, "tun_builder_new")
|
||||
configBuilder.clearAddresses()
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to set MTU of the VPN interface
|
||||
// Never called more than once per tun_builder session.
|
||||
override fun tun_builder_set_mtu(mtu: Int): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_mtu: $mtu")
|
||||
configBuilder.setMtu(mtu)
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to add network address to VPN interface
|
||||
// May be called more than once per tun_builder session
|
||||
override fun tun_builder_add_address(
|
||||
address: String, prefix_length: Int,
|
||||
gateway: String, ipv6: Boolean, net30: Boolean
|
||||
): Boolean {
|
||||
Log.d(TAG, "tun_builder_add_address: $address, $prefix_length, $gateway, $ipv6, $net30")
|
||||
configBuilder.addAddress(InetNetwork(address, prefix_length))
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to add route to VPN interface
|
||||
// May be called more than once per tun_builder session
|
||||
// metric is optional and should be ignored if < 0
|
||||
override fun tun_builder_add_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean {
|
||||
Log.d(TAG, "tun_builder_add_route: $address, $prefix_length, $metric, $ipv6")
|
||||
if (address == "remote_host") return false
|
||||
configBuilder.addRoute(InetNetwork(address, prefix_length))
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to exclude route from VPN interface
|
||||
// May be called more than once per tun_builder session
|
||||
// metric is optional and should be ignored if < 0
|
||||
override fun tun_builder_exclude_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean {
|
||||
Log.d(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6")
|
||||
configBuilder.excludeRoute(InetNetwork(address, prefix_length))
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to add DNS server to VPN interface
|
||||
// May be called more than once per tun_builder session
|
||||
// If reroute_dns is true, all DNS traffic should be routed over the
|
||||
// tunnel, while if false, only DNS traffic that matches an added search
|
||||
// domain should be routed.
|
||||
// Guaranteed to be called after tun_builder_reroute_gw.
|
||||
override fun tun_builder_add_dns_server(address: String, ipv6: Boolean): Boolean {
|
||||
Log.d(TAG, "tun_builder_add_dns_server: $address, $ipv6")
|
||||
configBuilder.addDnsServer(parseInetAddress(address))
|
||||
return true
|
||||
}
|
||||
|
||||
// Optional callback that indicates whether traffic of a certain
|
||||
// address family (AF_INET or AF_INET6) should be
|
||||
// blocked or allowed, to prevent unencrypted packet leakage when
|
||||
// the tunnel is IPv4-only/IPv6-only, but the local machine
|
||||
// has connectivity with the other protocol to the internet.
|
||||
// Controlled by "block-ipv6" and block-ipv6 config var.
|
||||
// If addresses are added for a family this setting should be
|
||||
// ignored for that family
|
||||
// See also Android's VPNService.Builder.allowFamily method
|
||||
/* override fun tun_builder_set_allow_family(af: Int, allow: Boolean): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_allow_family: $af, $allow")
|
||||
return true
|
||||
} */
|
||||
|
||||
// Callback to set address of remote server
|
||||
// Never called more than once per tun_builder session.
|
||||
override fun tun_builder_set_remote_address(address: String, ipv6: Boolean): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_remote_address: $address, $ipv6")
|
||||
return true
|
||||
}
|
||||
|
||||
// Optional callback that indicates OSI layer, should be 2 or 3.
|
||||
// Defaults to 3.
|
||||
override fun tun_builder_set_layer(layer: Int): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_layer: $layer")
|
||||
return layer == 3
|
||||
}
|
||||
|
||||
// Callback to set the session name
|
||||
// Never called more than once per tun_builder session.
|
||||
override fun tun_builder_set_session_name(name: String): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_session_name: $name")
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to establish the VPN tunnel, returning a file descriptor
|
||||
// to the tunnel, which the caller will henceforth own. Returns -1
|
||||
// if the tunnel could not be established.
|
||||
// Always called last after tun_builder session has been configured.
|
||||
override fun tun_builder_establish(): Int {
|
||||
Log.d(TAG, "tun_builder_establish")
|
||||
return establish(configBuilder)
|
||||
}
|
||||
|
||||
// Callback to reroute default gateway to VPN interface.
|
||||
// ipv4 is true if the default route to be added should be IPv4.
|
||||
// ipv6 is true if the default route to be added should be IPv6.
|
||||
// flags are defined in RGWFlags (rgwflags.hpp).
|
||||
// Never called more than once per tun_builder session.
|
||||
override fun tun_builder_reroute_gw(ipv4: Boolean, ipv6: Boolean, flags: Long): Boolean {
|
||||
Log.d(TAG, "tun_builder_reroute_gw: $ipv4, $ipv6, $flags")
|
||||
if ((flags and EMULATED_EXCLUDE_ROUTES.toLong()) != 0L) return true
|
||||
if (ipv4) {
|
||||
configBuilder.addRoute(InetNetwork("0.0.0.0", 0))
|
||||
}
|
||||
if (ipv6) {
|
||||
configBuilder.addRoute(InetNetwork("::", 0))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to add search domain to DNS resolver
|
||||
// May be called more than once per tun_builder session
|
||||
// See tun_builder_add_dns_server above for description of
|
||||
// reroute_dns parameter.
|
||||
// Guaranteed to be called after tun_builder_reroute_gw.
|
||||
override fun tun_builder_add_search_domain(domain: String): Boolean {
|
||||
Log.d(TAG, "tun_builder_add_search_domain: $domain")
|
||||
configBuilder.setSearchDomain(domain)
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to set the HTTP proxy
|
||||
// Never called more than once per tun_builder session.
|
||||
override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_proxy_http: $host, $port")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
try {
|
||||
configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not set proxy: ${e.message}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Callback to set the HTTPS proxy
|
||||
// Never called more than once per tun_builder session.
|
||||
override fun tun_builder_set_proxy_https(host: String, port: Int): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_proxy_https: $host, $port")
|
||||
return false
|
||||
}
|
||||
|
||||
// When the exclude local network option is enabled this
|
||||
// function is called to get a list of local networks so routes
|
||||
// to exclude them from the VPN network are generated
|
||||
// This should be a list of CIDR networks (e.g. 192.168.0.0/24)
|
||||
override fun tun_builder_get_local_networks(ipv6: Boolean): ClientAPI_StringVec {
|
||||
Log.d(TAG, "tun_builder_get_local_networks: $ipv6")
|
||||
val networks = ClientAPI_StringVec()
|
||||
for (address in getLocalNetworks(ipv6)) {
|
||||
networks.add(address.toString())
|
||||
}
|
||||
return networks
|
||||
}
|
||||
|
||||
// Optional callback to set default value for route metric.
|
||||
// Guaranteed to be called before other methods that deal
|
||||
// with routes such as tun_builder_add_route and
|
||||
// tun_builder_reroute_gw. Route metric is ignored
|
||||
// if < 0.
|
||||
/* override fun tun_builder_set_route_metric_default(metric: Int): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_route_metric_default: $metric")
|
||||
return super.tun_builder_set_route_metric_default(metric)
|
||||
} */
|
||||
|
||||
// Callback to add a host which should bypass the proxy
|
||||
// May be called more than once per tun_builder session
|
||||
/* override fun tun_builder_add_proxy_bypass(bypass_host: String): Boolean {
|
||||
Log.d(TAG, "tun_builder_add_proxy_bypass: $bypass_host")
|
||||
return super.tun_builder_add_proxy_bypass(bypass_host)
|
||||
} */
|
||||
|
||||
// Callback to set the proxy "Auto Config URL"
|
||||
// Never called more than once per tun_builder session.
|
||||
/* override fun tun_builder_set_proxy_auto_config_url(url: String): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_proxy_auto_config_url: $url")
|
||||
return super.tun_builder_set_proxy_auto_config_url(url)
|
||||
} */
|
||||
|
||||
// Callback to add Windows WINS server to VPN interface.
|
||||
// WINS server addresses are always IPv4.
|
||||
// May be called more than once per tun_builder session.
|
||||
// Guaranteed to be called after tun_builder_reroute_gw.
|
||||
/* override fun tun_builder_add_wins_server(address: String): Boolean {
|
||||
Log.d(TAG, "tun_builder_add_wins_server: $address")
|
||||
return super.tun_builder_add_wins_server(address)
|
||||
} */
|
||||
|
||||
// Optional callback to set a DNS suffix on tun/tap adapter.
|
||||
// Currently only implemented on Windows, where it will
|
||||
// set the "Connection-specific DNS Suffix" property on
|
||||
// the TAP driver.
|
||||
/* override fun tun_builder_set_adapter_domain_suffix(name: String): Boolean {
|
||||
Log.d(TAG, "tun_builder_set_adapter_domain_suffix: $name")
|
||||
return super.tun_builder_set_adapter_domain_suffix(name)
|
||||
} */
|
||||
|
||||
// Return true if tun interface may be persisted, i.e. rolled
|
||||
// into a new session with properties untouched. This method
|
||||
// is only called after all other tests of persistence
|
||||
// allowability succeed, therefore it can veto persistence.
|
||||
// If persistence is ultimately enabled,
|
||||
// tun_builder_establish_lite() will be called. Otherwise,
|
||||
// tun_builder_establish() will be called.
|
||||
/* override fun tun_builder_persist(): Boolean {
|
||||
Log.d(TAG, "tun_builder_persist")
|
||||
return super.tun_builder_persist()
|
||||
} */
|
||||
|
||||
// Indicates a reconnection with persisted tun state.
|
||||
/* override fun tun_builder_establish_lite() {
|
||||
Log.d(TAG, "tun_builder_establish_lite")
|
||||
super.tun_builder_establish_lite()
|
||||
} */
|
||||
|
||||
// Indicates that tunnel is being torn down.
|
||||
// If disconnect == true, then the teardown is occurring
|
||||
// prior to final disconnect.
|
||||
/* override fun tun_builder_teardown(disconnect: Boolean) {
|
||||
Log.d(TAG, "tun_builder_teardown: $disconnect")
|
||||
super.tun_builder_teardown(disconnect)
|
||||
} */
|
||||
|
||||
/**************************************************************************
|
||||
* Connection control methods
|
||||
**************************************************************************/
|
||||
|
||||
// Parse OpenVPN configuration file.
|
||||
override fun eval_config(arg0: ClientAPI_Config): ClientAPI_EvalConfig {
|
||||
Log.d(TAG, "eval_config")
|
||||
return super.eval_config(arg0)
|
||||
}
|
||||
|
||||
// Primary VPN client connect method, doesn't return until disconnect.
|
||||
// Should be called by a worker thread. This method will make callbacks
|
||||
// to event() and log() functions. Make sure to call eval_config()
|
||||
// and possibly provide_creds() as well before this function.
|
||||
override fun connect(): ClientAPI_Status {
|
||||
Log.d(TAG, "connect")
|
||||
return super.connect()
|
||||
}
|
||||
|
||||
// Callback to "protect" a socket from being routed through the tunnel.
|
||||
// Will be called from the thread executing connect().
|
||||
// The remote and ipv6 are the remote host this socket will connect to
|
||||
override fun socket_protect(socket: Int, remote: String, ipv6: Boolean): Boolean {
|
||||
Log.d(TAG, "socket_protect: $socket, $remote, $ipv6")
|
||||
return protect(socket)
|
||||
}
|
||||
|
||||
// Stop the client. Only meaningful when connect() is running.
|
||||
// May be called asynchronously from a different thread
|
||||
// when connect() is running.
|
||||
override fun stop() {
|
||||
Log.d(TAG, "stop")
|
||||
super.stop()
|
||||
}
|
||||
|
||||
// Pause the client -- useful to avoid continuous reconnection attempts
|
||||
// when network is down. May be called from a different thread
|
||||
// when connect() is running.
|
||||
override fun pause(reason: String) {
|
||||
Log.d(TAG, "pause: $reason")
|
||||
super.pause(reason)
|
||||
}
|
||||
|
||||
// Resume the client after it has been paused. May be called from a
|
||||
// different thread when connect() is running.
|
||||
override fun resume() {
|
||||
Log.d(TAG, "resume")
|
||||
super.resume()
|
||||
}
|
||||
|
||||
// Do a disconnect/reconnect cycle n seconds from now. May be called
|
||||
// from a different thread when connect() is running.
|
||||
override fun reconnect(seconds: Int) {
|
||||
Log.d(TAG, "reconnect: $seconds")
|
||||
super.reconnect(seconds)
|
||||
}
|
||||
|
||||
// When a connection is close to timeout, the core will call this
|
||||
// method. If it returns false, the core will disconnect with a
|
||||
// CONNECTION_TIMEOUT event. If true, the core will enter a PAUSE
|
||||
// state.
|
||||
override fun pause_on_connection_timeout(): Boolean {
|
||||
Log.d(TAG, "pause_on_connection_timeout")
|
||||
return false
|
||||
}
|
||||
|
||||
// Return information about the most recent connection. Should be called
|
||||
// after an event of type "CONNECTED".
|
||||
/* override fun connection_info(): ClientAPI_ConnectionInfo {
|
||||
Log.d(TAG, "connection_info")
|
||||
return super.connection_info()
|
||||
} */
|
||||
|
||||
/**************************************************************************
|
||||
* Status callbacks
|
||||
**************************************************************************/
|
||||
|
||||
// Callback for delivering events during connect() call.
|
||||
// Will be called from the thread executing connect().
|
||||
override fun event(event: ClientAPI_Event) {
|
||||
val name = event.name
|
||||
val info = event.info
|
||||
Log.d(TAG, "OpenVpn event: $name: $info")
|
||||
when (name) {
|
||||
"COMPRESSION_ENABLED", "WARN" -> Log.w(TAG, "$name: $info")
|
||||
"CONNECTED" -> state.value = CONNECTED
|
||||
"DISCONNECTED" -> state.value = DISCONNECTED
|
||||
"RECONNECTING" -> {
|
||||
state.getAndUpdate { state ->
|
||||
if (state == DISCONNECTED || state == CONNECTED) RECONNECTING
|
||||
else state
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.error || event.fatal) {
|
||||
state.value = DISCONNECTED
|
||||
onError("OpenVpn ${if (event.error) "ERROR" else "FATAL"}: $name: $info")
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for logging.
|
||||
// Will be called from the thread executing connect().
|
||||
override fun log(arg0: ClientAPI_LogInfo) {
|
||||
arg0.text.dropLastWhile { it == '\n' }.let {
|
||||
Log.d(TAG, "OpenVpnLog: $it")
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* Stats methods
|
||||
**************************************************************************/
|
||||
|
||||
// return transport stats only
|
||||
override fun transport_stats(): ClientAPI_TransportStats {
|
||||
Log.d(TAG, "transport_stats")
|
||||
return super.transport_stats()
|
||||
}
|
||||
|
||||
// return a stats value, index should be >= 0 and < stats_n()
|
||||
/* override fun stats_value(index: Int): Long {
|
||||
Log.d(TAG, "stats_value: $index")
|
||||
return super.stats_value(index)
|
||||
} */
|
||||
|
||||
// return all stats in a bundle
|
||||
/* override fun stats_bundle(): ClientAPI_LLVector {
|
||||
Log.d(TAG, "stats_bundle")
|
||||
return super.stats_bundle()
|
||||
} */
|
||||
|
||||
// return tun stats only
|
||||
/* override fun tun_stats(): ClientAPI_InterfaceStats {
|
||||
Log.d(TAG, "tun_stats")
|
||||
return super.tun_stats()
|
||||
} */
|
||||
|
||||
// post control channel message
|
||||
/* override fun post_cc_msg(msg: String) {
|
||||
Log.d(TAG, "post_cc_msg: $msg")
|
||||
super.post_cc_msg(msg)
|
||||
} */
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.amnezia.vpn.protocol.openvpn
|
||||
|
||||
import org.amnezia.vpn.protocol.ProtocolConfig
|
||||
|
||||
private const val OPENVPN_DEFAULT_MTU = 1500
|
||||
|
||||
class OpenVpnConfig private constructor(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
class Builder : ProtocolConfig.Builder(false) {
|
||||
override var mtu: Int = OPENVPN_DEFAULT_MTU
|
||||
|
||||
override fun build(): OpenVpnConfig = configBuild().run { OpenVpnConfig(this@Builder) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
inline fun build(block: Builder.() -> Unit): OpenVpnConfig = Builder().apply(block).build()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.protocol"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
sealed class ProtocolException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
||||
class LoadLibraryException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||
class BadConfigException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||
|
||||
class VpnStartException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||
class VpnException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||
@@ -1,191 +0,0 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.IpPrefix
|
||||
import android.net.VpnService
|
||||
import android.net.VpnService.Builder
|
||||
import android.os.Build
|
||||
import android.system.OsConstants
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val TAG = "Protocol"
|
||||
|
||||
const val VPN_SESSION_NAME = "AmneziaVPN"
|
||||
|
||||
private const val SPLIT_TUNNEL_DISABLE = 0
|
||||
private const val SPLIT_TUNNEL_INCLUDE = 1
|
||||
private const val SPLIT_TUNNEL_EXCLUDE = 2
|
||||
|
||||
abstract class Protocol {
|
||||
|
||||
abstract val statistics: Statistics
|
||||
protected lateinit var state: MutableStateFlow<ProtocolState>
|
||||
protected lateinit var onError: (String) -> Unit
|
||||
|
||||
open fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
|
||||
this.state = state
|
||||
this.onError = onError
|
||||
}
|
||||
|
||||
abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean)
|
||||
|
||||
abstract fun stopVpn()
|
||||
|
||||
abstract fun reconnectVpn(vpnBuilder: Builder)
|
||||
|
||||
protected fun ProtocolConfig.Builder.configSplitTunneling(config: JSONObject) {
|
||||
if (!allowSplitTunneling) {
|
||||
Log.i(TAG, "Global address split tunneling is prohibited, " +
|
||||
"only tunneling from the protocol config is used")
|
||||
return
|
||||
}
|
||||
|
||||
val splitTunnelType = config.optInt("splitTunnelType")
|
||||
if (splitTunnelType == SPLIT_TUNNEL_DISABLE) return
|
||||
val splitTunnelSites = config.getJSONArray("splitTunnelSites")
|
||||
val addressHandlerFunc = when (splitTunnelType) {
|
||||
SPLIT_TUNNEL_INCLUDE -> ::includeAddress
|
||||
SPLIT_TUNNEL_EXCLUDE -> ::excludeAddress
|
||||
|
||||
else -> throw BadConfigException("Unexpected value of the 'splitTunnelType' parameter: $splitTunnelType")
|
||||
}
|
||||
|
||||
for (i in 0 until splitTunnelSites.length()) {
|
||||
val address = InetNetwork.parse(splitTunnelSites.getString(i))
|
||||
addressHandlerFunc(address)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) {
|
||||
vpnBuilder.setSession(VPN_SESSION_NAME)
|
||||
|
||||
for (addr in config.addresses) {
|
||||
Log.d(TAG, "addAddress: $addr")
|
||||
vpnBuilder.addAddress(addr)
|
||||
}
|
||||
|
||||
for (addr in config.dnsServers) {
|
||||
Log.d(TAG, "addDnsServer: $addr")
|
||||
vpnBuilder.addDnsServer(addr)
|
||||
}
|
||||
// fix for Samsung android ignoring DNS servers outside the VPN route range
|
||||
if (Build.BRAND == "samsung") {
|
||||
for (addr in config.dnsServers) {
|
||||
Log.d(TAG, "addRoute: $addr")
|
||||
vpnBuilder.addRoute(InetNetwork(addr))
|
||||
}
|
||||
}
|
||||
|
||||
config.searchDomain?.let {
|
||||
Log.d(TAG, "addSearchDomain: $it")
|
||||
vpnBuilder.addSearchDomain(it)
|
||||
}
|
||||
|
||||
for (addr in config.routes) {
|
||||
Log.d(TAG, "addRoute: $addr")
|
||||
vpnBuilder.addRoute(addr)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
for (addr in config.excludedRoutes) {
|
||||
Log.d(TAG, "excludeRoute: $addr")
|
||||
vpnBuilder.excludeRoute(addr)
|
||||
}
|
||||
}
|
||||
|
||||
for (app in config.excludedApplications) {
|
||||
Log.d(TAG, "addDisallowedApplication: $app")
|
||||
vpnBuilder.addDisallowedApplication(app)
|
||||
}
|
||||
|
||||
Log.d(TAG, "setMtu: ${config.mtu}")
|
||||
vpnBuilder.setMtu(config.mtu)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
config.httpProxy?.let {
|
||||
Log.d(TAG, "setHttpProxy: $it")
|
||||
vpnBuilder.setHttpProxy(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (config.allowAllAF) {
|
||||
Log.d(TAG, "allowFamily")
|
||||
vpnBuilder.allowFamily(OsConstants.AF_INET)
|
||||
vpnBuilder.allowFamily(OsConstants.AF_INET6)
|
||||
}
|
||||
|
||||
Log.d(TAG, "setBlocking: ${config.blockingMode}")
|
||||
vpnBuilder.setBlocking(config.blockingMode)
|
||||
vpnBuilder.setUnderlyingNetworks(null)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
vpnBuilder.setMetered(false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean {
|
||||
Log.d(TAG, "Extracting library: $libraryName")
|
||||
val apks = hashSetOf<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.addRoute(addr: InetNetwork) = addRoute(addr.address, addr.mask)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private fun VpnService.Builder.excludeRoute(addr: InetNetwork) = excludeRoute(IpPrefix(addr.address, addr.mask))
|
||||
@@ -1,172 +0,0 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
import android.net.ProxyInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.net.InetAddress
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
import org.amnezia.vpn.util.net.IpRange
|
||||
import org.amnezia.vpn.util.net.IpRangeSet
|
||||
|
||||
open class ProtocolConfig protected constructor(
|
||||
val addresses: Set<InetNetwork>,
|
||||
val dnsServers: Set<InetAddress>,
|
||||
val searchDomain: String?,
|
||||
val routes: Set<InetNetwork>,
|
||||
val excludedRoutes: Set<InetNetwork>,
|
||||
val includedAddresses: Set<InetNetwork>,
|
||||
val excludedAddresses: Set<InetNetwork>,
|
||||
val excludedApplications: Set<String>,
|
||||
val httpProxy: ProxyInfo?,
|
||||
val allowAllAF: Boolean,
|
||||
val blockingMode: Boolean,
|
||||
val mtu: Int
|
||||
) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
builder.addresses,
|
||||
builder.dnsServers,
|
||||
builder.searchDomain,
|
||||
builder.routes,
|
||||
builder.excludedRoutes,
|
||||
builder.includedAddresses,
|
||||
builder.excludedAddresses,
|
||||
builder.excludedApplications,
|
||||
builder.httpProxy,
|
||||
builder.allowAllAF,
|
||||
builder.blockingMode,
|
||||
builder.mtu
|
||||
)
|
||||
|
||||
open class Builder(blockingMode: Boolean) {
|
||||
internal val addresses: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val dnsServers: MutableSet<InetAddress> = hashSetOf()
|
||||
internal val routes: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
|
||||
internal val excludedApplications: MutableSet<String> = hashSetOf()
|
||||
|
||||
internal var searchDomain: String? = null
|
||||
private set
|
||||
|
||||
internal var httpProxy: ProxyInfo? = null
|
||||
private set
|
||||
|
||||
internal var allowAllAF: Boolean = false
|
||||
private set
|
||||
|
||||
internal var blockingMode: Boolean = blockingMode
|
||||
private set
|
||||
|
||||
internal var allowSplitTunneling: Boolean = true
|
||||
private set
|
||||
|
||||
open var mtu: Int = 0
|
||||
protected set
|
||||
|
||||
fun addAddress(addr: InetNetwork) = apply { this.addresses += addr }
|
||||
fun addAddresses(addresses: Collection<InetNetwork>) = apply { this.addresses += addresses }
|
||||
fun clearAddresses() = apply { this.addresses.clear() }
|
||||
|
||||
fun addDnsServer(dnsServer: InetAddress) = apply { this.dnsServers += dnsServer }
|
||||
fun addDnsServers(dnsServers: Collection<InetAddress>) = apply { this.dnsServers += dnsServers }
|
||||
|
||||
fun setSearchDomain(domain: String) = apply { this.searchDomain = domain }
|
||||
|
||||
fun addRoute(route: InetNetwork) = apply { this.routes += route }
|
||||
fun addRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes }
|
||||
fun removeRoute(route: InetNetwork) = apply { this.routes.remove(route) }
|
||||
fun clearRoutes() = apply { this.routes.clear() }
|
||||
|
||||
fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route }
|
||||
fun excludeRoutes(routes: Collection<InetNetwork>) = apply { this.excludedRoutes += routes }
|
||||
|
||||
fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr }
|
||||
fun includeAddresses(addresses: Collection<InetNetwork>) = apply { this.includedAddresses += addresses }
|
||||
|
||||
fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr }
|
||||
fun excludeAddresses(addresses: Collection<InetNetwork>) = apply { this.excludedAddresses += addresses }
|
||||
|
||||
fun excludeApplication(application: String) = apply { this.excludedApplications += application }
|
||||
fun excludeApplications(applications: Collection<String>) = apply { this.excludedApplications += applications }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun setHttpProxy(httpProxy: ProxyInfo) = apply { this.httpProxy = httpProxy }
|
||||
|
||||
fun setAllowAllAF(allowAllAF: Boolean) = apply { this.allowAllAF = allowAllAF }
|
||||
|
||||
fun setBlockingMode(blockingMode: Boolean) = apply { this.blockingMode = blockingMode }
|
||||
|
||||
fun disableSplitTunneling() = apply { this.allowSplitTunneling = false }
|
||||
|
||||
fun setMtu(mtu: Int) = apply { this.mtu = mtu }
|
||||
|
||||
private fun processSplitTunneling() {
|
||||
if (includedAddresses.isNotEmpty() && excludedAddresses.isNotEmpty()) {
|
||||
throw BadConfigException("Config contains addresses for inclusive and exclusive split tunneling at the same time")
|
||||
}
|
||||
|
||||
if (includedAddresses.isNotEmpty()) {
|
||||
// remove default routes, if any
|
||||
removeRoute(InetNetwork("0.0.0.0", 0))
|
||||
removeRoute(InetNetwork("::", 0))
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
// for older versions of Android, add the default route to the excluded routes
|
||||
// to correctly build the excluded subnets list later
|
||||
excludeRoute(InetNetwork("0.0.0.0", 0))
|
||||
}
|
||||
addRoutes(includedAddresses)
|
||||
} else if (excludedAddresses.isNotEmpty()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// default routes are required for split tunneling in newer versions of Android
|
||||
addRoute(InetNetwork("0.0.0.0", 0))
|
||||
addRoute(InetNetwork("::", 0))
|
||||
}
|
||||
excludeRoutes(excludedAddresses)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processExcludedRoutes() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
// for older versions of Android, build a list of subnets without excluded routes
|
||||
// and add them to routes
|
||||
val ipRangeSet = IpRangeSet()
|
||||
ipRangeSet.remove(IpRange("127.0.0.0", 8))
|
||||
excludedRoutes.forEach {
|
||||
ipRangeSet.remove(IpRange(it))
|
||||
}
|
||||
// remove default routes, if any
|
||||
removeRoute(InetNetwork("0.0.0.0", 0))
|
||||
removeRoute(InetNetwork("::", 0))
|
||||
ipRangeSet.subnets().forEach(::addRoute)
|
||||
addRoute(InetNetwork("2000::", 3))
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate() {
|
||||
val errorMessage = StringBuilder()
|
||||
|
||||
with(errorMessage) {
|
||||
if (addresses.isEmpty()) appendLine("VPN interface network address not specified.")
|
||||
if (routes.isEmpty()) appendLine("VPN interface route not specified.")
|
||||
if (mtu == 0) appendLine("MTU not set.")
|
||||
}
|
||||
|
||||
if (errorMessage.isNotEmpty()) throw BadConfigException(errorMessage.toString())
|
||||
}
|
||||
|
||||
protected fun configBuild() {
|
||||
processSplitTunneling()
|
||||
processExcludedRoutes()
|
||||
validate()
|
||||
}
|
||||
|
||||
open fun build(): ProtocolConfig = configBuild().run { ProtocolConfig(this@Builder) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
inline fun build(blockingMode: Boolean, block: Builder.() -> Unit): ProtocolConfig =
|
||||
Builder(blockingMode).apply(block).build()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
// keep synchronized with client/platforms/android/android_controller.h ConnectionState
|
||||
enum class ProtocolState {
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
DISCONNECTING,
|
||||
RECONNECTING,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
private const val RX_BYTES_KEY = "rxBytes"
|
||||
private const val TX_BYTES_KEY = "txBytes"
|
||||
|
||||
@Suppress("DataClassPrivateConstructor")
|
||||
data class Statistics private constructor(
|
||||
val rxBytes: Long = 0L,
|
||||
val txBytes: Long = 0L
|
||||
) {
|
||||
|
||||
private constructor(builder: Builder) : this(builder.rxBytes, builder.txBytes)
|
||||
|
||||
@Suppress("SuspiciousEqualsCombination")
|
||||
fun isEmpty(): Boolean = this === EMPTY_STATISTICS || this == EMPTY_STATISTICS
|
||||
|
||||
class Builder {
|
||||
var rxBytes: Long = 0L
|
||||
private set
|
||||
|
||||
var txBytes: Long = 0L
|
||||
private set
|
||||
|
||||
fun setRxBytes(rxBytes: Long) = apply { this.rxBytes = rxBytes }
|
||||
fun setTxBytes(txBytes: Long) = apply { this.txBytes = txBytes }
|
||||
|
||||
fun build(): Statistics =
|
||||
if (rxBytes + txBytes == 0L) EMPTY_STATISTICS
|
||||
else Statistics(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY_STATISTICS: Statistics = Statistics()
|
||||
|
||||
inline fun build(block: Builder.() -> Unit): Statistics = Builder().apply(block).build()
|
||||
}
|
||||
}
|
||||
|
||||
fun Bundle.putStatistics(statistics: Statistics) {
|
||||
putLong(RX_BYTES_KEY, statistics.rxBytes)
|
||||
putLong(TX_BYTES_KEY, statistics.txBytes)
|
||||
}
|
||||
|
||||
fun Bundle.getStatistics(): Statistics =
|
||||
Statistics.build {
|
||||
setRxBytes(getLong(RX_BYTES_KEY))
|
||||
setTxBytes(getLong(TX_BYTES_KEY))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
private const val STATE_KEY = "state"
|
||||
|
||||
@Suppress("DataClassPrivateConstructor")
|
||||
data class Status private constructor(
|
||||
val state: ProtocolState
|
||||
) {
|
||||
private constructor(builder: Builder) : this(builder.state)
|
||||
|
||||
class Builder {
|
||||
lateinit var state: ProtocolState
|
||||
private set
|
||||
|
||||
fun setState(state: ProtocolState) = apply { this.state = state }
|
||||
|
||||
fun build(): Status = Status(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
inline fun build(block: Builder.() -> Unit): Status = Builder().apply(block).build()
|
||||
}
|
||||
}
|
||||
|
||||
fun Bundle.putStatus(status: Status) {
|
||||
putInt(STATE_KEY, status.state.ordinal)
|
||||
}
|
||||
|
||||
fun Bundle.getStatus(): Status =
|
||||
Status.build {
|
||||
setState(ProtocolState.entries[getInt(STATE_KEY)])
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id("property-delegate")
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
|
||||
val qtAndroidDir: String by gradleProperties
|
||||
|
||||
android {
|
||||
namespace = "org.qtproject.qt.android.binding"
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
java.setSrcDirs(listOf("$qtAndroidDir/src"))
|
||||
res.setSrcDirs(listOf("$qtAndroidDir/res"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar", "*.aar"))))
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
14
client/android/res/layout/activity_camera.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".CameraActivity">
|
||||
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/viewFinder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
5
client/android/res/layout/activity_import_config.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/viewFinder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="." />
|
||||
</full-backup-content>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="sharedpref" path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="sharedpref" path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
4
client/android/res/xml/fileprovider.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="/" />
|
||||
</paths>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path name="files_path" path="/"/>
|
||||
</paths>
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1000,
|
||||
"identityHash": "14b379f7776710b79b9d617090efe40e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Profile",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remotePort",
|
||||
"columnName": "remotePort",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "method",
|
||||
"columnName": "method",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteDns",
|
||||
"columnName": "remoteDns",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "udpdns",
|
||||
"columnName": "udpdns",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv6",
|
||||
"columnName": "ipv6",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tx",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rx",
|
||||
"columnName": "rx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userOrder",
|
||||
"columnName": "userOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "KeyValuePair",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "valueType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14b379f7776710b79b9d617090efe40e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "f1aab1fb633378621635c344dbc8ac7b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "KeyValuePair",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "valueType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f1aab1fb633378621635c344dbc8ac7b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
21
client/android/settings.gradle
Normal file
@@ -0,0 +1,21 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
include ':shadowsocks'
|
||||
@@ -1,60 +0,0 @@
|
||||
import com.android.build.api.dsl.SettingsExtension
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
includeBuild("./gradle/plugins")
|
||||
}
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
includeBuild("./gradle/plugins")
|
||||
|
||||
plugins {
|
||||
id("com.android.settings") version "8.2.0"
|
||||
id("settings-property-delegate")
|
||||
}
|
||||
|
||||
rootProject.name = "AmneziaVPN"
|
||||
rootProject.buildFileName = "build.gradle.kts"
|
||||
|
||||
include(":qt")
|
||||
include(":utils")
|
||||
include(":protocolApi")
|
||||
include(":wireguard")
|
||||
include(":awg")
|
||||
include(":openvpn")
|
||||
include(":cloak")
|
||||
|
||||
// get values from gradle or local properties
|
||||
val androidBuildToolsVersion: String by gradleProperties
|
||||
val androidCompileSdkVersion: String by gradleProperties
|
||||
val androidNdkVersion: String by gradleProperties
|
||||
val qtMinSdkVersion: String by gradleProperties
|
||||
|
||||
// set default values for all modules
|
||||
configure<SettingsExtension> {
|
||||
buildToolsVersion = androidBuildToolsVersion
|
||||
compileSdk = androidCompileSdkVersion.substringAfter('-').toInt()
|
||||
minSdk = qtMinSdkVersion.toInt()
|
||||
ndkVersion = androidNdkVersion
|
||||
}
|
||||
|
||||
// stop Gradle running by androiddeployqt
|
||||
gradle.taskGraph.whenReady {
|
||||
if (providers.environmentVariable("ANDROIDDEPLOYQT_RUN").isPresent
|
||||
&& !providers.systemProperty("explicitRun").isPresent) {
|
||||
allTasks.forEach { it.enabled = false }
|
||||
}
|
||||
}
|
||||
70
client/android/shadowsocks/build.gradle
Normal file
@@ -0,0 +1,70 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
//apply plugin: 'com.novoda.bintray-release'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
//def lifecycleVersion = '2.0.0'
|
||||
def roomVersion = "2.4.3"
|
||||
//def preferencexVersion = '1.0.0'
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30-M1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||
implementation "androidx.room:room-runtime:$roomVersion" // runtime
|
||||
implementation "androidx.preference:preference:1.1.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
implementation "androidx.browser:browser:1.3.0-alpha01"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation "com.google.android.material:material:1.2.0-alpha05"
|
||||
implementation "com.google.code.gson:gson:2.8.5"
|
||||
|
||||
implementation "dnsjava:dnsjava:2.1.9"
|
||||
implementation "com.github.kruton:jsocks:1.0.0"
|
||||
implementation "com.afollestad.material-dialogs:core:2.6.0"
|
||||
// api "com.takisoft.preferencex:preferencex:1.0.0"
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||
api 'com.github.kruton:jsocks:1.0.0'
|
||||
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:2.4.0"
|
||||
}
|
||||
12
client/android/shadowsocks/gfwlist/gen.pl
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env perl
|
||||
## ArchLinux install package via pacman: perl-net-cidr-lite
|
||||
use strict;
|
||||
use warnings;
|
||||
use Net::CIDR::Lite;
|
||||
my $cidr = Net::CIDR::Lite->new;
|
||||
while (my $line=<>) {
|
||||
$cidr->add($line);
|
||||
}
|
||||
foreach my $line( @{$cidr->list} ) {
|
||||
print "<item>$line</item>\n";
|
||||
}
|
||||
20
client/android/shadowsocks/gfwlist/gen.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/python
|
||||
# -*- encoding: utf8 -*-
|
||||
|
||||
import sys
|
||||
|
||||
import IPy
|
||||
|
||||
|
||||
def main():
|
||||
china_list_set = IPy.IPSet()
|
||||
for line in sys.stdin:
|
||||
china_list_set.add(IPy.IP(line))
|
||||
|
||||
# 输出结果
|
||||
for ip in china_list_set:
|
||||
print '<item>' + str(ip) + '</item>'
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
121
client/android/shadowsocks/gfwlist/parse.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pkgutil
|
||||
import urlparse
|
||||
import socket
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from datetime import date
|
||||
|
||||
__all__ = ['main']
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('-i', '--input', dest='input', required=True,
|
||||
help='path to gfwlist', metavar='GFWLIST')
|
||||
parser.add_argument('-f', '--file', dest='output', required=True,
|
||||
help='path to output acl', metavar='ACL')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def decode_gfwlist(content):
|
||||
# decode base64 if have to
|
||||
try:
|
||||
return content.decode('base64')
|
||||
except:
|
||||
return content
|
||||
|
||||
|
||||
def get_hostname(something):
|
||||
try:
|
||||
# quite enough for GFW
|
||||
if not something.startswith('http:'):
|
||||
something = 'http://' + something
|
||||
r = urlparse.urlparse(something)
|
||||
return r.hostname
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
|
||||
|
||||
def add_domain_to_set(s, something):
|
||||
hostname = get_hostname(something)
|
||||
if hostname is not None:
|
||||
if hostname.startswith('.'):
|
||||
hostname = hostname.lstrip('.')
|
||||
if hostname.endswith('/'):
|
||||
hostname = hostname.rstrip('/')
|
||||
if hostname:
|
||||
s.add(hostname)
|
||||
|
||||
|
||||
def parse_gfwlist(content):
|
||||
gfwlist = content.splitlines(False)
|
||||
domains = set()
|
||||
for line in gfwlist:
|
||||
if line.find('.*') >= 0:
|
||||
continue
|
||||
elif line.find('*') >= 0:
|
||||
line = line.replace('*', '/')
|
||||
if line.startswith('!'):
|
||||
continue
|
||||
elif line.startswith('['):
|
||||
continue
|
||||
elif line.startswith('@'):
|
||||
# ignore white list
|
||||
continue
|
||||
elif line.startswith('||'):
|
||||
add_domain_to_set(domains, line.lstrip('||'))
|
||||
elif line.startswith('|'):
|
||||
add_domain_to_set(domains, line.lstrip('|'))
|
||||
elif line.startswith('.'):
|
||||
add_domain_to_set(domains, line.lstrip('.'))
|
||||
else:
|
||||
add_domain_to_set(domains, line)
|
||||
# TODO: reduce ['www.google.com', 'google.com'] to ['google.com']
|
||||
return domains
|
||||
|
||||
|
||||
def generate_acl(domains):
|
||||
header ="""#
|
||||
# GFW list from https://github.com/gfwlist/gfwlist/blob/master/gfwlist.txt
|
||||
# updated on DATE
|
||||
#
|
||||
|
||||
[bypass_all]
|
||||
|
||||
[proxy_list]
|
||||
|
||||
"""
|
||||
header = header.replace('DATE', str(date.today()))
|
||||
proxy_content = ""
|
||||
ip_content = ""
|
||||
|
||||
for domain in sorted(domains):
|
||||
try:
|
||||
socket.inet_aton(domain)
|
||||
ip_content += (domain + "\n")
|
||||
except socket.error:
|
||||
domain = domain.replace('.', '\.')
|
||||
proxy_content += ('(^|\.)' + domain + '$\n')
|
||||
|
||||
proxy_content = header + ip_content + proxy_content
|
||||
|
||||
return proxy_content
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
with open(args.input, 'rb') as f:
|
||||
content = f.read()
|
||||
content = decode_gfwlist(content)
|
||||
domains = parse_gfwlist(content)
|
||||
acl_content = generate_acl(domains)
|
||||
with open(args.output, 'wb') as f:
|
||||
f.write(acl_content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
7
client/android/shadowsocks/lint.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="MissingDefaultResource" severity="warning" />
|
||||
<issue id="MissingTranslation" severity="informational" />
|
||||
</lint>
|
||||
132
client/android/shadowsocks/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,132 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.amnezia.vpn.shadowsocks.core"
|
||||
tools:ignore="MissingLeanbackLauncher">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:fullBackupOnly="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
tools:targetApi="n">
|
||||
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg" />
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:label="@string/app_name"-->
|
||||
<!-- android:permission="android.permission.BIND_VPN_SERVICE"-->
|
||||
<!-- android:process=":BG"-->
|
||||
<!-- tools:targetApi="n">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.net.VpnService" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </service>-->
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.TransproxyService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:process=":QtOnlyProcess"-->
|
||||
<!-- tools:targetApi="n" />-->
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.ProxyService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:process=":QtOnlyProcess"-->
|
||||
<!-- tools:targetApi="n" />-->
|
||||
|
||||
<!-- <activity-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.VpnRequestActivity"-->
|
||||
<!-- android:excludeFromRecents="true"-->
|
||||
<!-- android:launchMode="singleTask"-->
|
||||
<!-- android:taskAffinity=""-->
|
||||
<!-- android:theme="@style/Theme.AppCompat.Translucent" />-->
|
||||
|
||||
<receiver
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.BootReceiver"
|
||||
android:directBootAware="true"
|
||||
android:enabled="false"
|
||||
android:process=":QtOnlyProcess"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/work/workmanager/src/main/AndroidManifest.xml -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="androidx.work.impl.WorkManagerInitializer"
|
||||
tools:node="remove" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<service
|
||||
android:name="androidx.work.impl.background.systemjob.SystemJobService"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
|
||||
<receiver
|
||||
android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback;
|
||||
|
||||
interface IShadowsocksService {
|
||||
int getState();
|
||||
String getProfileName();
|
||||
|
||||
void registerCallback(in IShadowsocksServiceCallback cb);
|
||||
void startListeningForBandwidth(in IShadowsocksServiceCallback cb, long timeout);
|
||||
oneway void stopListeningForBandwidth(in IShadowsocksServiceCallback cb);
|
||||
oneway void unregisterCallback(in IShadowsocksServiceCallback cb);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats;
|
||||
|
||||
//"oneway" unexpected. xinlake
|
||||
interface IShadowsocksServiceCallback {
|
||||
oneway void stateChanged(int state, String profileName, String msg);
|
||||
oneway void trafficUpdated(long profileId, in TrafficStats stats);
|
||||
// Traffic data has persisted to database, listener should refetch their data from database
|
||||
oneway void trafficPersisted(long profileId);
|
||||
}
|
||||
|
||||
//oneway interface IShadowsocksServiceCallback {
|
||||
// void stateChanged(int state, String profileName, String msg);
|
||||
// void trafficUpdated(long profileId, in TrafficStats stats);
|
||||
// // Traffic data has persisted to database, listener should refetch their data from database
|
||||
// void trafficPersisted(long profileId);
|
||||
//}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl;
|
||||
|
||||
parcelable TrafficStats;
|
||||
10377
client/android/shadowsocks/src/main/assets/acl/bypass-china.acl
Normal file
10391
client/android/shadowsocks/src/main/assets/acl/bypass-lan-china.acl
Normal file
@@ -0,0 +1,17 @@
|
||||
[proxy_all]
|
||||
|
||||
[bypass_list]
|
||||
0.0.0.0/8
|
||||
10.0.0.0/8
|
||||
100.64.0.0/10
|
||||
127.0.0.0/8
|
||||
169.254.0.0/16
|
||||
172.16.0.0/12
|
||||
192.0.0.0/29
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.18.0.0/15
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/3
|
||||
5245
client/android/shadowsocks/src/main/assets/acl/china-list.acl
Normal file
5492
client/android/shadowsocks/src/main/assets/acl/gfwlist.acl
Normal file
@@ -0,0 +1,51 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
private val componentName by lazy { ComponentName(app, org.amnezia.vpn.shadowsocks.core.BootReceiver::class.java) }
|
||||
var enabled: Boolean
|
||||
get() = app.packageManager.getComponentEnabledSetting(org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName) ==
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
set(value) = app.packageManager.setComponentEnabledSetting(
|
||||
org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName,
|
||||
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val locked = when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> false
|
||||
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
|
||||
else -> return
|
||||
}
|
||||
if (DataStore.directBootAware == locked) org.amnezia.vpn.shadowsocks.core.Core.startService()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2018 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.UserManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.*
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object Core {
|
||||
const val TAG = "Core"
|
||||
|
||||
lateinit var app: Application
|
||||
lateinit var configureIntent: (Context) -> PendingIntent
|
||||
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
|
||||
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
|
||||
val directBootSupported by lazy {
|
||||
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
|
||||
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
|
||||
}
|
||||
|
||||
val activeProfileIds
|
||||
get() = ProfileManager.getProfile(DataStore.profileId).let {
|
||||
if (it == null) emptyList() else listOfNotNull(it.id, it.udpFallback)
|
||||
}
|
||||
val currentProfile: Pair<Profile, Profile?>?
|
||||
get() {
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile()?.apply { return this }
|
||||
return ProfileManager.expand(ProfileManager.getProfile(DataStore.profileId)
|
||||
?: return null)
|
||||
}
|
||||
|
||||
fun switchProfile(id: Long): Profile {
|
||||
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
|
||||
DataStore.profileId = result.id
|
||||
return result
|
||||
}
|
||||
|
||||
fun init(app: Application, configureClass: KClass<out Any>) {
|
||||
Core.app = app
|
||||
configureIntent = {
|
||||
PendingIntent.getActivity(it, 0,
|
||||
Intent(it, configureClass.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
|
||||
val old = Acl.getFile(Acl.CUSTOM_RULES, app)
|
||||
if (old.canRead()) {
|
||||
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
||||
old.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
||||
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
||||
|
||||
// handle data restored/crash
|
||||
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
|
||||
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
|
||||
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
|
||||
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
|
||||
val assetManager = app.assets
|
||||
try {
|
||||
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
|
||||
File(ContextCompat.getNoBackupFilesDir(deviceStorage), file).outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
printLog(e)
|
||||
}
|
||||
DataStore.publicStore.putLong(Key.assetUpdateTime, packageInfo.lastUpdateTime)
|
||||
}
|
||||
updateNotificationChannels()
|
||||
}
|
||||
|
||||
fun updateNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
val nm = app.getSystemService<NotificationManager>()!!
|
||||
nm.createNotificationChannels(listOf(
|
||||
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
|
||||
NotificationManager.IMPORTANCE_LOW)))
|
||||
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
||||
}
|
||||
}
|
||||
|
||||
fun getPackageInfo(packageName: String) = app.packageManager.getPackageInfo(packageName,
|
||||
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
||||
|
||||
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
|
||||
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
|
||||
|
||||
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() {
|
||||
init {
|
||||
app.registerReceiver(this, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
|
||||
callback()
|
||||
if (onetime) app.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
class VpnManager private constructor() {
|
||||
|
||||
var state = BaseService.State.Idle
|
||||
private var context: Context? = null
|
||||
private val handler = Handler()
|
||||
private val connection = ShadowsocksConnection(handler, true)
|
||||
private var listener: OnStatusChangeListener? = null
|
||||
private val callback: ShadowsocksConnection.Callback = object : ShadowsocksConnection.Callback {
|
||||
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {
|
||||
changeState(state)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
|
||||
|
||||
override fun onServiceConnected(service: IShadowsocksService) {
|
||||
changeState(try {
|
||||
BaseService.State.values()[service.state]
|
||||
} catch (_: DeadObjectException) {
|
||||
BaseService.State.Idle
|
||||
})
|
||||
}
|
||||
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
super.trafficUpdated(profileId, stats)
|
||||
listener?.onTrafficUpdated(profileId, stats)
|
||||
}
|
||||
override fun onBinderDied() {
|
||||
disconnect()
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
context?.let {
|
||||
connection.connect(it, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
context?.let { connection.disconnect(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CONNECT = 1
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var instance: VpnManager? = null
|
||||
|
||||
fun getInstance(): VpnManager {
|
||||
if (instance == null) {
|
||||
instance = VpnManager()
|
||||
}
|
||||
return instance as VpnManager
|
||||
}
|
||||
}
|
||||
|
||||
fun init(context: Context){
|
||||
this.context=context
|
||||
connect()
|
||||
}
|
||||
|
||||
fun run() {
|
||||
when {
|
||||
state.canStop -> Core.stopService()
|
||||
// DataStore.serviceMode == Key.modeVpn -> {
|
||||
// val intent = VpnService.prepare(activity)
|
||||
// if (intent != null) activity.startActivityForResult(intent, REQUEST_CONNECT)
|
||||
// else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
|
||||
// }
|
||||
else -> Core.startService()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setOnStatusChangeListener(listener: OnStatusChangeListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
connection.bandwidthTimeout = 0
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
connection.bandwidthTimeout = 1000
|
||||
}
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when {
|
||||
requestCode != REQUEST_CONNECT -> {
|
||||
}
|
||||
resultCode == Activity.RESULT_OK -> Core.startService()
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeState(state: BaseService.State) {
|
||||
this.state = state
|
||||
this.listener?.onStatusChanged(state)
|
||||
}
|
||||
|
||||
interface OnStatusChangeListener {
|
||||
fun onStatusChanged(state: BaseService.State)
|
||||
|
||||
fun onTrafficUpdated(profileId: Long, stats: TrafficStats)
|
||||
}
|
||||
|
||||
enum class Route(name: String) {
|
||||
|
||||
ALL("all")
|
||||
|
||||
,
|
||||
BY_PASS_LAN("bypass-lan")
|
||||
|
||||
,
|
||||
BY_PASS_CHINA("bypass-china")
|
||||
|
||||
,
|
||||
BY_PASS_LAN_CHINA("bypass-lan-china")
|
||||
|
||||
,
|
||||
GFW_LIST("gfwlist")
|
||||
|
||||
,
|
||||
CHINA_LIST("china-list")
|
||||
|
||||
,
|
||||
CUSTOM_RULES("custom-rules");
|
||||
|
||||
var route = name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
|
||||
class VpnRequestActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG = "VpnRequestActivity"
|
||||
private const val REQUEST_CONNECT = 1
|
||||
}
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (DataStore.serviceMode != Key.modeVpn) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
|
||||
receiver = broadcastReceiver { _, _ -> request() }
|
||||
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
|
||||
} else request()
|
||||
}
|
||||
|
||||
private fun request() {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
|
||||
else startActivityForResult(intent, REQUEST_CONNECT)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == RESULT_OK) Core.startService() else {
|
||||
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (receiver != null) unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
class Acl {
|
||||
companion object {
|
||||
const val TAG = "Acl"
|
||||
const val ALL = "all"
|
||||
const val BYPASS_LAN = "bypass-lan"
|
||||
const val BYPASS_CHN = "bypass-china"
|
||||
const val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||
const val GFWLIST = "gfwlist"
|
||||
const val CHINALIST = "china-list"
|
||||
const val CUSTOM_RULES = "custom-rules"
|
||||
|
||||
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||
|
||||
fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.noBackupFilesDir, "$id.acl")
|
||||
|
||||
var customRules: Acl
|
||||
get() {
|
||||
val acl = Acl()
|
||||
val str = DataStore.publicStore.getString(CUSTOM_RULES)
|
||||
if (str != null) acl.fromReader(str.reader(), true)
|
||||
if (!acl.bypass) {
|
||||
acl.bypass = true
|
||||
acl.subnets.clear()
|
||||
}
|
||||
return acl
|
||||
}
|
||||
set(value) = DataStore.publicStore.putString(CUSTOM_RULES,
|
||||
if ((!value.bypass || value.subnets.size() == 0) && value.bypassHostnames.size() == 0 &&
|
||||
value.proxyHostnames.size() == 0 && value.urls.size() == 0) null else value.toString())
|
||||
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
|
||||
}
|
||||
|
||||
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||
override fun onInserted(position: Int, count: Int) { }
|
||||
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) { }
|
||||
override fun onChanged(position: Int, count: Int) { }
|
||||
override fun onRemoved(position: Int, count: Int) { }
|
||||
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
|
||||
override fun compare(o1: T?, o2: T?): Int =
|
||||
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
|
||||
abstract fun compareNonNull(o1: T, o2: T): Int
|
||||
}
|
||||
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
|
||||
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
|
||||
}
|
||||
private object StringSorter : DefaultSorter<String>()
|
||||
private object SubnetSorter : DefaultSorter<Subnet>()
|
||||
private object URLSorter : BaseSorter<URL>() {
|
||||
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
|
||||
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
|
||||
}
|
||||
|
||||
val bypassHostnames = SortedList(String::class.java, StringSorter)
|
||||
val proxyHostnames = SortedList(String::class.java, StringSorter)
|
||||
val subnets = SortedList(Subnet::class.java, SubnetSorter)
|
||||
val urls = SortedList(URL::class.java, URLSorter)
|
||||
var bypass = false
|
||||
|
||||
fun fromAcl(other: Acl): Acl {
|
||||
bypassHostnames.clear()
|
||||
for (item in other.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||
proxyHostnames.clear()
|
||||
for (item in other.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||
subnets.clear()
|
||||
for (item in other.subnets.asIterable()) subnets.add(item)
|
||||
urls.clear()
|
||||
for (item in other.urls.asIterable()) urls.add(item)
|
||||
bypass = other.bypass
|
||||
return this
|
||||
}
|
||||
fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl {
|
||||
bypassHostnames.clear()
|
||||
proxyHostnames.clear()
|
||||
subnets.clear()
|
||||
urls.clear()
|
||||
bypass = defaultBypass
|
||||
val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||
val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||
var hostnames: SortedList<String>? = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||
var subnets: SortedList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
|
||||
reader.useLines {
|
||||
for (line in it) {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
val blocks = (line as java.lang.String).split("#", 2)
|
||||
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
|
||||
if (url != null) urls.add(URL(url))
|
||||
when (val input = blocks[0].trim()) {
|
||||
"[outbound_block_list]" -> {
|
||||
hostnames = null
|
||||
subnets = null
|
||||
}
|
||||
"[black_list]", "[bypass_list]" -> {
|
||||
hostnames = bypassHostnames
|
||||
subnets = bypassSubnets
|
||||
}
|
||||
"[white_list]", "[proxy_list]" -> {
|
||||
hostnames = proxyHostnames
|
||||
subnets = proxySubnets
|
||||
}
|
||||
"[reject_all]", "[bypass_all]" -> bypass = true
|
||||
"[accept_all]", "[proxy_all]" -> bypass = false
|
||||
else -> if (subnets != null && input.isNotEmpty()) {
|
||||
val subnet = Subnet.fromString(input)
|
||||
if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
|
||||
return this
|
||||
}
|
||||
|
||||
fun fromId(id: String): Acl = try {
|
||||
fromReader(getFile(id).bufferedReader())
|
||||
} catch (_: IOException) { this }
|
||||
|
||||
suspend fun flatten(depth: Int, connect: suspend (URL) -> URLConnection): Acl {
|
||||
if (depth > 0) for (url in urls.asIterable()) {
|
||||
val child = Acl()
|
||||
try {
|
||||
child.fromReader(connect(url).getInputStream().bufferedReader(), bypass).flatten(depth - 1, connect)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
continue
|
||||
}
|
||||
if (bypass != child.bypass) {
|
||||
child.subnets.clear() // subnets for the different mode are discarded
|
||||
child.bypass = bypass
|
||||
}
|
||||
for (item in child.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||
for (item in child.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||
for (item in child.subnets.asIterable()) subnets.add(item)
|
||||
}
|
||||
urls.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val result = StringBuilder()
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
val bypassList = (if (bypass) {
|
||||
bypassHostnames.asIterable().asSequence()
|
||||
} else {
|
||||
subnets.asIterable().asSequence().map(Subnet::toString) + bypassHostnames.asIterable().asSequence()
|
||||
}).toList()
|
||||
val proxyList = (if (bypass) {
|
||||
subnets.asIterable().asSequence().map(Subnet::toString) + proxyHostnames.asIterable().asSequence()
|
||||
} else {
|
||||
proxyHostnames.asIterable().asSequence()
|
||||
}).toList()
|
||||
if (bypassList.isNotEmpty()) {
|
||||
result.append("[bypass_list]\n")
|
||||
result.append(bypassList.joinToString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
if (proxyList.isNotEmpty()) {
|
||||
result.append("[proxy_list]\n")
|
||||
result.append(proxyList.joinToString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" })
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||
companion object {
|
||||
private const val KEY_ROUTE = "route"
|
||||
|
||||
fun schedule(route: String) = WorkManager.getInstance().enqueueUniqueWork(route, ExistingWorkPolicy.REPLACE,
|
||||
OneTimeWorkRequestBuilder<AclSyncer>().run {
|
||||
setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
|
||||
setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setRequiresCharging(true)
|
||||
.build())
|
||||
setInitialDelay(10, TimeUnit.SECONDS)
|
||||
build()
|
||||
})
|
||||
}
|
||||
|
||||
override val coroutineContext get() = Dispatchers.IO
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val route = inputData.getString(KEY_ROUTE)!!
|
||||
val acl = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openStream().bufferedReader()
|
||||
.use { it.readText() }
|
||||
Acl.getFile(route).printWriter().use { it.write(acl) }
|
||||
Result.success()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.TransproxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
/**
|
||||
* This object should be compact as it will not get GC-ed.
|
||||
*/
|
||||
class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
private var listenForDeath: Boolean = false) :
|
||||
ServiceConnection, IBinder.DeathRecipient {
|
||||
companion object {
|
||||
val serviceClass get() = when (DataStore.serviceMode) {
|
||||
Key.modeProxy -> ProxyService::class
|
||||
Key.modeVpn -> ShadowsocksVpnService::class
|
||||
Key.modeTransproxy -> TransproxyService::class
|
||||
else -> throw UnknownError()
|
||||
}.java
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
||||
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
|
||||
fun trafficPersisted(profileId: Long) { }
|
||||
|
||||
fun onServiceConnected(service: IShadowsocksService)
|
||||
/**
|
||||
* Different from Android framework, this method will be called even when you call `detachService`.
|
||||
*/
|
||||
fun onServiceDisconnected() { }
|
||||
fun onBinderDied() { }
|
||||
}
|
||||
|
||||
private var connectionActive = false
|
||||
private var callbackRegistered = false
|
||||
private var callback: Callback? = null
|
||||
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
val callback = callback ?: return
|
||||
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
|
||||
}
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
val callback = callback ?: return
|
||||
handler.post {
|
||||
callback.trafficUpdated(profileId, stats)
|
||||
}
|
||||
}
|
||||
override fun trafficPersisted(profileId: Long) {
|
||||
val callback = callback ?: return
|
||||
handler.post { callback.trafficPersisted(profileId) }
|
||||
}
|
||||
}
|
||||
private var binder: IBinder? = null
|
||||
|
||||
var bandwidthTimeout = 0L
|
||||
set(value) {
|
||||
val service = service
|
||||
if (bandwidthTimeout != value && service != null)
|
||||
if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
|
||||
service.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: DeadObjectException) { }
|
||||
field = value
|
||||
}
|
||||
var service: IShadowsocksService? = null
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||
this.binder = binder
|
||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||
this.service = service
|
||||
if (!callbackRegistered) try {
|
||||
service.registerCallback(serviceCallback)
|
||||
callbackRegistered = true
|
||||
if (bandwidthTimeout > 0) service.startListeningForBandwidth(serviceCallback, bandwidthTimeout)
|
||||
} catch (_: RemoteException) { }
|
||||
callback!!.onServiceConnected(service)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
unregisterCallback()
|
||||
callback?.onServiceDisconnected()
|
||||
service = null
|
||||
binder = null
|
||||
}
|
||||
|
||||
override fun binderDied() {
|
||||
service = null
|
||||
callback?.also { handler.post(it::onBinderDied) }
|
||||
}
|
||||
|
||||
private fun unregisterCallback() {
|
||||
val service = service
|
||||
if (service != null && callbackRegistered) try {
|
||||
service.unregisterCallback(serviceCallback)
|
||||
} catch (_: RemoteException) { }
|
||||
callbackRegistered = false
|
||||
}
|
||||
|
||||
fun connect(context: Context, callback: Callback) {
|
||||
if (connectionActive) return
|
||||
connectionActive = true
|
||||
check(this.callback == null)
|
||||
this.callback = callback
|
||||
val intent = Intent(context, serviceClass).setAction(Action.SERVICE)
|
||||
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
fun disconnect(context: Context) {
|
||||
unregisterCallback()
|
||||
if (connectionActive) try {
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) { } // ignore
|
||||
connectionActive = false
|
||||
if (listenForDeath) binder?.unlinkToDeath(this, 0)
|
||||
binder = null
|
||||
service?.stopListeningForBandwidth(serviceCallback)
|
||||
service = null
|
||||
callback = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
data class TrafficStats(
|
||||
// Bytes per second
|
||||
var txRate: Long = 0L,
|
||||
var rxRate: Long = 0L,
|
||||
|
||||
// Bytes for the current session
|
||||
var txTotal: Long = 0L,
|
||||
var rxTotal: Long = 0L
|
||||
) : Parcelable {
|
||||
operator fun plus(other: TrafficStats) = TrafficStats(
|
||||
txRate + other.txRate, rxRate + other.rxRate,
|
||||
txTotal + other.txTotal, rxTotal + other.rxTotal)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readLong(), parcel.readLong(), parcel.readLong())
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeLong(txRate)
|
||||
parcel.writeLong(rxRate)
|
||||
parcel.writeLong(txTotal)
|
||||
parcel.writeLong(rxTotal)
|
||||
}
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TrafficStats> {
|
||||
override fun createFromParcel(parcel: Parcel) = TrafficStats(parcel)
|
||||
override fun newArray(size: Int): Array<TrafficStats?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.*
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||
import java.io.File
|
||||
import java.net.BindException
|
||||
import java.net.InetAddress
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* This object uses WeakMap to simulate the effects of multi-inheritance.
|
||||
*/
|
||||
object BaseService {
|
||||
enum class State(val canStop: Boolean = false) {
|
||||
/**
|
||||
* Idle state is only used by UI and will never be returned by BaseService.
|
||||
*/
|
||||
Idle,
|
||||
Connecting(true),
|
||||
Connected(true),
|
||||
Stopping,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
const val CONFIG_FILE = "shadowsocks.conf"
|
||||
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
|
||||
|
||||
class Data(private val service: Interface) {
|
||||
var state = State.Stopped
|
||||
var processes: GuardedProcessPool? = null
|
||||
var proxy: ProxyInstance? = null
|
||||
var udpFallback: ProxyInstance? = null
|
||||
|
||||
// var notification: ServiceNotification? = null
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
Action.RELOAD -> service.forceLoad()
|
||||
else -> service.stopRunner()
|
||||
}
|
||||
}
|
||||
var closeReceiverRegistered = false
|
||||
|
||||
val binder = Binder(this)
|
||||
var connectingJob: Job? = null
|
||||
|
||||
fun changeState(s: State, msg: String? = null) {
|
||||
if (state == s && msg == null) return
|
||||
binder.stateChanged(s, msg)
|
||||
state = s
|
||||
}
|
||||
}
|
||||
|
||||
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
|
||||
val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
|
||||
super.onCallbackDied(callback, cookie)
|
||||
stopListeningForBandwidth(callback ?: return)
|
||||
}
|
||||
}
|
||||
private val bandwidthListeners =
|
||||
mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
private val handler = Handler()
|
||||
|
||||
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
|
||||
|
||||
override fun registerCallback(cb: IShadowsocksServiceCallback) {
|
||||
callbacks.register(cb)
|
||||
}
|
||||
|
||||
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
|
||||
repeat(callbacks.beginBroadcast()) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: DeadObjectException) {
|
||||
} catch (e: Exception) {
|
||||
printLog(e)
|
||||
}
|
||||
}
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
|
||||
private fun registerTimeout() {
|
||||
handler.postDelayed(this::onTimeout, bandwidthListeners.values.minOrNull() ?: return)
|
||||
}
|
||||
|
||||
private fun onTimeout() {
|
||||
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||
val stats = proxies
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) {
|
||||
stats.forEach { (id, stats) -> item.trafficUpdated(id, stats) }
|
||||
item.trafficUpdated(0, sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
registerTimeout()
|
||||
}
|
||||
|
||||
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
|
||||
val wasEmpty = bandwidthListeners.isEmpty()
|
||||
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
|
||||
if (wasEmpty) registerTimeout()
|
||||
if (data?.state != State.Connected) return
|
||||
var sum = TrafficStats()
|
||||
val data = data
|
||||
val proxy = data?.proxy ?: return
|
||||
proxy.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(
|
||||
proxy.profile.id, if (stats == null) sum else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
}
|
||||
data.udpFallback?.also { udpFallback ->
|
||||
udpFallback.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(
|
||||
udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
cb.trafficUpdated(0, sum)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregisterCallback(cb: IShadowsocksServiceCallback) {
|
||||
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
||||
callbacks.unregister(cb)
|
||||
}
|
||||
|
||||
fun stateChanged(s: State, msg: String?) {
|
||||
val profileName = profileName
|
||||
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
|
||||
}
|
||||
|
||||
fun trafficPersisted(ids: List<Long>) {
|
||||
if (bandwidthListeners.isNotEmpty() && ids.isNotEmpty()) broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) ids.forEach(item::trafficPersisted)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
callbacks.kill()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
data = null
|
||||
}
|
||||
}
|
||||
|
||||
interface Interface {
|
||||
val data: Data
|
||||
val tag: String
|
||||
// fun createNotification(profileName: String): ServiceNotification
|
||||
|
||||
fun onBind(intent: Intent): IBinder? =
|
||||
if (intent.action == Action.SERVICE) data.binder else null
|
||||
|
||||
fun forceLoad() {
|
||||
val (profile, fallback) = Core.currentProfile
|
||||
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
||||
if (profile.host.isEmpty() || profile.password.isEmpty() ||
|
||||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())
|
||||
) {
|
||||
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
||||
return
|
||||
}
|
||||
val s = data.state
|
||||
when {
|
||||
s == State.Stopped -> startRunner()
|
||||
s.canStop -> stopRunner(true)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
|
||||
|
||||
suspend fun startProcesses() {
|
||||
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||
?.isUserUnlocked != false
|
||||
) app else Core.deviceStorage).noBackupFilesDir
|
||||
val udpFallback = data.udpFallback
|
||||
data.proxy!!.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
if (udpFallback == null) "-u" else null
|
||||
)
|
||||
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
|
||||
udpFallback?.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||
File(configRoot, CONFIG_FILE_UDP),
|
||||
"-U"
|
||||
)
|
||||
}
|
||||
|
||||
fun startRunner() {
|
||||
this as Context
|
||||
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
|
||||
else startService(Intent(this, javaClass))
|
||||
}
|
||||
|
||||
fun killProcesses(scope: CoroutineScope) {
|
||||
data.processes?.run {
|
||||
close(scope)
|
||||
data.processes = null
|
||||
}
|
||||
}
|
||||
|
||||
fun stopRunner(restart: Boolean = false, msg: String? = null) {
|
||||
if (data.state == State.Stopping) return
|
||||
// change the state
|
||||
data.changeState(State.Stopping)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
|
||||
this@Interface as Service
|
||||
// we use a coroutineScope here to allow clean-up in parallel
|
||||
coroutineScope {
|
||||
killProcesses(this)
|
||||
// clean up receivers
|
||||
val data = data
|
||||
if (data.closeReceiverRegistered) {
|
||||
unregisterReceiver(data.closeReceiver)
|
||||
data.closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
// data.notification?.destroy()
|
||||
// data.notification = null
|
||||
|
||||
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
|
||||
it.shutdown(this)
|
||||
it.profile.id
|
||||
}
|
||||
data.proxy = null
|
||||
data.udpFallback = null
|
||||
data.binder.trafficPersisted(ids)
|
||||
}
|
||||
|
||||
// change the state
|
||||
data.changeState(State.Stopped, msg)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
if (restart) {
|
||||
startRunner()
|
||||
} else {
|
||||
Log.d("Aman", "Stop Self BaseService-------")
|
||||
// stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun preInit() {}
|
||||
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
|
||||
suspend fun openConnection(url: URL) = url.openConnection()
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val data = data
|
||||
if (data.state != State.Stopped) return Service.START_REDELIVER_INTENT
|
||||
val profilePair = Core.currentProfile
|
||||
this as Context
|
||||
if (profilePair == null) {
|
||||
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
|
||||
// data.notification = createNotification("")
|
||||
stopRunner(false, getString(R.string.profile_empty))
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
val (profile, fallback) = profilePair
|
||||
profile.name = profile.formattedName // save name for later queries
|
||||
val proxy = ProxyInstance(profile)
|
||||
data.proxy = proxy
|
||||
data.udpFallback =
|
||||
if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
|
||||
if (!data.closeReceiverRegistered) {
|
||||
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||
addAction(Action.RELOAD)
|
||||
addAction(Intent.ACTION_SHUTDOWN)
|
||||
addAction(Action.CLOSE)
|
||||
})
|
||||
data.closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
// data.notification = createNotification(profile.formattedName)
|
||||
|
||||
data.changeState(State.Connecting)
|
||||
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
Executable.killAll() // clean up old processes
|
||||
preInit()
|
||||
proxy.init(this@Interface)
|
||||
data.udpFallback?.init(this@Interface)
|
||||
|
||||
data.processes = GuardedProcessPool {
|
||||
printLog(it)
|
||||
stopRunner(false, it.readableMessage)
|
||||
}
|
||||
startProcesses()
|
||||
|
||||
proxy.scheduleUpdate()
|
||||
data.udpFallback?.scheduleUpdate()
|
||||
|
||||
data.changeState(State.Connected)
|
||||
} catch (_: CancellationException) {
|
||||
// if the job was cancelled, it is canceller's responsibility to call stopRunner
|
||||
} catch (_: UnknownHostException) {
|
||||
stopRunner(false, getString(R.string.invalid_server))
|
||||
} catch (exc: Throwable) {
|
||||
if (exc !is PluginManager.PluginNotFoundException &&
|
||||
exc !is BindException &&
|
||||
exc !is ShadowsocksVpnService.NullConnectionException
|
||||
) {
|
||||
printLog(exc)
|
||||
}
|
||||
stopRunner(
|
||||
false,
|
||||
"${getString(R.string.service_failed)}: ${exc.readableMessage}"
|
||||
)
|
||||
} finally {
|
||||
data.connectingJob = null
|
||||
}
|
||||
}
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.text.TextUtils
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object Executable {
|
||||
const val REDSOCKS = "libredsocks.so"
|
||||
const val SS_LOCAL = "libsslocal.so"
|
||||
const val TUN2SOCKS = "libtun2socks.so"
|
||||
|
||||
private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
val exe = File(try {
|
||||
File(process, "cmdline").inputStream().bufferedReader().readText()
|
||||
} catch (_: IOException) {
|
||||
continue
|
||||
}.split(Character.MIN_VALUE, limit = 2).first())
|
||||
if (EXECUTABLES.contains(exe.name)) try {
|
||||
Os.kill(process.name.toInt(), OsConstants.SIGKILL)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.ESRCH) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import androidx.annotation.MainThread
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
|
||||
companion object {
|
||||
private const val TAG = "GuardedProcessPool"
|
||||
private val pid by lazy {
|
||||
Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid").apply { isAccessible = true }
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Guard(private val cmd: List<String>) {
|
||||
private lateinit var process: Process
|
||||
|
||||
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
|
||||
input.bufferedReader().forEachLine(logger)
|
||||
} catch (_: IOException) {
|
||||
} // ignore
|
||||
|
||||
fun start() {
|
||||
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
|
||||
}
|
||||
|
||||
suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {
|
||||
var running = true
|
||||
val cmdName = File(cmd.first()).nameWithoutExtension
|
||||
val exitChannel = Channel<Int>()
|
||||
try {
|
||||
while (true) {
|
||||
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
|
||||
thread(name = "stdout-$cmdName") {
|
||||
streamLogger(process.inputStream) { Log.i(cmdName, it) }
|
||||
// this thread also acts as a daemon thread for waitFor
|
||||
runBlocking { exitChannel.send(process.waitFor()) }
|
||||
}
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
val exitCode = exitChannel.receive()
|
||||
running = false
|
||||
if (SystemClock.elapsedRealtime() - startTime < 1000) {
|
||||
throw IOException("$cmdName exits too fast (exit code: $exitCode)")
|
||||
}
|
||||
start()
|
||||
onRestartCallback?.invoke()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
|
||||
} finally {
|
||||
if (running) withContext(NonCancellable) {
|
||||
// clean-up cannot be cancelled
|
||||
if (Build.VERSION.SDK_INT < 24) {
|
||||
try {
|
||||
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.ESRCH) throw e
|
||||
}
|
||||
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext
|
||||
}
|
||||
process.destroy() // kill the process
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext
|
||||
process.destroyForcibly() // Force to kill the process if it's still alive
|
||||
}
|
||||
exitChannel.receive()
|
||||
} // otherwise process already exited, nothing to be done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
||||
|
||||
@MainThread
|
||||
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
|
||||
Guard(cmd).apply {
|
||||
start() // if start fails, IOException will be thrown directly
|
||||
launch { looper(onRestartCallback) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun close(scope: CoroutineScope) {
|
||||
cancel()
|
||||
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Socks5Endpoint
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
|
||||
object LocalDnsService {
|
||||
private val googleApisTester =
|
||||
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
|
||||
private val chinaIpList by lazy {
|
||||
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
|
||||
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
|
||||
}
|
||||
|
||||
private val servers = WeakHashMap<Interface, LocalDnsServer>()
|
||||
|
||||
interface Interface : BaseService.Interface {
|
||||
override suspend fun startProcesses() {
|
||||
super.startProcesses()
|
||||
val profile = data.proxy!!.profile
|
||||
val dns = URI("dns://${profile.remoteDns}")
|
||||
LocalDnsServer(this::resolver,
|
||||
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||
DataStore.proxyAddress).apply {
|
||||
tcp = !profile.udpdns
|
||||
when (profile.route) {
|
||||
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
|
||||
remoteDomainMatcher = googleApisTester
|
||||
localIpMatcher = chinaIpList
|
||||
}
|
||||
Acl.CHINALIST -> { }
|
||||
else -> forwardOnly = true
|
||||
}
|
||||
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
|
||||
}
|
||||
|
||||
override fun killProcesses(scope: CoroutineScope) {
|
||||
servers.remove(this)?.shutdown(scope)
|
||||
super.killProcesses(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.content.Context
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.AclSyncer
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* This class sets up environment for ss-local.
|
||||
*/
|
||||
class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
|
||||
private var configFile: File? = null
|
||||
var trafficMonitor: TrafficMonitor? = null
|
||||
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
|
||||
val pluginPath by lazy { PluginManager.init(plugin) }
|
||||
|
||||
suspend fun init(service: BaseService.Interface) {
|
||||
if (route == Acl.CUSTOM_RULES) withContext(Dispatchers.IO) {
|
||||
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, service::openConnection))
|
||||
}
|
||||
|
||||
// it's hard to resolve DNS on a specific interface so we'll do it here
|
||||
if (profile.host.parseNumericAddress() == null) {
|
||||
while (true) try {
|
||||
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
|
||||
profile.host = io.await().firstOrNull()?.hostAddress ?: throw UnknownHostException()
|
||||
return
|
||||
} catch (e: UnknownHostException) {
|
||||
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
|
||||
if (!DataStore.hasArc0) throw e
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
|
||||
* device storage, depending on which is currently available.
|
||||
*/
|
||||
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
|
||||
trafficMonitor = TrafficMonitor(stat)
|
||||
|
||||
this.configFile = configFile
|
||||
val config = profile.toJson()
|
||||
if (pluginPath != null) config.put("plugin", pluginPath).put("plugin_opts", plugin.toString())
|
||||
configFile.writeText(config.toString())
|
||||
|
||||
val cmd = service.buildAdditionalArguments(arrayListOf(
|
||||
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||
"-b", DataStore.listenAddress,
|
||||
"-l", DataStore.portProxy.toString(),
|
||||
"-t", "600",
|
||||
"-S", stat.absolutePath,
|
||||
"-c", configFile.absolutePath))
|
||||
if (extraFlag != null) cmd.add(extraFlag)
|
||||
|
||||
if (route != Acl.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += Acl.getFile(route).absolutePath
|
||||
}
|
||||
|
||||
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
|
||||
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_LAN) cmd += "-D"
|
||||
|
||||
if (DataStore.tcpFastOpen) cmd += "--fast-open"
|
||||
|
||||
service.data.processes!!.start(cmd)
|
||||
}
|
||||
|
||||
fun scheduleUpdate() {
|
||||
if (route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncer.schedule(route)
|
||||
}
|
||||
|
||||
fun shutdown(scope: CoroutineScope) {
|
||||
trafficMonitor?.apply {
|
||||
thread.shutdown(scope)
|
||||
// Make sure update total traffic when stopping the runner
|
||||
try {
|
||||
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
||||
val profile = ProfileManager.getProfile(profile.id) ?: return
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
ProfileManager.updateProfile(profile)
|
||||
} catch (e: IOException) {
|
||||
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
|
||||
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == profile.id }
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
profile.dirty = true
|
||||
DirectBoot.update(profile)
|
||||
DirectBoot.listenForUnlock()
|
||||
}
|
||||
}
|
||||
trafficMonitor = null
|
||||
configFile?.delete() // remove old config possibly in device storage
|
||||
configFile = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Shadowsocks service at its minimum.
|
||||
*/
|
||||
class ProxyService : Service(), BaseService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksProxyService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-proxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
data.binder.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
///*******************************************************************************
|
||||
// * *
|
||||
// * Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
// * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
// * *
|
||||
// * This program is free software: you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
// * *
|
||||
// *******************************************************************************/
|
||||
//
|
||||
//package org.amnezia.vpn.shadowsocks.core.bg
|
||||
//
|
||||
//import android.app.KeyguardManager
|
||||
//import android.app.NotificationManager
|
||||
//import android.app.PendingIntent
|
||||
//import android.app.Service
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.content.IntentFilter
|
||||
//import android.os.Build
|
||||
//import android.os.PowerManager
|
||||
//import android.text.format.Formatter
|
||||
//import androidx.core.app.NotificationCompat
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.core.content.getSystemService
|
||||
//import org.amnezia.vpn.shadowsocks.core.Core
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
//import org.amnezia.vpn.shadowsocks.core.R
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
//
|
||||
///**
|
||||
// * Android < 8 VPN: always invisible because of VPN notification/icon
|
||||
// * Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||
// * Android 8+: always visible due to system limitations
|
||||
// * (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||
// */
|
||||
//class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
// channel: String, private val visible: Boolean = false) {
|
||||
// private val keyGuard = (service as Context).getSystemService<KeyguardManager>()!!
|
||||
// private val nm by lazy { (service as Context).getSystemService<NotificationManager>()!! }
|
||||
// private val callback: IShadowsocksServiceCallback by lazy {
|
||||
// object : IShadowsocksServiceCallback.Stub() {
|
||||
// override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
// when (state) {
|
||||
// BaseService.State.Connected.ordinal -> {
|
||||
// builder.setContentText("VPN Connected")
|
||||
// }
|
||||
// BaseService.State.Stopped.ordinal -> {
|
||||
// builder.setContentText("VPN Disconnected")
|
||||
// }
|
||||
// }
|
||||
// } // ignore
|
||||
// override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
//// if (profileId != 0L) return
|
||||
//// service as Context
|
||||
//// val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate))
|
||||
//// val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))
|
||||
//// builder.setContentText("$txr↑\t$rxr↓")
|
||||
//// style.bigText(service.getString(R.string.stat_summary, txr, rxr,
|
||||
//// Formatter.formatFileSize(service, stats.txTotal),
|
||||
//// Formatter.formatFileSize(service, stats.rxTotal)))
|
||||
//// show()
|
||||
// }
|
||||
// override fun trafficPersisted(profileId: Long) { }
|
||||
// }
|
||||
// }
|
||||
//// private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||
// private var callbackRegistered = false
|
||||
//
|
||||
// private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
// .setWhen(0)
|
||||
// .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
// .setTicker(service.getString(R.string.forward_success))
|
||||
// .setContentTitle("AmneziaVPN -- testing")
|
||||
// .setContentIntent(Core.configureIntent(service))
|
||||
// .setSmallIcon(R.drawable.ic_amnezia_round)
|
||||
// private val style = NotificationCompat.BigTextStyle(builder).bigText("")
|
||||
// private var isVisible = true
|
||||
//
|
||||
// init {
|
||||
// service as Context
|
||||
//// if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
//// service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
//// update(if (service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||
//// Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
//// service.registerReceiver(lockReceiver, IntentFilter().apply {
|
||||
//// addAction(Intent.ACTION_SCREEN_ON)
|
||||
//// addAction(Intent.ACTION_SCREEN_OFF)
|
||||
//// if (visible && Build.VERSION.SDK_INT < 26) addAction(Intent.ACTION_USER_PRESENT)
|
||||
//// })
|
||||
// }
|
||||
//
|
||||
//// private fun update(action: String?, forceShow: Boolean = false) {
|
||||
//// if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
|
||||
//// Intent.ACTION_SCREEN_OFF -> {
|
||||
//// setVisible(false, forceShow)
|
||||
//// unregisterCallback() // unregister callback to save battery
|
||||
//// }
|
||||
//// Intent.ACTION_SCREEN_ON -> {
|
||||
//// setVisible(visible && !keyGuard.isKeyguardLocked, forceShow)
|
||||
//// service.data.binder.registerCallback(callback)
|
||||
//// service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||
//// callbackRegistered = true
|
||||
//// }
|
||||
//// Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// private fun unregisterCallback() {
|
||||
// if (callbackRegistered) {
|
||||
// service.data.binder.unregisterCallback(callback)
|
||||
// callbackRegistered = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||
// if (isVisible != visible) {
|
||||
// isVisible = visible
|
||||
// builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||
// show()
|
||||
// } else if (forceShow) show()
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private fun show() = (service as Service).startForeground(1337, builder.build())
|
||||
//
|
||||
// fun destroy() {
|
||||
//// (service as Service).unregisterReceiver(lockReceiver)
|
||||
// unregisterCallback()
|
||||
//// service.stopForeground(true)
|
||||
// nm.cancel(1337)
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import android.net.VpnService as BaseVpnService
|
||||
|
||||
open class ShadowsocksVpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
|
||||
|
||||
/**
|
||||
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
|
||||
*/
|
||||
private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
|
||||
}
|
||||
|
||||
class CloseableFd(val fd: FileDescriptor) : Closeable {
|
||||
override fun close() = Os.close(fd)
|
||||
}
|
||||
|
||||
private inner class ProtectWorker : ConcurrentLocalSocketListener("ShadowsocksVpnThread",
|
||||
File(Core.deviceStorage.noBackupFilesDir, "protect_path")) {
|
||||
override fun acceptInternal(socket: LocalSocket) {
|
||||
socket.inputStream.read()
|
||||
val fd = socket.ancillaryFileDescriptors!!.single()!!
|
||||
CloseableFd(fd).use {
|
||||
socket.outputStream.write(if (underlyingNetwork.let { network ->
|
||||
if (network != null && Build.VERSION.SDK_INT >= 23) try {
|
||||
network.bindSocket(fd)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
// suppress ENONET (Machine is not on the network)
|
||||
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
|
||||
false
|
||||
} else protect(getInt.invoke(fd) as Int)
|
||||
}) 0 else 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class NullConnectionException : NullPointerException() {
|
||||
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
||||
}
|
||||
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksVpnService"
|
||||
|
||||
val NOTIFICATION_CHANNEL_ID = "com.amnezia.vpnNotification"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, NOTIFICATION_CHANNEL_ID)
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
private var active = false
|
||||
private var metered = false
|
||||
private var underlyingNetwork: Network? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
|
||||
}
|
||||
private val underlyingNetworks
|
||||
get() =
|
||||
// clearing underlyingNetworks makes Android 9+ consider the network to be metered
|
||||
if (Build.VERSION.SDK_INT >= 28 && metered) null else underlyingNetwork?.let { arrayOf(it) }
|
||||
|
||||
override fun onBind(intent: Intent) = when (intent.action) {
|
||||
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
||||
else -> super<LocalDnsService.Interface>.onBind(intent)
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
stopRunner()
|
||||
}
|
||||
|
||||
override fun killProcesses(scope: CoroutineScope) {
|
||||
super.killProcesses(scope)
|
||||
active = false
|
||||
scope.launch { DefaultNetworkListener.stop(this) }
|
||||
worker?.shutdown(scope)
|
||||
worker = null
|
||||
conn?.close()
|
||||
conn = null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (DataStore.serviceMode == Key.modeVpn) {
|
||||
if (prepare(this) != null) {
|
||||
startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
stopRunner()
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
|
||||
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
|
||||
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
|
||||
|
||||
override suspend fun startProcesses() {
|
||||
worker = ProtectWorker().apply { start() }
|
||||
super.startProcesses()
|
||||
sendFd(startVpn())
|
||||
}
|
||||
|
||||
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
|
||||
cmd += "-V"
|
||||
return cmd
|
||||
}
|
||||
|
||||
private suspend fun startVpn(): FileDescriptor {
|
||||
val profile = data.proxy!!.profile
|
||||
val builder = Builder()
|
||||
.setConfigureIntent(Core.configureIntent(this))
|
||||
.setSession(profile.formattedName)
|
||||
.setMtu(VPN_MTU)
|
||||
.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||
.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
|
||||
if (profile.ipv6) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
val me = packageName
|
||||
if (profile.proxyApps) {
|
||||
profile.individual.split('\n')
|
||||
.filter { it != me }
|
||||
.forEach {
|
||||
try {
|
||||
if (profile.bypass) builder.addDisallowedApplication(it)
|
||||
else builder.addAllowedApplication(it)
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
printLog(ex)
|
||||
}
|
||||
}
|
||||
if (profile.bypass) {
|
||||
builder.addDisallowedApplication(me)
|
||||
}
|
||||
} else {
|
||||
builder.addDisallowedApplication(me)
|
||||
}
|
||||
|
||||
when (profile.route) {
|
||||
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
|
||||
else -> {
|
||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||
val subnet = Subnet.fromString(it)!!
|
||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||
}
|
||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
||||
}
|
||||
}
|
||||
|
||||
metered = profile.metered
|
||||
active = true // possible race condition here?
|
||||
if (Build.VERSION.SDK_INT >= 22) builder.setUnderlyingNetworks(underlyingNetworks)
|
||||
|
||||
val conn = builder.establish() ?: throw NullConnectionException()
|
||||
this.conn = conn
|
||||
|
||||
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",
|
||||
"--dnsgw", "127.0.0.1:${DataStore.portLocalDns}",
|
||||
"--loglevel", "warning")
|
||||
if (profile.ipv6) {
|
||||
cmd += "--netif-ip6addr"
|
||||
cmd += PRIVATE_VLAN6_ROUTER
|
||||
}
|
||||
cmd += "--enable-udprelay"
|
||||
data.processes!!.start(cmd, onRestartCallback = {
|
||||
try {
|
||||
sendFd(conn.fileDescriptor)
|
||||
} catch (e: ErrnoException) {
|
||||
e.printStackTrace()
|
||||
stopRunner(false, e.message)
|
||||
}
|
||||
})
|
||||
return conn.fileDescriptor
|
||||
}
|
||||
|
||||
private suspend fun sendFd(fd: FileDescriptor) {
|
||||
var tries = 0
|
||||
val path = File(Core.deviceStorage.noBackupFilesDir, "sock_path").absolutePath
|
||||
while (true) try {
|
||||
delay(50L shl tries)
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
localSocket.outputStream.write(42)
|
||||
}
|
||||
return
|
||||
} catch (e: IOException) {
|
||||
if (tries > 5) throw e
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
data.binder.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.os.SystemClock
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class TrafficMonitor(statFile: File) {
|
||||
val thread = object : LocalSocketListener("TrafficMonitor-" + statFile.name, statFile) {
|
||||
private val buffer = ByteArray(16)
|
||||
private val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
|
||||
override fun acceptInternal(socket: LocalSocket) {
|
||||
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
|
||||
val tx = stat.getLong(0)
|
||||
val rx = stat.getLong(8)
|
||||
if (current.txTotal != tx) {
|
||||
current.txTotal = tx
|
||||
dirty = true
|
||||
}
|
||||
if (current.rxTotal != rx) {
|
||||
current.rxTotal = rx
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
}.apply { start() }
|
||||
|
||||
val current = TrafficStats()
|
||||
var out = TrafficStats()
|
||||
private var timestampLast = 0L
|
||||
private var dirty = false
|
||||
|
||||
fun requestUpdate(): Pair<TrafficStats, Boolean> {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val delta = now - timestampLast
|
||||
timestampLast = now
|
||||
var updated = false
|
||||
if (delta != 0L) {
|
||||
if (dirty) {
|
||||
out = current.copy().apply {
|
||||
txRate = (txTotal - out.txTotal) * 1000 / delta
|
||||
rxRate = (rxTotal - out.rxTotal) * 1000 / delta
|
||||
}
|
||||
dirty = false
|
||||
updated = true
|
||||
} else {
|
||||
if (out.txRate != 0L) {
|
||||
out.txRate = 0
|
||||
updated = true
|
||||
}
|
||||
if (out.rxRate != 0L) {
|
||||
out.rxRate = 0
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair(out, updated)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import java.io.File
|
||||
|
||||
class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText(
|
||||
"""base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
redsocks {
|
||||
local_ip = ${DataStore.listenAddress};
|
||||
local_port = ${DataStore.portTransproxy};
|
||||
ip = 127.0.0.1;
|
||||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
"""
|
||||
)
|
||||
data.processes!!.start(
|
||||
listOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c",
|
||||
"redsocks.conf"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun startProcesses() {
|
||||
startRedsocksDaemon()
|
||||
super.startProcesses()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
data.binder.close()
|
||||
}
|
||||
}
|
||||