Compare commits

...

33 Commits

Author SHA1 Message Date
Mykola Baibuz
a5bf624f02 Merge branch 'dev' into feature/connection-string 2024-05-25 21:45:50 +03:00
Shehab Ahmed
d8020878d5 Fdroid metadata (#795) 2024-05-25 10:13:38 -07:00
Vladyslav Miachkov
b027fff103 Add clickable docs url on error (#806) 2024-05-25 03:00:51 -07:00
Vladyslav Miachkov
a0c06048cd Fix opening url after save config (#784) 2024-05-25 02:57:48 -07:00
Mykola Baibuz
140b3ae781 Merge branch 'dev' into feature/connection-string 2024-05-22 23:08:21 +03:00
pokamest
53746f2f66 Merge pull request #818 from amnezia-vpn/bugfix/dockerfile-copy
added deleting dockerfile before copying
2024-05-21 04:15:40 -07:00
pokamest
2649dba4ad Merge pull request #799 from amnezia-vpn/bugfix/fix-backup
Filter settings fields to backup
2024-05-21 04:11:41 -07:00
albexk
6a1e3c07b1 Update AWG (v0.2.8) (#809)
* Fix udpgso

* Fix amneziawg run dir

* Update Windows AWG binaries

* Update AWG (v0.2.8)

* Fix Windows pipe name

* Fix Windows tunnel service name

* Update Windows x86 AWG binary

* Change default MTU for WireGuard and AWG

* Fix preprocessor macros
2024-05-20 17:46:05 +01:00
pokamest
a365eff76f Merge pull request #812 from amnezia-vpn/feature/ios-tunnel-refactoring
PacketTunnelProvider refactoring
2024-05-20 08:28:48 -07:00
vladimir.kuznetsov
8f9acd9367 added deleting dockerfile before copying 2024-05-20 12:34:24 +02:00
Boris Verbitskii
9be13ea465 PacketTunnelProvider refactoring
- removing unnecessary dispatchQueue
- removing lazy initiation for wg and ovpn
- fix memory leaks
2024-05-17 18:17:08 +07:00
Mykola Baibuz
5f67ce2e45 Check UDP port for ShadowSocks container 2024-05-13 14:58:19 +03:00
Mykola Baibuz
206cccfaca ShadowSocks Xray protocol fixes 2024-05-11 22:58:13 +03:00
Mykola Baibuz
e4690cee0a open UDP port for SS container 2024-05-11 14:09:13 +03:00
Mykola Baibuz
8e56988ed0 trojan:// serialization refactoring 2024-05-10 23:18:43 +03:00
Mykola Baibuz
7cf49b8602 Add inbound traffic rule trojan protocol 2024-05-09 22:59:46 +03:00
Mykola Baibuz
a449baac78 Build fix 2024-05-09 17:10:02 +03:00
Mykola Baibuz
f721b90dc8 vless:// reality support 2024-05-09 14:17:55 +03:00
Mykola Baibuz
b23c61b6f7 "trojan://" connection string support 2024-05-09 13:56:48 +03:00
Mykola Baibuz
90d3dc5574 Hide Xray-ShadowSocks from VPN services 2024-05-09 13:31:58 +03:00
Mykola Baibuz
58cbba6bd2 Merge branch 'dev' into feature/connection-string 2024-05-09 13:21:45 +03:00
Vladyslav Miachkov
9faabe9e7d Filter settings fields to backup 2024-05-09 00:06:23 +03:00
Mykola Baibuz
79e02daddb Set protocol for default XRay connection strings 2024-05-03 21:20:34 +03:00
Mykola Baibuz
113758e381 Hide ShadowSocks Xray from VPN protocols 2024-05-03 16:46:11 +03:00
Mykola Baibuz
3bba823a2c Write ShadowSocks protocol name for ss:// strings 2024-05-02 23:53:08 +03:00
Mykola Baibuz
20c72ec97c Merge branch 'dev' into feature/connection-string 2024-04-30 20:56:22 +03:00
Mykola Baibuz
e5cdf9e397 Don't use h2 apln for XRay 2024-04-27 23:56:35 +03:00
Mykola Baibuz
126d2362bb Merge branch 'feature/connection-string' of https://github.com/amnezia-vpn/amnezia-client into feature/connection-string 2024-04-27 15:51:10 +03:00
Mykola Baibuz
79ef2cda93 Fix MacOS build 2024-04-27 15:50:52 +03:00
Mykola Baibuz
17585bc8c8 Merge branch 'dev' into feature/connection-string 2024-04-26 23:15:01 +03:00
Mykola Baibuz
030ed66b21 Fix cmake script 2024-04-26 15:07:22 +03:00
Mykola Baibuz
098577ec91 Add inbound traffic rule 2024-04-26 14:54:18 +03:00
Mykola Baibuz
4671b3a96f Connection string support for XRay protocol 2024-04-26 00:03:22 +03:00
86 changed files with 21452 additions and 667 deletions

2
client/3rd/QJsonStruct/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.user
build/

View File

@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.5)
project(QJsonStruct LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(BUILD_TESTING ON)
include(QJsonStruct.cmake)
find_package(Qt5 COMPONENTS Core REQUIRED)
if(BUILD_TESTING)
include(CTest)
add_subdirectory(test)
endif()

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Qv2ray Workgroup
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,190 @@
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QList>
#include <tuple>
enum class QJsonIOPathType
{
JSONIO_MODE_ARRAY,
JSONIO_MODE_OBJECT
};
typedef QPair<QString, QJsonIOPathType> QJsonIONodeType;
struct QJsonIOPath : QList<QJsonIONodeType>
{
template<typename type1, typename type2, typename... types>
QJsonIOPath(const type1 t1, const type2 t2, const types... ts)
{
AppendPath(t1);
AppendPath(t2);
(AppendPath(ts), ...);
}
void AppendPath(size_t index)
{
append({ QString::number(index), QJsonIOPathType::JSONIO_MODE_ARRAY });
}
void AppendPath(const QString &key)
{
append({ key, QJsonIOPathType::JSONIO_MODE_OBJECT });
}
template<typename t>
QJsonIOPath &operator<<(const t &str)
{
AppendPath(str);
return *this;
}
template<typename t>
QJsonIOPath &operator+=(const t &val)
{
AppendPath(val);
return *this;
}
QJsonIOPath &operator<<(const QJsonIOPath &other)
{
for (const auto &x : other)
this->append(x);
return *this;
}
template<typename t>
QJsonIOPath &operator<<(const t &val) const
{
auto _new = *this;
return _new << val;
}
template<typename t>
QJsonIOPath operator+(const t &val) const
{
auto _new = *this;
return _new << val;
}
QJsonIOPath operator+(const QJsonIOPath &other) const
{
auto _new = *this;
for (const auto &x : other)
_new.append(x);
return _new;
}
};
class QJsonIO
{
public:
const static inline QJsonValue Null = QJsonValue::Null;
const static inline QJsonValue Undefined = QJsonValue::Undefined;
template<typename current_key_type, typename... t_other_types>
static QJsonValue GetValue(const QJsonValue &parent, const current_key_type &current, const t_other_types &...other)
{
if constexpr (sizeof...(t_other_types) == 0)
if constexpr (std::is_integral_v<current_key_type>)
return parent.toArray()[current];
else
return parent.toObject()[current];
else if constexpr (std::is_integral_v<current_key_type>)
return GetValue(parent.toArray()[current], other...);
else
return GetValue(parent.toObject()[current], other...);
}
template<typename... key_types_t>
static QJsonValue GetValue(QJsonValue value, const std::tuple<key_types_t...> &keys, const QJsonValue &defaultValue = Undefined)
{
std::apply([&](auto &&...args) { ((value = value[args]), ...); }, keys);
return value.isUndefined() ? defaultValue : value;
}
template<typename parent_type, typename t_value_type, typename current_key_type, typename... t_other_key_types>
static void SetValue(parent_type &parent, const t_value_type &val, const current_key_type &current, const t_other_key_types &...other)
{
// If current parent is an array, increase its size to fit the "key"
if constexpr (std::is_integral_v<current_key_type>)
for (auto i = parent.size(); i <= current; i++)
parent.insert(i, {});
// If the t_other_key_types has nothing....
// Means we have reached the end of recursion.
if constexpr (sizeof...(t_other_key_types) == 0)
parent[current] = val;
else if constexpr (std::is_integral_v<typename std::tuple_element_t<0, std::tuple<t_other_key_types...>>>)
{
// Means we still have many keys
// So this element is an array.
auto _array = parent[current].toArray();
SetValue(_array, val, other...);
parent[current] = _array;
}
else
{
auto _object = parent[current].toObject();
SetValue(_object, val, other...);
parent[current] = _object;
}
}
static QJsonValue GetValue(const QJsonValue &parent, const QJsonIOPath &path, const QJsonValue &defaultValue = QJsonIO::Undefined)
{
QJsonValue val = parent;
for (const auto &[k, t] : path)
{
if (t == QJsonIOPathType::JSONIO_MODE_ARRAY)
val = val.toArray()[k.toInt()];
else
val = val.toObject()[k];
}
return val.isUndefined() ? defaultValue : val;
}
template<typename parent_type, typename value_type>
static void SetValue(parent_type &parent, const value_type &t, const QJsonIOPath &path)
{
QList<std::tuple<QString, QJsonIOPathType, QJsonValue>> _stack;
QJsonValue lastNode = parent;
for (const auto &[key, type] : path)
{
_stack.prepend({ key, type, lastNode });
if (type == QJsonIOPathType::JSONIO_MODE_ARRAY)
lastNode = lastNode.toArray().at(key.toInt());
else
lastNode = lastNode.toObject()[key];
}
lastNode = t;
for (const auto &[key, type, node] : _stack)
{
if (type == QJsonIOPathType::JSONIO_MODE_ARRAY)
{
const auto index = key.toInt();
auto nodeArray = node.toArray();
for (auto i = nodeArray.size(); i <= index; i++)
nodeArray.insert(i, {});
nodeArray[index] = lastNode;
lastNode = nodeArray;
}
else
{
auto nodeObject = node.toObject();
nodeObject[key] = lastNode;
lastNode = nodeObject;
}
}
if constexpr (std::is_same_v<parent_type, QJsonObject>)
parent = lastNode.toObject();
else if constexpr (std::is_same_v<parent_type, QJsonArray>)
parent = lastNode.toArray();
else
Q_UNREACHABLE();
}
};

View File

@@ -0,0 +1,5 @@
include_directories(${CMAKE_CURRENT_LIST_DIR})
set(QJSONSTRUCT_SOURCES
${CMAKE_CURRENT_LIST_DIR}/QJsonStruct.hpp
${CMAKE_CURRENT_LIST_DIR}/QJsonIO.hpp)

View File

@@ -0,0 +1,215 @@
#pragma once
#include "macroexpansion.hpp"
#ifndef _X
#include <QJsonArray>
#include <QJsonObject>
#include <QList>
#include <QVariant>
#endif
/// macro to define an operator==
#define ___JSONSTRUCT_DEFAULT_COMPARE_IMPL(x) (this->x == ___another___instance__.x) &&
#define JSONSTRUCT_COMPARE(CLASS, ...) \
bool operator==(const CLASS &___another___instance__) const \
{ \
return FOR_EACH(___JSONSTRUCT_DEFAULT_COMPARE_IMPL, __VA_ARGS__) true; \
}
// ============================================================================================
// Load JSON IMPL
#define ___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC_IMPL(name) name::loadJson(___json_object_);
#define ___DESERIALIZE_FROM_JSON_CONVERT_A_FUNC(name) ___DESERIALIZE_FROM_JSON_CONVERT_F_FUNC(name)
#define ___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC(...) FOREACH_CALL_FUNC_3(___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC_IMPL, __VA_ARGS__)
#define ___DESERIALIZE_FROM_JSON_CONVERT_F_FUNC(name) \
if (___json_object_.toObject().contains(#name)) \
{ \
JsonStructHelper::Deserialize(this->name, ___json_object_.toObject()[#name]); \
} \
else \
{ \
this->name = ___qjsonstruct_default_check.name; \
}
// ============================================================================================
// To JSON IMPL
#define ___SERIALIZE_TO_JSON_CONVERT_F_FUNC(name) \
if (!(___qjsonstruct_default_check.name == this->name)) \
{ \
___json_object_.insert(#name, JsonStructHelper::Serialize(name)); \
}
#define ___SERIALIZE_TO_JSON_CONVERT_A_FUNC(name) ___json_object_.insert(#name, JsonStructHelper::Serialize(name));
#define ___SERIALIZE_TO_JSON_CONVERT_B_FUNC_IMPL(name) JsonStructHelper::MergeJson(___json_object_, name::toJson());
#define ___SERIALIZE_TO_JSON_CONVERT_B_FUNC(...) FOREACH_CALL_FUNC_3(___SERIALIZE_TO_JSON_CONVERT_B_FUNC_IMPL, __VA_ARGS__)
// ============================================================================================
// Load JSON Wrapper
#define ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_A(...) FOREACH_CALL_FUNC_2(___DESERIALIZE_FROM_JSON_CONVERT_A_FUNC, __VA_ARGS__)
#define ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_F(...) FOREACH_CALL_FUNC_2(___DESERIALIZE_FROM_JSON_CONVERT_F_FUNC, __VA_ARGS__)
#define ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_B(...) FOREACH_CALL_FUNC_2(___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC, __VA_ARGS__)
#define ___DESERIALIZE_FROM_JSON_EXTRACT_B_F(name_option) ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_##name_option
// ============================================================================================
// To JSON Wrapper
#define ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_A(...) FOREACH_CALL_FUNC_2(___SERIALIZE_TO_JSON_CONVERT_A_FUNC, __VA_ARGS__)
#define ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_F(...) FOREACH_CALL_FUNC_2(___SERIALIZE_TO_JSON_CONVERT_F_FUNC, __VA_ARGS__)
#define ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_B(...) FOREACH_CALL_FUNC_2(___SERIALIZE_TO_JSON_CONVERT_B_FUNC, __VA_ARGS__)
#define ___SERIALIZE_TO_JSON_EXTRACT_B_F(name_option) ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_##name_option
// ============================================================================================
#define JSONSTRUCT_REGISTER_NOCOPYMOVE(___class_type_, ...) \
void loadJson(const QJsonValue &___json_object_) \
{ \
___class_type_ ___qjsonstruct_default_check; \
FOREACH_CALL_FUNC(___DESERIALIZE_FROM_JSON_EXTRACT_B_F, __VA_ARGS__); \
} \
[[nodiscard]] const QJsonObject toJson() const \
{ \
___class_type_ ___qjsonstruct_default_check; \
QJsonObject ___json_object_; \
FOREACH_CALL_FUNC(___SERIALIZE_TO_JSON_EXTRACT_B_F, __VA_ARGS__); \
return ___json_object_; \
}
#define JSONSTRUCT_REGISTER(___class_type_, ...) \
JSONSTRUCT_REGISTER_NOCOPYMOVE(___class_type_, __VA_ARGS__); \
[[nodiscard]] static auto fromJson(const QJsonValue &___json_object_) \
{ \
___class_type_ _t; \
_t.loadJson(___json_object_); \
return _t; \
}
#define ___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(type, convert_func) \
static void Deserialize(type &t, const QJsonValue &d) \
{ \
t = d.convert_func; \
}
class JsonStructHelper
{
public:
static void MergeJson(QJsonObject &mergeTo, const QJsonObject &mergeIn)
{
for (const auto &key : mergeIn.keys())
mergeTo[key] = mergeIn.value(key);
}
//
template<typename T>
static void Deserialize(T &t, const QJsonValue &d)
{
if constexpr (std::is_enum_v<T>)
t = (T) d.toInt();
else if constexpr (std::is_same_v<T, QJsonObject>)
t = d.toObject();
else if constexpr (std::is_same_v<T, QJsonArray>)
t = d.toArray();
else
t.loadJson(d);
}
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(QString, toString());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(QChar, toVariant().toChar());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(std::string, toString().toStdString());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(std::wstring, toString().toStdWString());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(bool, toBool());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(double, toDouble());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(float, toVariant().toFloat());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(int, toInt());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(long, toVariant().toLongLong());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(long long, toVariant().toLongLong());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(unsigned int, toVariant().toUInt());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(unsigned long, toVariant().toULongLong());
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(unsigned long long, toVariant().toULongLong());
template<typename T>
static void Deserialize(QList<T> &t, const QJsonValue &d)
{
t.clear();
for (const auto &val : d.toArray())
{
T data;
Deserialize(data, val);
t.push_back(data);
}
}
template<typename TKey, typename TValue>
static void Deserialize(QMap<TKey, TValue> &t, const QJsonValue &d)
{
t.clear();
const auto &jsonObject = d.toObject();
TKey keyVal;
TValue valueVal;
for (const auto &key : jsonObject.keys())
{
Deserialize(keyVal, key);
Deserialize(valueVal, jsonObject.value(key));
t.insert(keyVal, valueVal);
}
}
// =========================== Store Json Data ===========================
template<typename T>
static QJsonValue Serialize(const T &t)
{
if constexpr (std::is_enum_v<T>)
return (int) t;
else if constexpr (std::is_same_v<T, QJsonObject> || std::is_same_v<T, QJsonArray>)
return t;
else
return t.toJson();
}
#define pure_func(x) (x)
#define ___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(type) \
static QJsonValue Serialize(const type &t) \
{ \
return QJsonValue(t); \
}
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(int);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(bool);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(QJsonArray);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(QJsonObject);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(QString);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(long long);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(float);
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(double);
#define ___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(type, func) \
static QJsonValue Serialize(const type &t) \
{ \
return QJsonValue::fromVariant(func); \
}
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(std::string, QString::fromStdString(t))
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(std::wstring, QString::fromStdWString(t))
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(long, QVariant::fromValue<long>(t))
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(unsigned int, QVariant::fromValue<unsigned int>(t))
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(unsigned long, QVariant::fromValue<unsigned long>(t))
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(unsigned long long, QVariant::fromValue<unsigned long long>(t))
template<typename TValue>
static QJsonValue Serialize(const QMap<QString, TValue> &t)
{
QJsonObject mapObject;
for (const auto &key : t.keys())
{
auto valueVal = Serialize(t.value(key));
mapObject.insert(key, valueVal);
}
return mapObject;
}
template<typename T>
static QJsonValue Serialize(const QList<T> &t)
{
QJsonArray listObject;
for (const auto &item : t)
{
listObject.push_back(Serialize(item));
}
return listObject;
}
};

View File

@@ -0,0 +1,74 @@
#pragma once
#define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2)
#define CONCATENATE2(arg1, arg2) arg1##arg2
#define CONCATENATE(x, y) x##y
#define EXPAND(...) __VA_ARGS__
#define FOR_EACH_1(what, x, ...) what(x)
#define FOR_EACH_2(what, x, ...) what(x) EXPAND(FOR_EACH_1(what, __VA_ARGS__))
#define FOR_EACH_3(what, x, ...) what(x) EXPAND(FOR_EACH_2(what, __VA_ARGS__))
#define FOR_EACH_4(what, x, ...) what(x) EXPAND(FOR_EACH_3(what, __VA_ARGS__))
#define FOR_EACH_5(what, x, ...) what(x) EXPAND(FOR_EACH_4(what, __VA_ARGS__))
#define FOR_EACH_6(what, x, ...) what(x) EXPAND(FOR_EACH_5(what, __VA_ARGS__))
#define FOR_EACH_7(what, x, ...) what(x) EXPAND(FOR_EACH_6(what, __VA_ARGS__))
#define FOR_EACH_8(what, x, ...) what(x) EXPAND(FOR_EACH_7(what, __VA_ARGS__))
#define FOR_EACH_9(what, x, ...) what(x) EXPAND(FOR_EACH_8(what, __VA_ARGS__))
#define FOR_EACH_10(what, x, ...) what(x) EXPAND(FOR_EACH_9(what, __VA_ARGS__))
#define FOR_EACH_11(what, x, ...) what(x) EXPAND(FOR_EACH_10(what, __VA_ARGS__))
#define FOR_EACH_12(what, x, ...) what(x) EXPAND(FOR_EACH_11(what, __VA_ARGS__))
#define FOR_EACH_13(what, x, ...) what(x) EXPAND(FOR_EACH_12(what, __VA_ARGS__))
#define FOR_EACH_14(what, x, ...) what(x) EXPAND(FOR_EACH_13(what, __VA_ARGS__))
#define FOR_EACH_15(what, x, ...) what(x) EXPAND(FOR_EACH_14(what, __VA_ARGS__))
#define FOR_EACH_16(what, x, ...) what(x) EXPAND(FOR_EACH_15(what, __VA_ARGS__))
#define FOR_EACH_NARG(...) FOR_EACH_NARG_(__VA_ARGS__, FOR_EACH_RSEQ_N())
#define FOR_EACH_NARG_(...) EXPAND(FOR_EACH_ARG_N(__VA_ARGS__))
#define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, N, ...) N
#define FOR_EACH_RSEQ_N() 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
#define FOR_EACH_(N, what, ...) EXPAND(CONCATENATE(FOR_EACH_, N)(what, __VA_ARGS__))
#define FOR_EACH(what, ...) FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
#define FOREACH_CALL_FUNC(func, ...) FOR_EACH(func, __VA_ARGS__)
// Bad hack ==========================================================================================================================
#define _2X_FOR_EACH_1(what, x, ...) what(x)
#define _2X_FOR_EACH_2(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_1(what, __VA_ARGS__))
#define _2X_FOR_EACH_3(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_2(what, __VA_ARGS__))
#define _2X_FOR_EACH_4(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_3(what, __VA_ARGS__))
#define _2X_FOR_EACH_5(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_4(what, __VA_ARGS__))
#define _2X_FOR_EACH_6(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_5(what, __VA_ARGS__))
#define _2X_FOR_EACH_7(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_6(what, __VA_ARGS__))
#define _2X_FOR_EACH_8(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_7(what, __VA_ARGS__))
#define _2X_FOR_EACH_9(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_8(what, __VA_ARGS__))
#define _2X_FOR_EACH_10(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_9(what, __VA_ARGS__))
#define _2X_FOR_EACH_11(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_10(what, __VA_ARGS__))
#define _2X_FOR_EACH_12(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_11(what, __VA_ARGS__))
#define _2X_FOR_EACH_13(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_12(what, __VA_ARGS__))
#define _2X_FOR_EACH_14(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_13(what, __VA_ARGS__))
#define _2X_FOR_EACH_15(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_14(what, __VA_ARGS__))
#define _2X_FOR_EACH_16(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_15(what, __VA_ARGS__))
#define _2X_FOR_EACH_(N, what, ...) EXPAND(CONCATENATE(_2X_FOR_EACH_, N)(what, __VA_ARGS__))
#define _2X_FOR_EACH(what, ...) _2X_FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
#define FOREACH_CALL_FUNC_2(func, ...) _2X_FOR_EACH(func, __VA_ARGS__)
// Bad hack ==========================================================================================================================
#define _3X_FOR_EACH_1(what, x, ...) what(x)
#define _3X_FOR_EACH_2(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_1(what, __VA_ARGS__))
#define _3X_FOR_EACH_3(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_2(what, __VA_ARGS__))
#define _3X_FOR_EACH_4(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_3(what, __VA_ARGS__))
#define _3X_FOR_EACH_5(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_4(what, __VA_ARGS__))
#define _3X_FOR_EACH_6(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_5(what, __VA_ARGS__))
#define _3X_FOR_EACH_7(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_6(what, __VA_ARGS__))
#define _3X_FOR_EACH_8(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_7(what, __VA_ARGS__))
#define _3X_FOR_EACH_9(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_8(what, __VA_ARGS__))
#define _3X_FOR_EACH_10(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_9(what, __VA_ARGS__))
#define _3X_FOR_EACH_11(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_10(what, __VA_ARGS__))
#define _3X_FOR_EACH_12(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_11(what, __VA_ARGS__))
#define _3X_FOR_EACH_13(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_12(what, __VA_ARGS__))
#define _3X_FOR_EACH_14(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_13(what, __VA_ARGS__))
#define _3X_FOR_EACH_15(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_14(what, __VA_ARGS__))
#define _3X_FOR_EACH_16(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_15(what, __VA_ARGS__))
#define _3X_FOR_EACH_(N, what, ...) EXPAND(CONCATENATE(_3X_FOR_EACH_, N)(what, __VA_ARGS__))
#define _3X_FOR_EACH(what, ...) _3X_FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
#define FOREACH_CALL_FUNC_3(func, ...) _3X_FOR_EACH(func, __VA_ARGS__)

View File

@@ -0,0 +1,16 @@
function(QJSONSTRUCT_ADD_TEST TEST_NAME TEST_SOURCE)
add_executable(${TEST_NAME} ${TEST_SOURCE} catch.hpp ${QJSONSTRUCT_SOURCES})
target_include_directories(${TEST_NAME}
PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
target_link_libraries(
${TEST_NAME}
PRIVATE
Qt::Core
)
add_test(NAME QJSONSTRUCT_TEST_${TEST_NAME} COMMAND $<TARGET_FILE:${TEST_NAME}> -s)
endfunction()
QJSONSTRUCT_ADD_TEST(serialization serialize/main.cpp)
#QJSONSTRUCT_ADD_TEST(serialize_strings serialize/strings.cpp)

View File

@@ -0,0 +1,45 @@
#pragma once
#include "QJsonStruct.hpp"
#ifndef _X
#include <QList>
#include <QString>
#include <QStringList>
#endif
struct BaseStruct
{
QString baseStr;
int o;
JSONSTRUCT_REGISTER(BaseStruct, F(baseStr, o))
};
struct BaseStruct2
{
QString baseStr2;
int o2;
JSONSTRUCT_REGISTER(BaseStruct, F(baseStr2, o2))
};
struct TestInnerStruct
: BaseStruct
, BaseStruct2
{
QJsonObject jobj;
QJsonArray jarray;
QString str;
JSONSTRUCT_REGISTER(TestInnerStruct, B(BaseStruct, BaseStruct2), F(str, jobj, jarray))
};
struct JsonIOTest
{
QString str;
QList<int> listOfNumber;
QList<bool> listOfBool;
QList<QString> listOfString;
QList<QList<QString>> listOfListOfString;
QMap<QString, QString> map;
TestInnerStruct inner;
JSONSTRUCT_REGISTER(JsonIOTest, F(str, listOfNumber, listOfBool, listOfString, listOfListOfString, map, inner));
JsonIOTest(){};
};

View File

@@ -0,0 +1,19 @@
#pragma once
#include "QJsonStruct.hpp"
struct SubData
{
QString subString;
JSONSTRUCT_REGISTER_TOJSON(subString)
};
struct ToJsonOnlyData
{
QString x;
int y;
int z;
QList<int> ints;
SubData sub;
QMap<QString, SubData> subs;
JSONSTRUCT_REGISTER_TOJSON(x, y, z, sub, ints, subs)
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
#include "QJsonIO.hpp"
#include "QJsonStruct.hpp"
#include "TestIO.hpp"
#include "TestOut.hpp"
#include <QCoreApplication>
#include <QJsonDocument>
#include <iostream>
int main(int argc, char *argv[])
{
Q_UNUSED(argc)
Q_UNUSED(argv)
{
ToJsonOnlyData data;
data.x = "1string";
data.y = 2;
data.ints << 0;
data.ints << 100;
data.ints << 900;
data.sub.subString = "subs";
data.subs["subs-1"] = { "subs1-data" };
data.subs["subs-2"] = { "subs2-data" };
data.subs["subs-3"] = { "subs3-data" };
data.z = 3;
auto x = data.toJson();
std::cout << QJsonDocument(x).toJson().toStdString() << std::endl;
}
//
{
auto f = JsonIOTest::fromJson( //
QJsonObject{
{ "inner", QJsonObject{ { "str", "innerString" }, //
{ "jobj", QJsonObject{ { "key", "value" } } }, //
{ "jarray", QJsonArray{ "array0", "array1", "array2" } }, //
{ "baseStr", "baseInnerString" } } }, //
{ "str", "data1" }, //
{ "map", QJsonObject{ { "mapStr", "mapData" } } }, //
{ "listOfString", QJsonArray{ "1", "2", "3", "4", "5" } }, //
{ "listOfNumber", QJsonArray{ 1, 2, 3, 4, 5 } }, //
{ "listOfBool", QJsonArray{ true, false, false, true, true } }, //
{ "listOfListOfString", QJsonArray{ QJsonArray{ "1" }, //
QJsonArray{ "1", "2" }, //
QJsonArray{ "1", "2", "3" }, //
QJsonArray{ "1", "2", "3", "4" }, //
QJsonArray{ "1", "2", "3", "4", "5" } } }, //
});
auto x = f.toJson();
std::cout << QJsonDocument(x).toJson().toStdString() << std::endl;
}
{
QJsonObject obj{
{ "inner", QJsonObject{ { "str", "innerString" }, { "baseStr", "baseInnerString" } } }, //
{ "str", "data1" }, //
{ "map", QJsonObject{ { "mapStr", "mapData" } } }, //
{ "listOfString", QJsonArray{ "1", "2", "3", "4", "5" } }, //
{ "listOfNumber", QJsonArray{ 1, 2, 3, 4, 5 } }, //
{ "listOfBool", QJsonArray{ true, false, false, true, true } }, //
{ "listOfListOfString", QJsonArray{ QJsonArray{ "1" }, //
QJsonArray{ "1", "2" }, //
QJsonArray{ "1", "2", "3" }, //
QJsonArray{ "1", "2", "3", "4" }, //
QJsonArray{ "1", "2", "3", "4", "5" } } }, //
};
auto y = QJsonIO::GetValue(obj, std::tuple{ "listOfListOfString", 2 });
y.toObject();
}
return 0;
}

View File

@@ -0,0 +1,181 @@
#include "QJsonStruct.hpp"
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
const static inline auto INT_TEST_MAX = std::numeric_limits<int>::max() - 1;
const static inline auto INT_TEST_MIN = -(std::numeric_limits<int>::min() + 1);
#define SINGLE_ELEMENT_CLASS_DECL(CLASS, TYPE, field, defaultvalue, existance) \
class CLASS \
{ \
public: \
TYPE field = defaultvalue; \
JSONSTRUCT_REGISTER(CLASS, existance(field)); \
};
// SINGLE_ELEMENT_REQUIRE( CLASS_NAME , TYPE , FIELD , DEFAULT_VALUE , SET VALUE , CHECK VALUE )
#define SINGLE_ELEMENT_REQUIRE(CLASS, TYPE, field, defaultvalue, value, checkvalue, existance) \
SINGLE_ELEMENT_CLASS_DECL(CLASS, TYPE, field, defaultvalue, existance); \
CLASS CLASS##_class; \
CLASS##_class.field = value; \
REQUIRE(CLASS##_class.toJson()[#field] == checkvalue);
using namespace std;
SCENARIO("Test Serialization", "[Serialize]")
{
GIVEN("Single Element")
{
const static QList<QString> defaultList{ "entry 1", "entry 2" };
const static QMap<QString, QString> defaultMap{ { "key1", "value1" }, { "key2", "value2" } };
typedef QMap<QString, QString> QStringQStringMap;
WHEN("Serialize a single element")
{
const static QStringQStringMap setValueMap{ { "newkey1", "newvalue1" } };
const static QJsonObject setValueJson{ { "newkey1", QJsonValue{ "newvalue1" } } };
SINGLE_ELEMENT_REQUIRE(QStringTest_Empty, QString, a, "empty", "", "", F);
SINGLE_ELEMENT_REQUIRE(QStringTest, QString, a, "empty", "Some QString", "Some QString", F);
SINGLE_ELEMENT_REQUIRE(QStringTest_WithQoutes, QString, a, "empty", "\"", "\"", F);
SINGLE_ELEMENT_REQUIRE(QStringTest_zint, int, a, -10, 0, 0, F);
SINGLE_ELEMENT_REQUIRE(QStringTest_nint, int, a, -10, 1, 1, F);
SINGLE_ELEMENT_REQUIRE(QStringTest_pint, int, a, -10, -1, -1, F);
SINGLE_ELEMENT_REQUIRE(QStringTest_pmint, int, a, -1, INT_TEST_MAX, INT_TEST_MAX, F);
SINGLE_ELEMENT_REQUIRE(QStringTest_zmint, int, a, -1, INT_TEST_MIN, INT_TEST_MIN, F);
SINGLE_ELEMENT_REQUIRE(QStringTest_zuint, uint, a, -10, 0, 0, F);
SINGLE_ELEMENT_REQUIRE(QStringTest_puint, uint, a, -10, 1, 1, F);
SINGLE_ELEMENT_REQUIRE(BoolTest_True, bool, a, false, true, true, F);
SINGLE_ELEMENT_REQUIRE(BoolTest_False, bool, a, true, false, false, F);
SINGLE_ELEMENT_REQUIRE(StdStringTest, string, a, "def", "std::string _test", "std::string _test", F);
SINGLE_ELEMENT_REQUIRE(QListTest, QList<QString>, a, defaultList, { "newEntry" }, QJsonArray{ "newEntry" }, F);
SINGLE_ELEMENT_REQUIRE(QMapTest, QStringQStringMap, a, defaultMap, {}, QJsonObject{}, F);
SINGLE_ELEMENT_REQUIRE(QMapValueTest, QStringQStringMap, a, defaultMap, setValueMap, setValueJson, F);
}
WHEN("Serialize a default value")
{
SINGLE_ELEMENT_REQUIRE(DefaultQString, QString, a, "defaultvalue", "defaultvalue", QJsonValue::Undefined, F);
SINGLE_ELEMENT_REQUIRE(DefaultInteger, int, a, 12345, 12345, QJsonValue::Undefined, F);
SINGLE_ELEMENT_REQUIRE(DefaultList, QList<QString>, a, defaultList, defaultList, QJsonValue::Undefined, F);
SINGLE_ELEMENT_REQUIRE(DefaultMap, QStringQStringMap, a, defaultMap, defaultMap, QJsonValue::Undefined, F);
}
WHEN("Serialize a force existance default value")
{
const static QJsonArray defaultListJson{ "entry 1", "entry 2" };
const static QJsonObject defaultMapJson{ { "key1", "value1" }, { "key2", "value2" } };
SINGLE_ELEMENT_REQUIRE(DefaultQString, QString, a, "defaultvalue", "defaultvalue", "defaultvalue", A);
SINGLE_ELEMENT_REQUIRE(DefaultInteger, int, a, 12345, 12345, 12345, A);
SINGLE_ELEMENT_REQUIRE(DefaultList, QList<QString>, a, defaultList, defaultList, defaultListJson, A);
SINGLE_ELEMENT_REQUIRE(DefaultMap, QStringQStringMap, a, defaultMap, defaultMap, defaultMapJson, A);
}
}
GIVEN("Multiple Simple Elements")
{
WHEN("Can Omit Default Value")
{
class MultipleNonDefaultElementTestClass
{
public:
QString astring;
int integer = 0;
double adouble = 0.0;
QList<QString> myList;
JSONSTRUCT_REGISTER(MultipleNonDefaultElementTestClass, F(astring, integer, adouble, myList))
};
MultipleNonDefaultElementTestClass instance;
const auto json = instance.toJson();
REQUIRE(json["astring"] == QJsonValue::Undefined);
REQUIRE(json["integer"] == QJsonValue::Undefined);
REQUIRE(json["adouble"] == QJsonValue::Undefined);
REQUIRE(json["myList"] == QJsonValue::Undefined);
}
WHEN("Forcing Existance")
{
class MultipleNonDefaultExistanceElementTestClass
{
public:
QString astring;
int integer = 0;
double adouble = 0.0;
QList<QString> myList;
JSONSTRUCT_REGISTER(MultipleNonDefaultExistanceElementTestClass, A(astring, integer, adouble, myList))
};
MultipleNonDefaultExistanceElementTestClass instance;
const auto json = instance.toJson();
REQUIRE(json["astring"] == "");
REQUIRE(json["integer"] == 0);
REQUIRE(json["adouble"] == 0.0);
REQUIRE(json["myList"] == QJsonArray{});
}
}
GIVEN("Nested Elements")
{
WHEN("Can Omit Default Value")
{
class Parent
{
class NestedChild
{
class NestedChild2
{
public:
int childChildInt = 13579;
JSONSTRUCT_COMPARE(NestedChild2, childChildInt)
JSONSTRUCT_REGISTER(NestedChild2, F(childChildInt))
};
public:
int childInt = 54321;
QString childQString = "A QString";
NestedChild2 anotherChild;
JSONSTRUCT_COMPARE(NestedChild, childInt, childQString, anotherChild)
JSONSTRUCT_REGISTER(NestedChild, F(childInt, childQString, anotherChild))
};
public:
int parentInt = 12345;
NestedChild child;
JSONSTRUCT_REGISTER(Parent, F(parentInt, child))
};
WHEN("Omitted whole child element")
{
Parent parent;
const auto json = parent.toJson();
REQUIRE(json["parentInt"] == QJsonValue::Undefined);
REQUIRE(json["child"] == QJsonValue::Undefined);
}
WHEN("Omitted one element in the child")
{
const auto childJson = QJsonObject{ { "childInt", 1314 } };
Parent parent;
parent.child.childInt = 1314;
const auto json = parent.toJson();
REQUIRE(json["parentInt"] == QJsonValue::Undefined);
REQUIRE(json["child"] == childJson);
REQUIRE(json["child"]["childInt"] == 1314);
REQUIRE(json["child"]["childQString"] == QJsonValue::Undefined);
REQUIRE(json["child"]["child"]["anotherChild"] == QJsonValue::Undefined);
}
WHEN("Omitted one element in the child child")
{
Parent parent;
parent.child.childInt = 1314;
parent.child.anotherChild.childChildInt = 97531;
const auto json = parent.toJson();
REQUIRE(json["parentInt"] == QJsonValue::Undefined);
REQUIRE(json["child"]["childInt"] == 1314);
REQUIRE(json["child"]["childQString"] == QJsonValue::Undefined);
const QJsonObject childChild{ { "childChildInt", 97531 } };
REQUIRE(json["child"]["anotherChild"] == childChild);
REQUIRE(json["child"]["anotherChild"]["childChildInt"] == 97531);
}
}
}
}

View File

@@ -134,6 +134,8 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/version.h
${CMAKE_CURRENT_LIST_DIR}/core/sshclient.h
${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.h
${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
)
# Mozilla headres
@@ -176,6 +178,14 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp
${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp
${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/outbound.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/inbound.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/ss.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/ssd.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vless.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
)
# Mozilla sources

View File

@@ -363,10 +363,16 @@ void AmneziaApplication::initControllers()
new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings));
m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get());
connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](const QString &errorMessage) {
connect(m_connectionController.get(), qOverload<const QString &>(&ConnectionController::connectionErrorOccurred), this, [this](const QString &errorMessage) {
emit m_pageController->showErrorMessage(errorMessage);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_connectionController.get(), qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred), this, [this](ErrorCode errorCode) {
emit m_pageController->showErrorMessage(errorCode);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_connectionController.get(), &ConnectionController::connectButtonClicked, m_connectionController.get(),
&ConnectionController::toggleConnection, Qt::QueuedConnection);

View File

@@ -63,6 +63,8 @@ QVector<amnezia::Proto> ContainerProps::protocolsForContainer(amnezia::DockerCon
case DockerContainer::Xray: return { Proto::Xray };
case DockerContainer::SSXray: return { Proto::SSXray };
case DockerContainer::Dns: return { Proto::Dns };
case DockerContainer::Sftp: return { Proto::Sftp };
@@ -92,6 +94,7 @@ QMap<DockerContainer, QString> ContainerProps::containerHumanNames()
{ DockerContainer::Awg, "AmneziaWG" },
{ DockerContainer::Xray, "XRay" },
{ DockerContainer::Ipsec, QObject::tr("IPsec") },
{ DockerContainer::SSXray, "ShadowSocks"},
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
{ DockerContainer::Dns, QObject::tr("Amnezia DNS") },
@@ -257,6 +260,7 @@ Proto ContainerProps::defaultProtocol(DockerContainer c)
case DockerContainer::Awg: return Proto::Awg;
case DockerContainer::Xray: return Proto::Xray;
case DockerContainer::Ipsec: return Proto::Ikev2;
case DockerContainer::SSXray: return Proto::SSXray;
case DockerContainer::TorWebSite: return Proto::TorWebSite;
case DockerContainer::Dns: return Proto::Dns;

View File

@@ -23,6 +23,7 @@ namespace amnezia
ShadowSocks,
Ipsec,
Xray,
SSXray,
// non-vpn
TorWebSite,

View File

@@ -8,7 +8,6 @@
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
#include "version.h"
#include "core/errorstrings.h"
namespace
{
@@ -110,7 +109,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (ba.isEmpty()) {
emit errorOccurred(errorString(ErrorCode::ApiConfigEmptyError));
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return;
}
@@ -135,14 +134,14 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
} else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
emit errorOccurred(errorString(ErrorCode::ApiConfigTimeoutError));
emit errorOccurred(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
emit errorOccurred(errorString(ErrorCode::ApiConfigDownloadError));
emit errorOccurred(ErrorCode::ApiConfigDownloadError);
}
}
@@ -153,7 +152,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
[this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; });
connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList<QSslError> &errors) {
qDebug().noquote() << errors;
emit errorOccurred(errorString(ErrorCode::ApiConfigSslError));
emit errorOccurred(ErrorCode::ApiConfigSslError);
});
}
}

View File

@@ -20,7 +20,7 @@ public slots:
void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig);
signals:
void errorOccurred(const QString &errorMessage);
void errorOccurred(ErrorCode errorCode);
void configUpdated(const bool updateConfig, const QJsonObject &config, const int serverIndex);
private:

View File

@@ -416,11 +416,16 @@ ErrorCode ServerController::prepareHostWorker(const ServerCredentials &credentia
ErrorCode ServerController::buildContainerWorker(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config)
{
ErrorCode e = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),
amnezia::server::getDockerfileFolder(container) + "/Dockerfile");
QString dockerFilePath = amnezia::server::getDockerfileFolder(container) + "/Dockerfile";
QString scriptString = QString("sudo rm %1").arg(dockerFilePath);
ErrorCode errorCode = runScript(credentials, replaceVars(scriptString, genVarsForScript(credentials, container)));
if (errorCode)
return errorCode;
if (e)
return e;
errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),dockerFilePath);
if (errorCode)
return errorCode;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
@@ -428,13 +433,13 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
return ErrorCode::NoError;
};
e = runScript(credentials,
errorCode = runScript(credentials,
replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)),
cbReadStdOut);
if (e)
return e;
if (errorCode)
return errorCode;
return e;
return errorCode;
}
ErrorCode ServerController::runContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config)
@@ -686,6 +691,30 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
for (auto &port : fixedPorts) {
script = script.append("|:%1").arg(port);
}
if (transportProto == "tcpandudp") {
QString tcpProtoScript = script;
QString udpProtoScript = script;
tcpProtoScript.append("' | grep -i tcp");
udpProtoScript.append("' | grep -i udp");
tcpProtoScript.append(" | grep LISTEN");
ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
errorCode = runScript(credentials, replaceVars(udpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (!stdOut.isEmpty()) {
return ErrorCode::ServerPortAlreadyAllocatedError;
}
return ErrorCode::NoError;
}
script = script.append("' | grep -i %1").arg(transportProto);
if (transportProto == "tcp") {

View File

@@ -24,6 +24,7 @@ QScopedPointer<ConfiguratorBase> VpnConfigurationsController::createConfigurator
case Proto::Awg: return QScopedPointer<ConfiguratorBase>(new AwgConfigurator(m_settings, m_serverController));
case Proto::Ikev2: return QScopedPointer<ConfiguratorBase>(new Ikev2Configurator(m_settings, m_serverController));
case Proto::Xray: return QScopedPointer<ConfiguratorBase>(new XrayConfigurator(m_settings, m_serverController));
case Proto::SSXray: return QScopedPointer<ConfiguratorBase>(new XrayConfigurator(m_settings, m_serverController));
default: return QScopedPointer<ConfiguratorBase>();
}
}

View File

@@ -36,7 +36,11 @@ namespace amnezia
}
};
enum ErrorCode {
namespace error_code_ns
{
Q_NAMESPACE
// TODO: change to enum class
enum ErrorCode {
// General error codes
NoError = 0,
UnknownError = 100,
@@ -75,7 +79,7 @@ namespace amnezia
AmneziaServiceConnectionFailed = 603,
ExecutableMissing = 604,
XrayExecutableMissing = 605,
Tun2SockExecutableMissing = 606,
Tun2SockExecutableMissing = 606,
// VPN errors
OpenVpnAdaptersInUseError = 700,
@@ -110,7 +114,11 @@ namespace amnezia
UnspecifiedError = 1203,
FatalError = 1204,
AbortError = 1205
};
};
Q_ENUM_NS(ErrorCode)
}
using ErrorCode = error_code_ns::ErrorCode;
} // namespace amnezia

View File

@@ -8,68 +8,68 @@ QString errorString(ErrorCode code) {
switch (code) {
// General error codes
case(NoError): errorMessage = QObject::tr("No error"); break;
case(UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
case(NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
case(AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break;
case(ErrorCode::NoError): errorMessage = QObject::tr("No error"); break;
case(ErrorCode::UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
case(ErrorCode::NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
case(ErrorCode::AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break;
// Server errors
case(ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
case(ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
case(ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
case(ErrorCode::ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
case(ErrorCode::ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
case(ErrorCode::ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ErrorCode::ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
// Libssh errors
case(SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
case(SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
case(SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
case(SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
case(SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
case(ErrorCode::SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
case(ErrorCode::SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
case(ErrorCode::SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
case(ErrorCode::SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(ErrorCode::SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
// Ssh scp errors
case(SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
case(ErrorCode::SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
// Local errors
case (OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
case (OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
case (ErrorCode::OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
case (ErrorCode::OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
// Distro errors
case (OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
case (CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
case (AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ErrorCode::ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
case (ErrorCode::CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
// VPN errors
case (OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (ErrorCode::OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
// Android errors
case (AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
// Api errors
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case (ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break;
case (ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
case (ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
case (ErrorCode::ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ErrorCode::ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case (ErrorCode::ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break;
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
// QFile errors
case(OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(ErrorCode::PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(ErrorCode::UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(InternalError):
case(ErrorCode::InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;
}

View File

@@ -0,0 +1,38 @@
#include <QString>
#include <QJsonObject>
#include <QList>
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "transfer.h"
#include "serialization.h"
namespace amnezia::serialization::inbounds
{
//"inbounds": [
// {
// "listen": "127.0.0.1",
// "port": 10808,
// "protocol": "socks",
// "settings": {
// "udp": true
// }
// }
//],
const static QString listen = "127.0.0.1";
const static int port = 10808;
const static QString protocol = "socks";
QJsonObject GenerateInboundEntry()
{
QJsonObject root;
QJsonIO::SetValue(root, listen, "listen");
QJsonIO::SetValue(root, port, "port");
QJsonIO::SetValue(root, protocol, "protocol");
QJsonIO::SetValue(root, true, "settings", "udp");
return root;
}
} // namespace amnezia::serialization::inbounds

View File

@@ -0,0 +1,122 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
#include <QString>
#include <QJsonObject>
#include <QList>
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "transfer.h"
#include "serialization.h"
namespace amnezia::serialization::outbounds
{
QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect)
{
QJsonObject root;
JADD(domainStrategy, redirect)
return root;
}
QJsonObject GenerateBlackHoleOUT(bool useHTTP)
{
QJsonObject root;
QJsonObject resp;
resp.insert("type", useHTTP ? "http" : "none");
root.insert("response", resp);
return root;
}
QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password)
{
QJsonObject root;
JADD(address, port, method, password)
return root;
}
QJsonObject GenerateShadowSocksOUT(const QList<ShadowSocksServerObject> &_servers)
{
QJsonObject root;
QJsonArray x;
for (const auto &server : _servers)
{
x.append(GenerateShadowSocksServerOUT(server.address, server.port, server.method, server.password));
}
root.insert("servers", x);
return root;
}
QJsonObject GenerateHTTPSOCKSOut(const QString &addr, int port, bool useAuth, const QString &username, const QString &password)
{
QJsonObject root;
QJsonIO::SetValue(root, addr, "servers", 0, "address");
QJsonIO::SetValue(root, port, "servers", 0, "port");
if (useAuth)
{
QJsonIO::SetValue(root, username, "servers", 0, "users", 0, "user");
QJsonIO::SetValue(root, password, "servers", 0, "users", 0, "pass");
}
return root;
}
QJsonObject GenerateOutboundEntry(const QString &tag, const QString &protocol, const QJsonObject &settings, const QJsonObject &streamSettings,
const QJsonObject &mux, const QString &sendThrough)
{
QJsonObject root;
JADD(sendThrough, protocol, settings, tag, streamSettings, mux)
return root;
}
QJsonObject GenerateTrojanOUT(const QList<TrojanObject> &_servers)
{
QJsonObject root;
QJsonArray x;
for (const auto &server : _servers)
{
x.append(GenerateTrojanServerOUT(server.address, server.port, server.password));
}
root.insert("servers", x);
return root;
}
QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password)
{
QJsonObject root;
JADD(address, port, password)
return root;
}
} // namespace amnezia::serialization::outbounds

View File

@@ -0,0 +1,66 @@
#ifndef SERIALIZATION_H
#define SERIALIZATION_H
#include <QJsonObject>
#include "transfer.h"
namespace amnezia::serialization
{
namespace vmess
{
QJsonObject Deserialize(const QString &vmess, QString *alias, QString *errMessage);
const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias);
} // namespace vmess
namespace vmess_new
{
QJsonObject Deserialize(const QString &vmess, QString *alias, QString *errMessage);
const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias);
} // namespace vmess_new
namespace vless
{
QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage);
} // namespace vless
namespace ss
{
QJsonObject Deserialize(const QString &ss, QString *alias, QString *errMessage);
const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool isSip002);
} // namespace ss
namespace ssd
{
QList<std::pair<QString, QJsonObject>> Deserialize(const QString &uri, QString *groupName, QStringList *logList);
} // namespace ssd
namespace trojan
{
QJsonObject Deserialize(const QString &trojan, QString *alias, QString *errMessage);
const QString Serialize(const TrojanObject &server, const QString &alias);
} // namespace trojan
namespace outbounds
{
QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect);
QJsonObject GenerateBlackHoleOUT(bool useHTTP);
QJsonObject GenerateShadowSocksOUT(const QList<ShadowSocksServerObject> &servers);
QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password);
QJsonObject GenerateHTTPSOCKSOut(const QString &address, int port, bool useAuth, const QString &username, const QString &password);
QJsonObject GenerateTrojanOUT(const QList<TrojanObject> &servers);
QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password);
QJsonObject GenerateOutboundEntry(const QString &tag, //
const QString &protocol, //
const QJsonObject &settings, //
const QJsonObject &streamSettings, //
const QJsonObject &mux = {}, //
const QString &sendThrough = "0.0.0.0");
} // namespace outbounds
namespace inbounds
{
QJsonObject GenerateInboundEntry();
}
}
#endif // SERIALIZATION_H

View File

@@ -0,0 +1,142 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#include "utilities.h"
#include "serialization.h"
#define OUTBOUND_TAG_PROXY "PROXY"
#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__)
namespace amnezia::serialization::ss
{
QJsonObject Deserialize(const QString &ssUri, QString *alias, QString *errMessage)
{
ShadowSocksServerObject server;
QString d_name;
// auto ssUri = _ssUri.toStdString();
if (ssUri.length() < 5)
{
*errMessage = QObject::tr("SS URI is too short");
}
auto uri = ssUri.mid(5);
auto hashPos = uri.lastIndexOf("#");
if (hashPos >= 0)
{
// Get the name/remark
d_name = uri.mid(uri.lastIndexOf("#") + 1);
uri.truncate(hashPos);
}
auto atPos = uri.indexOf('@');
if (atPos < 0)
{
// Old URI scheme
QString decoded = QByteArray::fromBase64(uri.toUtf8(), QByteArray::Base64Option::OmitTrailingEquals);
auto colonPos = decoded.indexOf(':');
if (colonPos < 0)
{
*errMessage = QObject::tr("Can't find the colon separator between method and password");
}
server.method = decoded.left(colonPos);
decoded.remove(0, colonPos + 1);
atPos = decoded.lastIndexOf('@');
if (atPos < 0)
{
*errMessage = QObject::tr("Can't find the at separator between password and hostname");
}
server.password = decoded.mid(0, atPos);
decoded.remove(0, atPos + 1);
colonPos = decoded.lastIndexOf(':');
if (colonPos < 0)
{
*errMessage = QObject::tr("Can't find the colon separator between hostname and port");
}
server.address = decoded.mid(0, colonPos);
server.port = decoded.mid(colonPos + 1).toInt();
}
else
{
// SIP002 URI scheme
auto x = QUrl::fromUserInput(uri);
server.address = x.host();
server.port = x.port();
const auto userInfo = Utils::SafeBase64Decode(x.userName());
const auto userInfoSp = userInfo.indexOf(':');
if (userInfoSp < 0)
{
*errMessage = QObject::tr("Can't find the colon separator between method and password");
return QJsonObject{};
}
const auto method = userInfo.mid(0, userInfoSp);
server.method = method;
server.password = userInfo.mid(userInfoSp + 1);
}
d_name = QUrl::fromPercentEncoding(d_name.toUtf8());
QJsonObject root;
QJsonArray outbounds;
outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ server }), {}));
JADD(outbounds)
QJsonObject inbound = inbounds::GenerateInboundEntry();
root["inbounds"] = QJsonArray{ inbound };
*alias = alias->isEmpty() ? d_name : *alias + "_" + d_name;
return root;
}
const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool)
{
QUrl url;
const auto plainUserInfo = server.method + ":" + server.password;
const auto userinfo = plainUserInfo.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
url.setUserInfo(userinfo);
url.setScheme("ss");
url.setHost(server.address);
url.setPort(server.port);
url.setFragment(alias);
return url.toString(QUrl::ComponentFormattingOption::FullyEncoded);
}
} // namespace amnezia::serialization::ss

View File

@@ -0,0 +1,244 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
/**
* A Naive SSD Decoder for Qv2ray
*
* @author DuckSoft <realducksoft@gmail.com>
* @copyright Licensed under GPLv3.
*/
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#include "utilities.h"
#include "serialization.h"
const inline QString QV2RAY_SSD_DEFAULT_NAME_PATTERN = "%1 - %2 (rate %3)";
#define OUTBOUND_TAG_PROXY "PROXY"
namespace amnezia::serialization::ssd
{
// These below are super strict checking schemes, but necessary.
#define MUST_EXIST(fieldName) \
if (!obj.contains((fieldName)) || obj[(fieldName)].isUndefined() || obj[(fieldName)].isNull()) \
{ \
*logList << QObject::tr("Invalid ssd link: json: field %1 must exist").arg(fieldName); \
return {}; \
}
#define MUST_PORT(fieldName) \
MUST_EXIST(fieldName); \
if (int value = obj[(fieldName)].toInt(-1); value < 0 || value > 65535) \
{ \
*logList << QObject::tr("Invalid ssd link: json: field %1 must be valid port number"); \
return {}; \
}
#define MUST_STRING(fieldName) \
MUST_EXIST(fieldName); \
if (!obj[(fieldName)].isString()) \
{ \
*logList << QObject::tr("Invalid ssd link: json: field %1 must be of type 'string'").arg(fieldName); \
return {}; \
}
#define MUST_ARRAY(fieldName) \
MUST_EXIST(fieldName); \
if (!obj[(fieldName)].isArray()) \
{ \
*logList << QObject::tr("Invalid ssd link: json: field %1 must be an array").arg(fieldName); \
return {}; \
}
#define SERVER_SHOULD_BE_OBJECT(server) \
if (!server.isObject()) \
{ \
*logList << QObject::tr("Skipping invalid ssd server: server must be an object"); \
continue; \
}
#define SHOULD_EXIST(fieldName) \
if (serverObject[(fieldName)].isUndefined()) \
{ \
*logList << QObject::tr("Skipping invalid ssd server: missing required field %1").arg(fieldName); \
continue; \
}
#define SHOULD_STRING(fieldName) \
SHOULD_EXIST(fieldName); \
if (!serverObject[(fieldName)].isString()) \
{ \
*logList << QObject::tr("Skipping invalid ssd server: field %1 should be of type 'string'").arg(fieldName); \
continue; \
}
QList<std::pair<QString, QJsonObject>> Deserialize(const QString &uri, QString *groupName, QStringList *logList)
{
// ssd links should begin with "ssd://"
if (!uri.startsWith("ssd://"))
{
*logList << QObject::tr("Invalid ssd link: should begin with ssd://");
return {};
}
// decode base64
const auto ssdURIBody = uri.mid(6, uri.length() - 6); //(&uri, 6, uri.length() - 6);
const auto decodedJSON = Utils::SafeBase64Decode(ssdURIBody).toUtf8();
if (decodedJSON.length() == 0)
{
*logList << QObject::tr("Invalid ssd link: base64 parse failed");
return {};
}
const auto decodeError = Utils::VerifyJsonString(decodedJSON);
if (!decodeError.isEmpty())
{
*logList << QObject::tr("Invalid ssd link: json parse failed");
return {};
}
// casting to object
const auto obj = Utils::JsonFromString(decodedJSON);
// obj.airport
MUST_STRING("airport");
*groupName = obj["airport"].toString();
// obj.port
MUST_PORT("port");
const int port = obj["port"].toInt();
// obj.encryption
MUST_STRING("encryption");
const auto encryption = obj["encryption"].toString();
// check: rc4-md5 is not supported by v2ray-core
// TODO: more checks, including all algorithms
if (encryption.toLower() == "rc4-md5")
{
*logList << QObject::tr("Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core");
return {};
}
// obj.password
MUST_STRING("password");
const auto password = obj["password"].toString();
// obj.servers
MUST_ARRAY("servers");
//
QList<std::pair<QString, QJsonObject>> serverList;
//
// iterate through the servers
for (const auto &server : obj["servers"].toArray())
{
SERVER_SHOULD_BE_OBJECT(server);
const auto serverObject = server.toObject();
ShadowSocksServerObject ssObject;
// encryption
ssObject.method = encryption;
// password
ssObject.password = password;
// address :-> "server"
SHOULD_STRING("server");
const auto serverAddress = serverObject["server"].toString();
ssObject.address = serverAddress;
// port selection:
// normal: use global settings
// overriding: use current config
if (serverObject["port"].isUndefined())
{
ssObject.port = port;
}
else if (auto currPort = serverObject["port"].toInt(-1); (currPort >= 0 && currPort <= 65535))
{
ssObject.port = currPort;
}
else
{
ssObject.port = port;
}
// name decision:
// untitled: using server:port as name
// entitled: using given name
QString nodeName;
if (serverObject["remarks"].isUndefined())
{
nodeName = QString("%1:%2").arg(ssObject.address).arg(ssObject.port);
}
else if (serverObject["remarks"].isString())
{
nodeName = serverObject["remarks"].toString();
}
else
{
nodeName = QString("%1:%2").arg(ssObject.address).arg(ssObject.port);
}
// ratio decision:
// unspecified: ratio = 1
// specified: use given value
double ratio = 1.0;
if (auto currRatio = serverObject["ratio"].toDouble(-1.0); currRatio != -1.0)
{
ratio = currRatio;
}
// else if (!serverObject["ratio"].isUndefined())
// {
// //*logList << QObject::tr("Invalid ratio encountered. using fallback value.");
// }
// format the total name of the node.
const auto finalName = QV2RAY_SSD_DEFAULT_NAME_PATTERN.arg(*groupName, nodeName).arg(ratio);
// appending to the total list
QJsonObject root;
QJsonArray outbounds;
QJsonObject inbound = inbounds::GenerateInboundEntry();
outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ ssObject }), {}));
root["outbounds"] = outbounds;
root["inbounds"] = QJsonArray{ inbound };
serverList.append({ finalName, root });
}
// returns the current result
return serverList;
}
#undef MUST_EXIST
#undef MUST_PORT
#undef MUST_ARRAY
#undef MUST_STRING
#undef SERVER_SHOULD_BE_OBJECT
#undef SHOULD_EXIST
#undef SHOULD_STRING
} // namespace amnezia::serialization::ssd

View File

@@ -0,0 +1,313 @@
#ifndef TRANSFER_H
#define TRANSFER_H
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#define JADDEx(field) root.insert(#field, field);
#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__)
constexpr auto VMESS_USER_ALTERID_DEFAULT = 0;
namespace amnezia::serialization {
struct ShadowSocksServerObject
{
QString address;
QString method;
QString password;
int port;
JSONSTRUCT_COMPARE(ShadowSocksServerObject, address, method, password)
JSONSTRUCT_REGISTER(ShadowSocksServerObject, F(address, port, method, password))
};
struct VMessServerObject
{
struct UserObject
{
QString id;
int alterId = VMESS_USER_ALTERID_DEFAULT;
QString security = "auto";
int level = 0;
JSONSTRUCT_COMPARE(UserObject, id, alterId, security, level)
JSONSTRUCT_REGISTER(UserObject, F(id, alterId, security, level))
};
QString address;
int port;
QList<UserObject> users;
JSONSTRUCT_COMPARE(VMessServerObject, address, port, users)
JSONSTRUCT_REGISTER(VMessServerObject, F(address, port, users))
};
namespace transfer
{
struct HTTPRequestObject
{
QString version = "1.1";
QString method = "GET";
QList<QString> path = { "/" };
QMap<QString, QList<QString>> headers;
HTTPRequestObject()
{
headers = {
{ "Host", { "www.baidu.com", "www.bing.com" } },
{ "User-Agent",
{ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46" } },
{ "Accept-Encoding", { "gzip, deflate" } },
{ "Connection", { "keep-alive" } },
{ "Pragma", { "no-cache" } }
};
}
JSONSTRUCT_COMPARE(HTTPRequestObject, version, method, path, headers)
JSONSTRUCT_REGISTER(HTTPRequestObject, F(version, method, path, headers))
};
//
//
struct HTTPResponseObject
{
QString version = "1.1";
QString status = "200";
QString reason = "OK";
QMap<QString, QList<QString>> headers;
HTTPResponseObject()
{
headers = { { "Content-Type", { "application/octet-stream", "video/mpeg" } }, //
{ "Transfer-Encoding", { "chunked" } }, //
{ "Connection", { "keep-alive" } }, //
{ "Pragma", { "no-cache" } } };
}
JSONSTRUCT_COMPARE(HTTPResponseObject, version, status, reason, headers)
JSONSTRUCT_REGISTER(HTTPResponseObject, F(version, status, reason, headers))
};
//
//
struct TCPHeader_Internal
{
QString type = "none";
HTTPRequestObject request;
HTTPResponseObject response;
JSONSTRUCT_COMPARE(TCPHeader_Internal, type, request, response)
JSONSTRUCT_REGISTER(TCPHeader_Internal, A(type), F(request, response))
};
//
//
struct ObfsHeaderObject
{
QString type = "none";
JSONSTRUCT_COMPARE(ObfsHeaderObject, type)
JSONSTRUCT_REGISTER(ObfsHeaderObject, F(type))
};
//
//
struct TCPObject
{
TCPHeader_Internal header;
JSONSTRUCT_COMPARE(TCPObject, header)
JSONSTRUCT_REGISTER(TCPObject, F(header))
};
//
//
struct KCPObject
{
int mtu = 1350;
int tti = 50;
int uplinkCapacity = 5;
int downlinkCapacity = 20;
bool congestion = false;
int readBufferSize = 2;
int writeBufferSize = 2;
QString seed;
ObfsHeaderObject header;
KCPObject(){};
JSONSTRUCT_COMPARE(KCPObject, mtu, tti, uplinkCapacity, downlinkCapacity, congestion, readBufferSize, writeBufferSize, seed, header)
JSONSTRUCT_REGISTER(KCPObject, F(mtu, tti, uplinkCapacity, downlinkCapacity, congestion, readBufferSize, writeBufferSize, header, seed))
};
//
//
struct WebSocketObject
{
QString path = "/";
QMap<QString, QString> headers;
int maxEarlyData = 0;
bool useBrowserForwarding = false;
QString earlyDataHeaderName;
JSONSTRUCT_COMPARE(WebSocketObject, path, headers, maxEarlyData, useBrowserForwarding, earlyDataHeaderName)
JSONSTRUCT_REGISTER(WebSocketObject, F(path, headers, maxEarlyData, useBrowserForwarding, earlyDataHeaderName))
};
//
//
struct HttpObject
{
QList<QString> host;
QString path = "/";
QString method = "PUT";
QMap<QString, QList<QString>> headers;
JSONSTRUCT_COMPARE(HttpObject, host, path, method, headers)
JSONSTRUCT_REGISTER(HttpObject, F(host, path, method, headers))
};
//
//
struct DomainSocketObject
{
QString path = "/";
JSONSTRUCT_COMPARE(DomainSocketObject, path)
JSONSTRUCT_REGISTER(DomainSocketObject, F(path))
};
//
//
struct QuicObject
{
QString security = "none";
QString key;
ObfsHeaderObject header;
JSONSTRUCT_COMPARE(QuicObject, security, key, header)
JSONSTRUCT_REGISTER(QuicObject, F(security, key, header))
};
//
//
struct gRPCObject
{
QString serviceName;
bool multiMode = false;
JSONSTRUCT_COMPARE(gRPCObject, serviceName, multiMode)
JSONSTRUCT_REGISTER(gRPCObject, F(serviceName, multiMode))
};
//
//
struct SockoptObject
{
int mark = 0;
bool tcpFastOpen = false;
QString tproxy = "off";
int tcpKeepAliveInterval = 0;
JSONSTRUCT_COMPARE(SockoptObject, mark, tcpFastOpen, tproxy, tcpKeepAliveInterval)
JSONSTRUCT_REGISTER(SockoptObject, F(mark, tcpFastOpen, tproxy, tcpKeepAliveInterval))
};
//
//
struct CertificateObject
{
QString usage = "encipherment";
QString certificateFile;
QString keyFile;
QList<QString> certificate;
QList<QString> key;
JSONSTRUCT_COMPARE(CertificateObject, usage, certificateFile, keyFile, certificate, key)
JSONSTRUCT_REGISTER(CertificateObject, F(usage, certificateFile, keyFile, certificate, key))
};
//
//
struct TLSObject
{
QString serverName;
bool allowInsecure = false;
bool enableSessionResumption = false;
bool disableSystemRoot = false;
QList<QString> alpn;
QList<QString> pinnedPeerCertificateChainSha256;
QList<CertificateObject> certificates;
JSONSTRUCT_COMPARE(TLSObject, serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn,
pinnedPeerCertificateChainSha256, certificates)
JSONSTRUCT_REGISTER(TLSObject, F(serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn,
pinnedPeerCertificateChainSha256, certificates))
};
//
//
struct XTLSObject
{
QString serverName;
bool allowInsecure = false;
bool enableSessionResumption = false;
bool disableSystemRoot = false;
QList<QString> alpn;
QList<CertificateObject> certificates;
JSONSTRUCT_COMPARE(XTLSObject, serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, certificates)
JSONSTRUCT_REGISTER(XTLSObject, F(serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, certificates))
};
} // namespace transfer
//
//
struct TrojanObject
{
quint16 port;
QString address;
QString password;
QString sni;
bool ignoreCertificate = false;
bool ignoreHostname = false;
bool reuseSession = false;
bool sessionTicket = false;
bool reusePort = false;
bool tcpFastOpen = false;
#define _X(name) json[#name] = name
QJsonObject toJson() const
{
QJsonObject json;
_X(port);
_X(address);
_X(password);
_X(sni);
_X(ignoreCertificate);
_X(ignoreHostname);
_X(reuseSession);
_X(reusePort);
_X(sessionTicket);
_X(tcpFastOpen);
return json;
};
#undef _X
#define _X(name, type) name = root[#name].to##type()
void loadJson(const QJsonObject &root)
{
_X(port, Int);
_X(address, String);
_X(password, String);
_X(sni, String);
_X(ignoreHostname, Bool);
_X(ignoreCertificate, Bool);
_X(reuseSession, Bool);
_X(reusePort, Bool);
_X(sessionTicket, Bool);
_X(tcpFastOpen, Bool);
}
#undef _X
[[nodiscard]] static TrojanObject fromJson(const QJsonObject &root)
{
TrojanObject o;
o.loadJson(root);
return o;
}
};
struct StreamSettingsObject
{
QString network = "tcp";
QString security = "none";
transfer::SockoptObject sockopt;
transfer::TLSObject tlsSettings;
transfer::XTLSObject xtlsSettings;
transfer::TCPObject tcpSettings;
transfer::KCPObject kcpSettings;
transfer::WebSocketObject wsSettings;
transfer::HttpObject httpSettings;
transfer::DomainSocketObject dsSettings;
transfer::QuicObject quicSettings;
transfer::gRPCObject grpcSettings;
JSONSTRUCT_COMPARE(StreamSettingsObject, network, security, sockopt, //
tcpSettings, tlsSettings, xtlsSettings, kcpSettings, wsSettings, httpSettings, dsSettings, quicSettings, grpcSettings)
JSONSTRUCT_REGISTER(StreamSettingsObject, F(network, security, sockopt),
F(tcpSettings, tlsSettings, xtlsSettings, kcpSettings, wsSettings, httpSettings, dsSettings, quicSettings, grpcSettings))
};
}
#endif //TRANSFER_H

View File

@@ -0,0 +1,271 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include <QUrlQuery>
#include "serialization.h"
#define OUTBOUND_TAG_PROXY "PROXY"
namespace amnezia::serialization::trojan
{
const QString Serialize(const TrojanObject &object, const QString &alias)
{
QUrlQuery query;
if (object.ignoreHostname)
query.addQueryItem("allowInsecureHostname", "1");
if (object.ignoreCertificate)
query.addQueryItem("allowInsecureCertificate", "1");
if (object.sessionTicket)
query.addQueryItem("sessionTicket", "1");
if (object.ignoreCertificate || object.ignoreHostname)
query.addQueryItem("allowInsecure", "1");
if (object.tcpFastOpen)
query.addQueryItem("tfo", "1");
if (!object.sni.isEmpty())
query.addQueryItem("sni", object.sni);
QUrl link;
if (!object.password.isEmpty())
link.setUserName(object.password, QUrl::DecodedMode);
link.setPort(object.port);
link.setHost(object.address);
link.setFragment(alias);
link.setQuery(query);
link.setScheme("trojan");
return link.toString(QUrl::FullyEncoded);
}
QJsonObject Deserialize(const QString &trojanUri, QString *alias, QString *errMessage)
{
const QString prefix = "trojan://";
if (!trojanUri.startsWith(prefix))
{
*errMessage = ("Invalid Trojan URI");
return {};
}
//
const auto trueList = QStringList{ "true", "1", "yes", "y" };
const QUrl trojanUrl(trojanUri.trimmed());
const QUrlQuery query(trojanUrl.query());
*alias = trojanUrl.fragment(QUrl::FullyDecoded);
auto getQueryValue = [&](const QString &key) {
return query.queryItemValue(key, QUrl::FullyDecoded);
};
//
TrojanObject result;
result.address = trojanUrl.host();
result.password = QUrl::fromPercentEncoding(trojanUrl.userInfo().toUtf8());
result.port = trojanUrl.port();
// process sni (and also "peer")
if (query.hasQueryItem("sni"))
{
result.sni = getQueryValue("sni");
}
else if (query.hasQueryItem("peer"))
{
// This is evil and may be removed in a future version.
qWarning() << "use of 'peer' in trojan url is deprecated";
result.sni = getQueryValue("peer");
}
else
{
// Use the hostname
result.sni = result.address;
}
//
result.tcpFastOpen = trueList.contains(getQueryValue("tfo").toLower());
result.sessionTicket = trueList.contains(getQueryValue("sessionTicket").toLower());
//
bool allowAllInsecure = trueList.contains(getQueryValue("allowInsecure").toLower());
result.ignoreHostname = allowAllInsecure || trueList.contains(getQueryValue("allowInsecureHostname").toLower());
result.ignoreCertificate = allowAllInsecure || trueList.contains(getQueryValue("allowInsecureCertificate").toLower());
QJsonObject stream;
// handle type
const auto hasType = query.hasQueryItem("type");
const auto type = hasType ? query.queryItemValue("type") : "tcp";
if (type != "tcp")
QJsonIO::SetValue(stream, type, "network");
// type-wise settings
if (type == "kcp")
{
const auto hasSeed = query.hasQueryItem("seed");
if (hasSeed)
QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" });
const auto hasHeaderType = query.hasQueryItem("headerType");
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
if (headerType != "none")
QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" });
}
else if (type == "http")
{
const auto hasPath = query.hasQueryItem("path");
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
if (path != "/")
QJsonIO::SetValue(stream, path, { "httpSettings", "path" });
const auto hasHost = query.hasQueryItem("host");
if (hasHost)
{
const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(","));
QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" });
}
}
else if (type == "ws")
{
const auto hasPath = query.hasQueryItem("path");
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
if (path != "/")
QJsonIO::SetValue(stream, path, { "wsSettings", "path" });
const auto hasHost = query.hasQueryItem("host");
if (hasHost)
{
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" });
}
}
else if (type == "quic")
{
const auto hasQuicSecurity = query.hasQueryItem("quicSecurity");
if (hasQuicSecurity)
{
const auto quicSecurity = query.queryItemValue("quicSecurity");
QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" });
if (quicSecurity != "none")
{
const auto key = query.queryItemValue("key");
QJsonIO::SetValue(stream, key, { "quicSettings", "key" });
}
const auto hasHeaderType = query.hasQueryItem("headerType");
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
if (headerType != "none")
QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" });
}
}
else if (type == "grpc")
{
const auto hasServiceName = query.hasQueryItem("serviceName");
if (hasServiceName)
{
const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8());
QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" });
}
const auto hasMode = query.hasQueryItem("mode");
if (hasMode)
{
const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi";
QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" });
}
}
// tls-wise settings
const auto hasSecurity = query.hasQueryItem("security");
const auto security = hasSecurity ? query.queryItemValue("security") : "none";
const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" );
if (security != "none")
{
QJsonIO::SetValue(stream, security, "security");
}
// sni
const auto hasSNI = query.hasQueryItem("sni");
if (hasSNI)
{
const auto sni = query.queryItemValue("sni");
QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" });
}
// alpn
const auto hasALPN = query.hasQueryItem("alpn");
if (hasALPN)
{
const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8());
QStringList aplnElems = alpnRaw.split(",");
// h2 protocol is not supported by xray
aplnElems.removeAll("h2");
if (!aplnElems.isEmpty()) {
const auto alpnArray = QJsonArray::fromStringList(aplnElems);
QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" });
}
}
if (security == "reality")
{
if (query.hasQueryItem("fp"))
{
const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8());
QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" });
}
if (query.hasQueryItem("pbk"))
{
const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8());
QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" });
}
if (query.hasQueryItem("spiderX"))
{
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8());
QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" });
}
if (query.hasQueryItem("sid"))
{
const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8());
QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" });
}
}
QJsonObject root;
QJsonArray outbounds;
QJsonObject outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "trojan", outbounds::GenerateTrojanOUT({ result }), {});
outbound["streamSettings"] = stream;
outbounds.append(outbound);
JADD(outbounds)
QJsonObject inbound = inbounds::GenerateInboundEntry();
root["inbounds"] = QJsonArray { inbound };
return root;
}
} // namespace amnezia::serialization::trojan

View File

@@ -0,0 +1,256 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include <QUrlQuery>
#include "serialization.h"
namespace amnezia::serialization::vless
{
QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
{
// must start with vless://
if (!str.startsWith("vless://"))
{
*errMessage = QObject::tr("VLESS link should start with vless://");
return QJsonObject();
}
// parse url
QUrl url(str);
if (!url.isValid())
{
*errMessage = QObject::tr("link parse failed: %1").arg(url.errorString());
return QJsonObject();
}
// fetch host
const auto hostRaw = url.host();
if (hostRaw.isEmpty())
{
*errMessage = QObject::tr("empty host");
return QJsonObject();
}
const auto host = (hostRaw.startsWith('[') && hostRaw.endsWith(']')) ? hostRaw.mid(1, hostRaw.length() - 2) : hostRaw;
// fetch port
const auto port = url.port();
if (port == -1)
{
*errMessage = QObject::tr("missing port");
return QJsonObject();
}
// fetch remarks
const auto remarks = url.fragment();
if (!remarks.isEmpty())
{
*alias = remarks;
}
// fetch uuid
const auto uuid = url.userInfo();
if (uuid.isEmpty())
{
*errMessage = QObject::tr("missing uuid");
return QJsonObject();
}
// initialize QJsonObject with basic info
QJsonObject outbound;
QJsonObject stream;
QJsonIO::SetValue(outbound, "vless", "protocol");
QJsonIO::SetValue(outbound, host, { "settings", "vnext", 0, "address" });
QJsonIO::SetValue(outbound, port, { "settings", "vnext", 0, "port" });
QJsonIO::SetValue(outbound, uuid, { "settings", "vnext", 0, "users", 0, "id" });
// parse query
QUrlQuery query(url.query());
// handle type
const auto hasType = query.hasQueryItem("type");
const auto type = hasType ? query.queryItemValue("type") : "tcp";
if (type != "tcp")
QJsonIO::SetValue(stream, type, "network");
// handle encryption
const auto hasEncryption = query.hasQueryItem("encryption");
const auto encryption = hasEncryption ? query.queryItemValue("encryption") : "none";
QJsonIO::SetValue(outbound, encryption, { "settings", "vnext", 0, "users", 0, "encryption" });
// type-wise settings
if (type == "kcp")
{
const auto hasSeed = query.hasQueryItem("seed");
if (hasSeed)
QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" });
const auto hasHeaderType = query.hasQueryItem("headerType");
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
if (headerType != "none")
QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" });
}
else if (type == "http")
{
const auto hasPath = query.hasQueryItem("path");
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
if (path != "/")
QJsonIO::SetValue(stream, path, { "httpSettings", "path" });
const auto hasHost = query.hasQueryItem("host");
if (hasHost)
{
const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(","));
QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" });
}
}
else if (type == "ws")
{
const auto hasPath = query.hasQueryItem("path");
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
if (path != "/")
QJsonIO::SetValue(stream, path, { "wsSettings", "path" });
const auto hasHost = query.hasQueryItem("host");
if (hasHost)
{
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" });
}
}
else if (type == "quic")
{
const auto hasQuicSecurity = query.hasQueryItem("quicSecurity");
if (hasQuicSecurity)
{
const auto quicSecurity = query.queryItemValue("quicSecurity");
QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" });
if (quicSecurity != "none")
{
const auto key = query.queryItemValue("key");
QJsonIO::SetValue(stream, key, { "quicSettings", "key" });
}
const auto hasHeaderType = query.hasQueryItem("headerType");
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
if (headerType != "none")
QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" });
}
}
else if (type == "grpc")
{
const auto hasServiceName = query.hasQueryItem("serviceName");
if (hasServiceName)
{
const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8());
QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" });
}
const auto hasMode = query.hasQueryItem("mode");
if (hasMode)
{
const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi";
QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" });
}
}
// tls-wise settings
const auto hasSecurity = query.hasQueryItem("security");
const auto security = hasSecurity ? query.queryItemValue("security") : "none";
const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" );
if (security != "none")
{
QJsonIO::SetValue(stream, security, "security");
}
// sni
const auto hasSNI = query.hasQueryItem("sni");
if (hasSNI)
{
const auto sni = query.queryItemValue("sni");
QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" });
}
// alpn
const auto hasALPN = query.hasQueryItem("alpn");
if (hasALPN)
{
const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8());
QStringList aplnElems = alpnRaw.split(",");
// h2 protocol is not supported by xray
aplnElems.removeAll("h2");
if (!aplnElems.isEmpty()) {
const auto alpnArray = QJsonArray::fromStringList(aplnElems);
QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" });
}
}
// xtls-specific
if (security == "xtls" || security == "reality")
{
const auto flow = query.queryItemValue("flow");
QJsonIO::SetValue(outbound, flow, { "settings", "vnext", 0, "users", 0, "flow" });
}
if (security == "reality")
{
if (query.hasQueryItem("fp"))
{
const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8());
QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" });
}
if (query.hasQueryItem("pbk"))
{
const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8());
QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" });
}
if (query.hasQueryItem("spiderX"))
{
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8());
QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" });
}
if (query.hasQueryItem("sid"))
{
const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8());
QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" });
}
}
// assembling config
QJsonObject root;
outbound["streamSettings"] = stream;
QJsonObject inbound = inbounds::GenerateInboundEntry();
root["outbounds"] = QJsonArray{ outbound };
root["inbounds"] = QJsonArray { inbound };
return root;
}
} // namespace amnezia::serialization::vless

View File

@@ -0,0 +1,344 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#include <QJsonDocument>
#include "transfer.h"
#include "utilities.h"
#include "serialization.h"
#define nothing
#define OUTBOUND_TAG_PROXY "PROXY"
namespace amnezia::serialization::vmess
{
// From https://github.com/2dust/v2rayN/wiki/分享链接格式说明(ver-2)
const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias)
{
QJsonObject vmessUriRoot;
// Constant
vmessUriRoot["v"] = 2;
vmessUriRoot["ps"] = alias;
vmessUriRoot["add"] = server.address;
vmessUriRoot["port"] = server.port;
vmessUriRoot["id"] = server.users.front().id;
vmessUriRoot["aid"] = server.users.front().alterId;
const auto scy = server.users.front().security;
vmessUriRoot["scy"] = (scy == "aes-128-gcm" || scy == "chacha20-poly1305" || scy == "none" || scy == "zero") ? scy : "auto";
vmessUriRoot["net"] = transfer.network == "http" ? "h2" : transfer.network;
vmessUriRoot["tls"] = (transfer.security == "tls" || transfer.security == "xtls") ? "tls" : "none";
if (transfer.security == "tls")
{
vmessUriRoot["sni"] = transfer.tlsSettings.serverName;
}
else if (transfer.security == "xtls")
{
vmessUriRoot["sni"] = transfer.xtlsSettings.serverName;
}
if (transfer.network == "tcp")
{
vmessUriRoot["type"] = transfer.tcpSettings.header.type;
}
else if (transfer.network == "kcp")
{
vmessUriRoot["type"] = transfer.kcpSettings.header.type;
}
else if (transfer.network == "quic")
{
vmessUriRoot["type"] = transfer.quicSettings.header.type;
vmessUriRoot["host"] = transfer.quicSettings.security;
vmessUriRoot["path"] = transfer.quicSettings.key;
}
else if (transfer.network == "ws")
{
auto x = transfer.wsSettings.headers;
auto host = x.contains("host");
auto CapHost = x.contains("Host");
auto realHost = host ? x["host"] : (CapHost ? x["Host"] : "");
//
vmessUriRoot["host"] = realHost;
vmessUriRoot["path"] = transfer.wsSettings.path;
}
else if (transfer.network == "h2" || transfer.network == "http")
{
vmessUriRoot["host"] = transfer.httpSettings.host.join(",");
vmessUriRoot["path"] = transfer.httpSettings.path;
}
else if (transfer.network == "grpc")
{
vmessUriRoot["path"] = transfer.grpcSettings.serviceName;
}
if (!vmessUriRoot.contains("type") || vmessUriRoot["type"].toString().isEmpty())
{
vmessUriRoot["type"] = "none";
}
//
QString jString = Utils::JsonToString(vmessUriRoot, QJsonDocument::JsonFormat::Compact);
auto vmessPart = jString.toUtf8().toBase64();
return "vmess://" + vmessPart;
}
// This generates global config containing only one outbound....
QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage)
{
#define default QJsonObject()
QString vmess = vmessStr;
if (vmess.trimmed() != vmess)
{
vmess = vmessStr.trimmed();
}
// Reset errMessage
*errMessage = "";
if (!vmess.toLower().startsWith("vmess://"))
{
*errMessage = QObject::tr("VMess string should start with 'vmess://'");
return default;
}
const auto b64Str = vmess.mid(8, vmess.length() - 8);
if (b64Str.isEmpty())
{
*errMessage = QObject::tr("VMess string should be a valid base64 string");
return default;
}
auto vmessString = Utils::SafeBase64Decode(b64Str);
auto jsonErr = Utils::VerifyJsonString(vmessString);
if (!jsonErr.isEmpty())
{
*errMessage = jsonErr;
return default;
}
auto vmessConf = Utils::JsonFromString(vmessString);
if (vmessConf.isEmpty())
{
*errMessage = QObject::tr("JSON should not be empty");
return default;
}
// --------------------------------------------------------------------------------------
QJsonObject root;
QString ps, add, id, net, type, host, path, tls, scy, sni;
int port, aid;
//
// __vmess_checker__func(key, values)
//
// - Key = Key in JSON and the variable name.
// - Values = Candidate variable list, if not match, the first one is used as default.
//
// - [[val.size() <= 1]] is used when only the default value exists.
//
// - It can be empty, if so, if the key is not in the JSON, or the value is empty, report an error.
// - Else if it contains one thing. if the key is not in the JSON, or the value is empty, use that one.
// - Else if it contains many things, when the key IS in the JSON but not within the THINGS, use the first in the THINGS
// - Else -------------------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> use the JSON value
//
#define __vmess_checker__func(key, values) \
{ \
auto val = QStringList() values; \
if (vmessConf.contains(#key) && !vmessConf[#key].toVariant().toString().trimmed().isEmpty() && \
(val.size() <= 1 || val.contains(vmessConf[#key].toVariant().toString()))) \
{ \
key = vmessConf[#key].toVariant().toString(); \
} \
else if (!val.isEmpty()) \
{ \
key = val.first(); \
} \
else \
{ \
*errMessage = QObject::tr(#key " does not exist."); \
} \
}
// vmess v1 upgrader
if (!vmessConf.contains("v"))
{
qDebug() << "Detected deprecated vmess v1. Trying to upgrade...";
if (const auto network = vmessConf["net"].toString(); network == "ws" || network == "h2")
{
const QStringList hostComponents = vmessConf["host"].toString().replace(" ", "").split(";");
if (const auto nParts = hostComponents.length(); nParts == 1)
vmessConf["path"] = hostComponents[0], vmessConf["host"] = "";
else if (nParts == 2)
vmessConf["path"] = hostComponents[0], vmessConf["host"] = hostComponents[1];
else
vmessConf["path"] = "/", vmessConf["host"] = "";
}
}
// Strict check of VMess protocol, to check if the specified value
// is in the correct range.
//
// Get Alias (AKA ps) from address and port.
{
// Some idiot vmess:// links are using alterId...
aid = vmessConf.contains("aid") ? vmessConf.value("aid").toInt(VMESS_USER_ALTERID_DEFAULT) :
vmessConf.value("alterId").toInt(VMESS_USER_ALTERID_DEFAULT);
//
//
__vmess_checker__func(ps, << vmessConf["add"].toVariant().toString() + ":" + vmessConf["port"].toVariant().toString()); //
__vmess_checker__func(add, nothing); //
__vmess_checker__func(id, nothing); //
__vmess_checker__func(scy, << "aes-128-gcm" //
<< "chacha20-poly1305" //
<< "auto" //
<< "none" //
<< "zero"); //
//
__vmess_checker__func(type, << "none" //
<< "http" //
<< "srtp" //
<< "utp" //
<< "wechat-video"); //
//
__vmess_checker__func(net, << "tcp" //
<< "http" //
<< "h2" //
<< "ws" //
<< "kcp" //
<< "quic" //
<< "grpc"); //
//
__vmess_checker__func(tls, << "none" //
<< "tls"); //
//
path = vmessConf.contains("path") ? vmessConf["path"].toVariant().toString() : (net == "quic" ? "" : "/");
host = vmessConf.contains("host") ? vmessConf["host"].toVariant().toString() : (net == "quic" ? "none" : "");
}
// Respect connection type rather than obfs type
if (QStringList{ "srtp", "utp", "wechat-video" }.contains(type)) //
{ //
if (net != "quic" && net != "kcp") //
{ //
type = "none"; //
} //
}
port = vmessConf["port"].toVariant().toInt();
aid = vmessConf["aid"].toVariant().toInt();
//
// Apply the settings.
// User
VMessServerObject::UserObject user;
user.id = id;
user.alterId = aid;
user.security = scy;
//
// Server
VMessServerObject serv;
serv.port = port;
serv.address = add;
serv.users.push_back(user);
//
//
// Stream Settings
StreamSettingsObject streaming;
if (net == "tcp")
{
streaming.tcpSettings.header.type = type;
}
else if (net == "http" || net == "h2")
{
// Fill hosts for HTTP
for (const auto &_host : host.split(','))
{
if (!_host.isEmpty())
{
streaming.httpSettings.host << _host.trimmed();
}
}
streaming.httpSettings.path = path;
}
else if (net == "ws")
{
if (!host.isEmpty())
streaming.wsSettings.headers["Host"] = host;
streaming.wsSettings.path = path;
}
else if (net == "kcp")
{
streaming.kcpSettings.header.type = type;
}
else if (net == "quic")
{
streaming.quicSettings.security = host;
streaming.quicSettings.header.type = type;
streaming.quicSettings.key = path;
}
else if (net == "grpc")
{
streaming.grpcSettings.serviceName = path;
}
streaming.security = tls;
if (tls == "tls")
{
if (sni.isEmpty() && !host.isEmpty())
sni = host;
streaming.tlsSettings.serverName = sni;
streaming.tlsSettings.allowInsecure = false;
}
//
// Network type
// NOTE(DuckSoft): Damn vmess:// just don't write 'http' properly
if (net == "h2")
net = "http";
streaming.network = net;
//
// VMess root config
QJsonObject vConf;
vConf["vnext"] = QJsonArray{ serv.toJson() };
const auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, streaming.toJson());
QJsonObject inbound = inbounds::GenerateInboundEntry();
root["outbounds"] = QJsonArray{ outbound };
root["inbounds"] = QJsonArray{ inbound };
// If previous alias is empty, just the PS is needed, else, append a "_"
*alias = alias->trimmed().isEmpty() ? ps : *alias + "_" + ps;
return root;
#undef default
}
} // namespace amnezia::serialization::vmess

View File

@@ -0,0 +1,172 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// 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 <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file 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 file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#include "transfer.h"
#include "serialization.h"
#include <QUrlQuery>
#define OUTBOUND_TAG_PROXY "PROXY"
namespace amnezia::serialization::vmess_new
{
const static QStringList NetworkType{ "tcp", "http", "ws", "kcp", "quic", "grpc" };
const static QStringList QuicSecurityTypes{ "none", "aes-128-gcm", "chacha20-poly1305" };
const static QStringList QuicKcpHeaderTypes{ "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" };
const static QStringList FalseTypes{ "false", "False", "No", "Off", "0" };
QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage)
{
QUrl url{ vmessStr };
QUrlQuery query{ url };
//
#define default QJsonObject()
if (!url.isValid())
{
*errMessage = QObject::tr("vmess:// url is invalid");
return default;
}
// If previous alias is empty, just the PS is needed, else, append a "_"
const auto name = url.fragment(QUrl::FullyDecoded).trimmed();
*alias = alias->isEmpty() ? name : (*alias + "_" + name);
VMessServerObject server;
server.users << VMessServerObject::UserObject{};
StreamSettingsObject stream;
QString net;
bool tls = false;
// Check streamSettings
{
for (const auto &_protocol : url.userName().split("+"))
{
if (_protocol == "tls")
tls = true;
else
net = _protocol;
}
if (!NetworkType.contains(net))
{
*errMessage = QObject::tr("Invalid streamSettings protocol: ") + net;
return default;
}
stream.network = net;
stream.security = tls ? "tls" : "";
}
// Host Port UUID AlterID
{
const auto host = url.host();
int port = url.port();
QString uuid;
int aid;
{
const auto pswd = url.password();
const auto index = pswd.lastIndexOf("-");
uuid = pswd.mid(0, index);
aid = pswd.right(pswd.length() - index - 1).toInt();
}
server.address = host;
server.port = port;
server.users.first().id = uuid;
server.users.first().alterId = aid;
server.users.first().security = "auto";
}
const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) {
if (query.hasQueryItem(key))
return query.queryItemValue(key, QUrl::FullyDecoded);
else
return defaultValue;
};
//
// Begin transport settings parser
{
if (net == "tcp")
{
stream.tcpSettings.header.type = getQueryValue("type", "none");
}
else if (net == "http")
{
stream.httpSettings.host.append(getQueryValue("host", ""));
stream.httpSettings.path = getQueryValue("path", "/");
}
else if (net == "ws")
{
stream.wsSettings.headers["Host"] = getQueryValue("host", "");
stream.wsSettings.path = getQueryValue("path", "/");
}
else if (net == "kcp")
{
stream.kcpSettings.seed = getQueryValue("seed", "");
stream.kcpSettings.header.type = getQueryValue("type", "none");
}
else if (net == "quic")
{
stream.quicSettings.security = getQueryValue("security", "none");
stream.quicSettings.key = getQueryValue("key", "");
stream.quicSettings.header.type = getQueryValue("type", "none");
}
else if (net == "grpc")
{
stream.grpcSettings.serviceName = getQueryValue("serviceName", "");
}
else
{
*errMessage = QObject::tr("Unknown transport method: ") + net;
return default;
}
}
#undef default
if (tls)
{
stream.tlsSettings.allowInsecure = !FalseTypes.contains(getQueryValue("allowInsecure", "false"));
stream.tlsSettings.serverName = getQueryValue("tlsServerName", "");
}
QJsonObject root;
QJsonObject vConf;
QJsonArray vnextArray;
vnextArray.append(server.toJson());
vConf["vnext"] = vnextArray;
auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, stream.toJson());
QJsonObject inbound = inbounds::GenerateInboundEntry();
//
root["outbounds"] = QJsonArray{ outbound };
root["inbound"] = QJsonArray{ inbound };
return root;
}
} // namespace amnezia::serialization::vmess_new

View File

@@ -136,7 +136,7 @@ ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
callActivityMethod("start", "(Ljava/lang/String;)V",
QJniObject::fromString(config).object<jstring>());
return NoError;
return ErrorCode::NoError;
}
void AndroidController::stop()

View File

@@ -2,15 +2,15 @@ import Foundation
import os.log
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
}
public func wg_log(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "WG: \(title)", message: message)
neLog(type, title: "WG: \(title)", message: message)
}
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "OVPN: \(title)", message: message)
neLog(type, title: "OVPN: \(title)", message: message)
}
public func neLog(_ type: OSLogType, title: String = "", message: String) {

View File

@@ -3,223 +3,232 @@ import NetworkExtension
import OpenVPNAdapter
struct OpenVPNConfig: Decodable {
let config: String
let splitTunnelType: Int
let splitTunnelSites: [String]
let config: String
let splitTunnelType: Int
let splitTunnelSites: [String]
var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
}
var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
}
}
extension PacketTunnelProvider {
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
ovpnLog(.error, message: "Can't start")
return
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
ovpnLog(.error, message: "Can't start")
return
}
do {
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
return
}
}
do {
// ovpnLog(.info, message: "providerConfiguration: \(String(decoding: openVPNConfigData, as: UTF8.self))")
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch")
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration
if str.contains("cloak") {
configuration.setPTCloak()
}
return
let evaluation: OpenVPNConfigurationEvaluation?
do {
ovpnAdapter = OpenVPNAdapter()
ovpnAdapter?.delegate = self
evaluation = try ovpnAdapter?.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
if evaluation?.autologin == false {
ovpnLog(.info, message: "Implement login with user credentials")
}
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
ovpnAdapter?.connect(using: packetFlow)
}
}
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
let bytesin = ovpnAdapter?.transportStatistics.bytesIn
let bytesout = ovpnAdapter?.transportStatistics.bytesOut
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch")
guard let bytesin, let bytesout else {
completionHandler(nil)
return
}
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
]
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration
if str.contains("cloak") {
configuration.setPTCloak()
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
let evaluation: OpenVPNConfigurationEvaluation
do {
evaluation = try ovpnAdapter.apply(configuration: configuration)
func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)")
} catch {
completionHandler(error)
return
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
ovpnAdapter?.disconnect()
}
if !evaluation.autologin {
ovpnLog(.info, message: "Implement login with user credentials")
}
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
ovpnAdapter.connect(using: packetFlow)
// let ifaces = Interface.allInterfaces()
// .filter { $0.family == .ipv4 }
// .map { iface in iface.name }
// ovpn_log(.error, message: "Available TUN Interfaces: \(ifaces)")
}
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
let bytesin = ovpnAdapter.transportStatistics.bytesIn
let bytesout = ovpnAdapter.transportStatistics.bytesOut
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)")
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
ovpnAdapter.disconnect()
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: @escaping (Error?) -> Void
) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: @escaping (Error?) -> Void
) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
guard let splitTunnelSites else {
completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0))
return
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else {
if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
for excludeIPString in splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludeIP.address)",
subnetMask: "\(excludeIP.subnetMask())"))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else {
if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
guard let splitTunnelSites else {
completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0))
return
}
for excludeIPString in splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludeIP.address)",
subnetMask: "\(excludeIP.subnetMask())"))
}
}
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allIPv4.address)",
subnetMask: "\(allIPv4.subnetMask())"))
}
if let allIPv6 = IPAddressRange(from: "::/0") {
ipv6IncludedRoutes.append(NEIPv6Route(
destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allIPv4.address)",
subnetMask: "\(allIPv4.subnetMask())"))
}
if let allIPv6 = IPAddressRange(from: "::/0") {
ipv6IncludedRoutes.append(NEIPv6Route(
destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Process events returned by the OpenVPN library
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
handleEvent event: OpenVPNAdapterEvent,
message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
// Process events returned by the OpenVPN library
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
handleEvent event: OpenVPNAdapterEvent,
message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
if let startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle log messages
ovpnLog(.info, message: logMessage)
}
if let startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle log messages
ovpnLog(.info, message: logMessage)
}
}
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

View File

@@ -2,220 +2,186 @@ import Foundation
import NetworkExtension
extension PacketTunnelProvider {
func startWireguard(activationAttemptId: String?,
errorNotifier: ErrorNotifier,
completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
wg_log(.error, message: "Can't start, config missing")
completionHandler(nil)
return
}
do {
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
let wgConfigStr = wgConfig.str
wg_log(.info, title: "config: ", message: wgConfig.redux)
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
if tunnelConfiguration.peers.first!.allowedIPs
.map({ $0.stringRepresentation })
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
if wgConfig.splitTunnelType == 1 {
for index in tunnelConfiguration.peers.indices {
tunnelConfiguration.peers[index].allowedIPs.removeAll()
var allowedIPs = [IPAddressRange]()
for allowedIPString in wgConfig.splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP)
}
}
tunnelConfiguration.peers[index].allowedIPs = allowedIPs
}
} else if wgConfig.splitTunnelType == 2 {
for index in tunnelConfiguration.peers.indices {
var excludeIPs = [IPAddressRange]()
for excludeIPString in wgConfig.splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
excludeIPs.append(excludeIP)
}
}
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
}
}
}
wg_log(.info, message: "Starting tunnel from the " +
(activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
// Start the tunnel
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
guard let adapterError else {
let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil)
return
}
switch adapterError {
case .cannotLocateTunnelFileDescriptor:
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
case .dnsResolution(let dnsErrors):
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
.joined(separator: ", ")
wg_log(.error, message:
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
case .setNetworkSettings(let error):
wg_log(.error, message:
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .startWireGuardBackend(let errorCode):
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
case .invalidState:
fatalError()
}
}
} catch {
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
completionHandler(nil)
return
}
}
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
let components = settings!.components(separatedBy: "\n")
var settingsDictionary: [String: String] = [:]
for component in components {
let pair = component.components(separatedBy: "=")
if pair.count == 2 {
settingsDictionary[pair[0]] = pair[1]
}
}
let response: [String: Any] = [
"rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
"tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
}
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
wgAdapter.update(tunnelConfiguration: tunnelConfiguration) { error in
if let error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
func startWireguard(activationAttemptId: String?,
errorNotifier: ErrorNotifier,
completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
wg_log(.error, message: "Can't start, config missing")
completionHandler(nil)
return
}
do {
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
let wgConfigStr = wgConfig.str
wg_log(.info, title: "config: ", message: wgConfig.redux)
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
if tunnelConfiguration.peers.first!.allowedIPs
.map({ $0.stringRepresentation })
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
if wgConfig.splitTunnelType == 1 {
for index in tunnelConfiguration.peers.indices {
tunnelConfiguration.peers[index].allowedIPs.removeAll()
var allowedIPs = [IPAddressRange]()
for allowedIPString in wgConfig.splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP)
}
}
tunnelConfiguration.peers[index].allowedIPs = allowedIPs
}
} else if wgConfig.splitTunnelType == 2 {
for index in tunnelConfiguration.peers.indices {
var excludeIPs = [IPAddressRange]()
for excludeIPString in wgConfig.splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
excludeIPs.append(excludeIP)
}
}
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
}
}
}
wg_log(.info, message: "Starting tunnel from the " +
(activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
// Start the tunnel
wgAdapter = WireGuardAdapter(with: self) { logLevel, message in
wg_log(logLevel.osLogLevel, message: message)
}
wgAdapter?.start(tunnelConfiguration: tunnelConfiguration) { [weak self] adapterError in
guard let adapterError else {
let interfaceName = self?.wgAdapter?.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil)
return
}
switch adapterError {
case .cannotLocateTunnelFileDescriptor:
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
case .dnsResolution(let dnsErrors):
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
.joined(separator: ", ")
wg_log(.error, message:
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
case .setNetworkSettings(let error):
wg_log(.error, message:
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .startWireGuardBackend(let errorCode):
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
case .invalidState:
fatalError()
}
}
} catch {
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
completionHandler(nil)
return
}
self.wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
}
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
// private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) {
// dispatchPrecondition(condition: .onQueue(dispatchQueue))
//
// let emptyTunnelConfiguration = TunnelConfiguration(
// name: nil,
// interface: InterfaceConfiguration(privateKey: PrivateKey()),
// peers: []
// )
//
// wgAdapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in
// self.dispatchQueue.async {
// if let error {
// wg_log(.error, message: "Failed to start an empty tunnel")
// completionHandler(error)
// } else {
// wg_log(.info, message: "Started an empty tunnel")
// self.tunnelAdapterDidStart()
// }
// }
// }
//
// let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
//
// self.setTunnelNetworkSettings(settings) { error in
// completionHandler(error)
// }
// }
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter?.getRuntimeConfiguration { settings in
let components = settings!.components(separatedBy: "\n")
// private func tunnelAdapterDidStart() {
// dispatchPrecondition(condition: .onQueue(dispatchQueue))
// // ...
// }
var settingsDictionary: [String: String] = [:]
for component in components {
let pair = component.components(separatedBy: "=")
if pair.count == 2 {
settingsDictionary[pair[0]] = pair[1]
}
}
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)")
let response: [String: Any] = [
"rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
"tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
]
wgAdapter.stop { error in
ErrorNotifier.removeLastErrorFile()
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
}
if let error {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
}
completionHandler()
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter?.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
wgAdapter?.update(tunnelConfiguration: tunnelConfiguration) { [weak self] error in
if let error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
completionHandler(nil)
return
}
self?.wgAdapter?.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
}
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)")
wgAdapter?.stop { error in
ErrorNotifier.removeLastErrorFile()
if let error {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
}
completionHandler()
#if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users.
exit(0)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users.
exit(0)
#endif
}
}
}
}

View File

@@ -5,7 +5,7 @@ import Darwin
import OpenVPNAdapter
enum TunnelProtoType: String {
case wireguard, openvpn, shadowsocks, none
case wireguard, openvpn
}
struct Constants {
@@ -34,160 +34,138 @@ struct Constants {
}
class PacketTunnelProvider: NEPacketTunnelProvider {
lazy var wgAdapter = {
WireGuardAdapter(with: self) { logLevel, message in
wg_log(logLevel.osLogLevel, message: message)
}
}()
lazy var ovpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
/// Internal queue.
private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
var splitTunnelType: Int!
var splitTunnelSites: [String]!
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var protoType: TunnelProtoType = .none
var wgAdapter: WireGuardAdapter?
var ovpnAdapter: OpenVPNAdapter?
var splitTunnelType: Int?
var splitTunnelSites: [String]?
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var protoType: TunnelProtoType?
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let message = String(data: messageData, encoding: .utf8) else {
if let completionHandler {
completionHandler(nil)
guard let message = String(data: messageData, encoding: .utf8) else {
if let completionHandler {
completionHandler(nil)
}
return
}
neLog(.info, title: "App said: ", message: message)
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
neLog(.error, message: "Failed to serialize message from app")
return
}
guard let completionHandler else {
neLog(.error, message: "Missing message completion handler")
return
}
guard let action = message[Constants.kMessageKeyAction] as? String else {
neLog(.error, message: "Missing action key in app message")
completionHandler(nil)
return
}
if action == Constants.kActionStatus {
handleStatusAppMessage(messageData,
completionHandler: completionHandler)
}
return
}
neLog(.info, title: "App said: ", message: message)
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
neLog(.error, message: "Failed to serialize message from app")
return
}
guard let completionHandler else {
neLog(.error, message: "Missing message completion handler")
return
}
guard let action = message[Constants.kMessageKeyAction] as? String else {
neLog(.error, message: "Missing action key in app message")
completionHandler(nil)
return
}
if action == Constants.kActionStatus {
handleStatusAppMessage(messageData, completionHandler: completionHandler)
}
}
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
dispatchQueue.async {
let activationAttemptId = options?[Constants.kActivationAttemptId] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
neLog(.info, message: "Start tunnel")
if let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol {
let providerConfiguration = protocolConfiguration.providerConfiguration
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
self.protoType = .openvpn
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
self.protoType = .wireguard
override func startTunnel(options: [String : NSObject]? = nil,
completionHandler: @escaping ((any Error)?) -> Void) {
let activationAttemptId = options?[Constants.kActivationAttemptId] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
neLog(.info, message: "Start tunnel")
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
let providerConfiguration = protocolConfiguration.providerConfiguration
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
protoType = .openvpn
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
protoType = .wireguard
}
}
guard let protoType else {
let error = NSError(domain: "Protocol is not selected", code: 0)
completionHandler(error)
return
}
switch protoType {
case .wireguard:
startWireguard(activationAttemptId: activationAttemptId,
errorNotifier: errorNotifier,
completionHandler: completionHandler)
case .openvpn:
startOpenVPN(completionHandler: completionHandler)
}
} else {
self.protoType = .none
}
switch self.protoType {
case .wireguard:
self.startWireguard(activationAttemptId: activationAttemptId,
errorNotifier: errorNotifier,
completionHandler: completionHandler)
case .openvpn:
self.startOpenVPN(completionHandler: completionHandler)
case .shadowsocks:
break
// startShadowSocks(completionHandler: completionHandler)
case .none:
break
}
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
dispatchQueue.async {
switch self.protoType {
case .wireguard:
self.stopWireguard(with: reason, completionHandler: completionHandler)
case .openvpn:
self.stopOpenVPN(with: reason, completionHandler: completionHandler)
case .shadowsocks:
break
// stopShadowSocks(with: reason, completionHandler: completionHandler)
case .none:
break
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
guard let protoType else {
completionHandler()
return
}
switch protoType {
case .wireguard:
stopWireguard(with: reason,
completionHandler: completionHandler)
case .openvpn:
stopOpenVPN(with: reason,
completionHandler: completionHandler)
}
}
}
func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
switch protoType {
case .wireguard:
handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
case .openvpn:
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
case .shadowsocks:
break
// handleShadowSocksAppMessage(messageData, completionHandler: completionHandler)
case .none:
break
func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let protoType else {
completionHandler?(nil)
return
}
switch protoType {
case .wireguard:
handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
case .openvpn:
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
}
}
}
// MARK: Network observing methods
private func startListeningForNetworkChanges() {
stopListeningForNetworkChanges()
addObserver(self, forKeyPath: Constants.kDefaultPathKey, options: .old, context: nil)
}
private func stopListeningForNetworkChanges() {
removeObserver(self, forKeyPath: Constants.kDefaultPathKey)
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
guard Constants.kDefaultPathKey != keyPath else { return }
// Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi,
// even though the underlying network has not changed (i.e. `isEqualToPath` returns false),
// leading to "wakeup crashes" due to excessive network activity. Guard against false positives by
// comparing the paths' string description, which includes properties not exposed by the class
guard let lastPath: NWPath = change?[.oldKey] as? NWPath,
let defPath = defaultPath,
lastPath != defPath || lastPath.description != defPath.description else {
return
// MARK: Network observing methods
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
guard Constants.kDefaultPathKey != keyPath else { return }
// Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi,
// even though the underlying network has not changed (i.e. `isEqualToPath` returns false),
// leading to "wakeup crashes" due to excessive network activity. Guard against false positives by
// comparing the paths' string description, which includes properties not exposed by the class
guard let lastPath: NWPath = change?[.oldKey] as? NWPath,
let defPath = defaultPath,
lastPath != defPath || lastPath.description != defPath.description else {
return
}
DispatchQueue.main.async { [weak self] in
guard let self, self.defaultPath != nil else { return }
self.handle(networkChange: self.defaultPath!) { _ in }
}
}
DispatchQueue.main.async { [weak self] in
guard let self, self.defaultPath != nil else { return }
self.handle(networkChange: self.defaultPath!) { _ in }
}
}
private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) {
wg_log(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion)
}
private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) {
wg_log(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion)
}
}
extension WireGuardLogLevel {

View File

@@ -18,7 +18,7 @@
#include "logger.h"
constexpr const int WG_TUN_PROC_TIMEOUT = 5000;
constexpr const char* WG_RUNTIME_DIR = "/var/run/wireguard";
constexpr const char* WG_RUNTIME_DIR = "/var/run/amneziawg";
namespace {
Logger logger("WireguardUtilsLinux");

View File

@@ -16,7 +16,7 @@
#include "logger.h"
constexpr const int WG_TUN_PROC_TIMEOUT = 5000;
constexpr const char* WG_RUNTIME_DIR = "/var/run/wireguard";
constexpr const char* WG_RUNTIME_DIR = "/var/run/amneziawg";
namespace {
Logger logger("WireguardUtilsMacos");

View File

@@ -11,7 +11,7 @@
#include "windowstunnelservice.h"
#include "wireguardutilswindows.h"
#define TUNNEL_SERVICE_NAME L"WireGuardTunnel$AmneziaVPN"
#define TUNNEL_SERVICE_NAME L"AmneziaWGTunnel$AmneziaVPN"
class WindowsDaemon final : public Daemon {
Q_DISABLE_COPY_MOVE(WindowsDaemon)

View File

@@ -17,7 +17,7 @@
#define TUNNEL_NAMED_PIPE \
"\\\\." \
"\\pipe\\ProtectedPrefix\\Administrators\\WireGuard\\AmneziaVPN"
"\\pipe\\ProtectedPrefix\\Administrators\\AmneziaWG\\AmneziaVPN"
constexpr uint32_t WINDOWS_TUNNEL_MONITOR_TIMEOUT_MSEC = 2000;

View File

@@ -72,6 +72,8 @@ QMap<amnezia::Proto, QString> ProtocolProps::protocolHumanNames()
{ Proto::Ikev2, "IKEv2" },
{ Proto::L2tp, "L2TP" },
{ Proto::Xray, "XRay" },
{ Proto::SSXray, "ShadowSocks"},
{ Proto::TorWebSite, "Website in Tor network" },
{ Proto::Dns, "DNS Service" },
@@ -87,6 +89,8 @@ amnezia::ServiceType ProtocolProps::protocolService(Proto p)
{
switch (p) {
case Proto::Any: return ServiceType::None;
case Proto::SSXray: return ServiceType::None;
case Proto::OpenVpn: return ServiceType::Vpn;
case Proto::Cloak: return ServiceType::Vpn;
case Proto::ShadowSocks: return ServiceType::Vpn;
@@ -160,7 +164,7 @@ TransportProto ProtocolProps::defaultTransportProto(Proto p)
case Proto::Any: return TransportProto::Udp;
case Proto::OpenVpn: return TransportProto::Udp;
case Proto::Cloak: return TransportProto::Tcp;
case Proto::ShadowSocks: return TransportProto::Tcp;
case Proto::ShadowSocks: return TransportProto::TcpAndUdp;
case Proto::WireGuard: return TransportProto::Udp;
case Proto::Awg: return TransportProto::Udp;
case Proto::Ikev2: return TransportProto::Udp;

View File

@@ -83,6 +83,7 @@ namespace amnezia
constexpr char sftp[] = "sftp";
constexpr char awg[] = "awg";
constexpr char xray[] = "xray";
constexpr char ssxray[] = "ssxray";
constexpr char configVersion[] = "config_version";
@@ -173,7 +174,12 @@ namespace amnezia
constexpr char defaultSubnetCidr[] = "24";
constexpr char defaultPort[] = "51820";
constexpr char defaultMtu[] = "1420";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
constexpr char defaultMtu[] = "1280";
#else
constexpr char defaultMtu[] = "1376";
#endif
constexpr char serverConfigPath[] = "/opt/amnezia/wireguard/wg0.conf";
constexpr char serverPublicKeyPath[] = "/opt/amnezia/wireguard/wireguard_server_public_key.key";
constexpr char serverPskKeyPath[] = "/opt/amnezia/wireguard/wireguard_psk.key";
@@ -189,7 +195,11 @@ namespace amnezia
namespace awg
{
constexpr char defaultPort[] = "55424";
constexpr char defaultMtu[] = "1420";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
constexpr char defaultMtu[] = "1280";
#else
constexpr char defaultMtu[] = "1376";
#endif
constexpr char serverConfigPath[] = "/opt/amnezia/awg/wg0.conf";
constexpr char serverPublicKeyPath[] = "/opt/amnezia/awg/wireguard_server_public_key.key";
@@ -214,7 +224,8 @@ namespace amnezia
enum TransportProto {
Udp,
Tcp
Tcp,
TcpAndUdp
};
Q_ENUM_NS(TransportProto)
@@ -228,6 +239,7 @@ namespace amnezia
Ikev2,
L2tp,
Xray,
SSXray,
// non-vpn
TorWebSite,

View File

@@ -116,6 +116,7 @@ VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &
case DockerContainer::WireGuard: return new WireguardProtocol(configuration);
case DockerContainer::Awg: return new WireguardProtocol(configuration);
case DockerContainer::Xray: return new XrayProtocol(configuration);
case DockerContainer::SSXray: return new XrayProtocol(configuration);
#endif
default: return nullptr;
}

View File

@@ -233,6 +233,9 @@ void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration)
{
m_configData = configuration;
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
}
m_xrayConfig = xrayConfiguration;
m_localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort).toInt();
m_remoteAddress = configuration.value(amnezia::config_key::hostName).toString();

View File

@@ -123,8 +123,27 @@ QByteArray SecureQSettings::backupAppConfig() const
{
QJsonObject cfg;
const auto needToBackup = [this](const auto &key) {
for (const auto &item : m_fieldsToBackup)
{
if (key == "Conf/installationUuid")
{
return false;
}
if (key.startsWith(item))
{
return true;
}
}
return false;
};
for (const QString &key : m_settings.allKeys()) {
if (key == "Conf/installationUuid") {
if (!needToBackup(key))
{
continue;
}

View File

@@ -47,6 +47,10 @@ private:
mutable QMap<QString, QVariant> m_cache;
QStringList encryptedKeys; // encode only key listed here
// only this fields need for backup
QStringList m_fieldsToBackup = {
"Conf/", "Servers/",
};
mutable QByteArray m_key;
mutable QByteArray m_iv;

View File

@@ -40,6 +40,7 @@ cat > /opt/amnezia/shadowsocks/ss-config.json <<EOF
"password": "$SHADOWSOCKS_PASSWORD",
"server": "0.0.0.0",
"server_port": $SHADOWSOCKS_SERVER_PORT,
"timeout": 60
"timeout": 60,
"mode" : "tcp_and_udp"
}
EOF

View File

@@ -5,6 +5,7 @@ sudo docker run -d \
--restart always \
--cap-add=NET_ADMIN \
-p $SHADOWSOCKS_SERVER_PORT:$SHADOWSOCKS_SERVER_PORT/tcp \
-p $SHADOWSOCKS_SERVER_PORT:$SHADOWSOCKS_SERVER_PORT/udp \
--name $CONTAINER_NAME $CONTAINER_NAME
sudo docker network connect amnezia-dns-net $CONTAINER_NAME

View File

@@ -8,7 +8,6 @@
#include <QtConcurrent>
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "version.h"
ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &serversModel,
@@ -30,7 +29,7 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
connect(&m_apiController, &ApiController::configUpdated, this,
static_cast<void (ConnectionController::*)(const bool, const QJsonObject &, const int)>(&ConnectionController::openConnection));
connect(&m_apiController, &ApiController::errorOccurred, this, &ConnectionController::connectionErrorOccurred);
connect(&m_apiController, qOverload<ErrorCode>(&ApiController::errorOccurred), this, qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred));
m_state = Vpn::ConnectionState::Disconnected;
}
@@ -40,7 +39,7 @@ void ConnectionController::openConnection()
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
{
emit connectionErrorOccurred(errorString(ErrorCode::AmneziaServiceNotRunning));
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
return;
}
#endif
@@ -63,9 +62,9 @@ void ConnectionController::closeConnection()
emit disconnectFromVpn();
}
QString ConnectionController::getLastConnectionError()
ErrorCode ConnectionController::getLastConnectionError()
{
return errorString(m_vpnConnection->lastError());
return m_vpnConnection->lastError();
}
void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
@@ -218,7 +217,7 @@ void ConnectionController::openConnection(const bool updateConfig, const QJsonOb
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
ErrorCode errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorString(errorCode));
emit connectionErrorOccurred(errorCode);
return;
}

View File

@@ -34,7 +34,7 @@ public slots:
void openConnection();
void closeConnection();
QString getLastConnectionError();
ErrorCode getLastConnectionError();
void onConnectionStateChanged(Vpn::ConnectionState state);
void onCurrentContainerUpdated();
@@ -50,6 +50,7 @@ signals:
void connectionStateChanged();
void connectionErrorOccurred(const QString &errorMessage);
void connectionErrorOccurred(ErrorCode errorCode);
void reconnectWithUpdatedContainer(const QString &message);
void noInstalledContainers();

View File

@@ -9,7 +9,6 @@
#include <QStandardPaths>
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "systemController.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_utils.h"
@@ -101,7 +100,7 @@ void ExportController::generateConnectionConfig(const QString &clientName)
errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, clientName, serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -171,7 +170,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName)
}
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -189,7 +188,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::WireGuard, clientName, Proto::WireGuard, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -209,7 +208,7 @@ void ExportController::generateAwgConfig(const QString &clientName)
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Awg, clientName, Proto::Awg, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -237,7 +236,7 @@ void ExportController::generateShadowSocksConfig()
}
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -263,7 +262,7 @@ void ExportController::generateCloakConfig()
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Cloak, "", Proto::Cloak, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -283,7 +282,7 @@ void ExportController::generateXrayConfig()
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
return;
}
@@ -320,7 +319,7 @@ void ExportController::updateClientManagementModel(const DockerContainer contain
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
ErrorCode errorCode = m_clientManagementModel->updateModel(container, credentials, serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
}
}
@@ -330,7 +329,7 @@ void ExportController::revokeConfig(const int row, const DockerContainer contain
ErrorCode errorCode =
m_clientManagementModel->revokeClient(row, container, credentials, m_serversModel->getProcessedServerIndex(), serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
}
}
@@ -339,7 +338,7 @@ void ExportController::renameClient(const int row, const QString &clientName, co
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
ErrorCode errorCode = m_clientManagementModel->renameClient(row, clientName, container, credentials, serverController);
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
emit exportErrorOccurred(errorCode);
}
}

View File

@@ -49,6 +49,7 @@ public slots:
signals:
void generateConfig(int type);
void exportErrorOccurred(const QString &errorMessage);
void exportErrorOccurred(ErrorCode errorCode);
void exportConfigChanged();

View File

@@ -4,9 +4,13 @@
#include <QFileInfo>
#include <QQuickItem>
#include <QRandomGenerator>
#include <QUrlQuery>
#include <QStandardPaths>
#include "utilities.h"
#include "core/serialization/serialization.h"
#include "core/errorstrings.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
@@ -87,6 +91,55 @@ bool ImportController::extractConfigFromFile(const QString &fileName)
bool ImportController::extractConfigFromData(QString data)
{
QString config = data;
QString prefix;
QString errormsg;
if (config.startsWith("vless://")) {
m_configType = ConfigTypes::Xray;
m_config = extractXrayConfig(Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg),
QJsonDocument::JsonFormat::Compact), prefix);
return m_config.empty() ? false : true;
}
if (config.startsWith("vmess://") && config.contains("@")) {
m_configType = ConfigTypes::Xray;
m_config = extractXrayConfig(Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg),
QJsonDocument::JsonFormat::Compact), prefix);
return m_config.empty() ? false : true;
}
if (config.startsWith("vmess://")) {
m_configType = ConfigTypes::Xray;
m_config = extractXrayConfig(Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg),
QJsonDocument::JsonFormat::Compact), prefix);
return m_config.empty() ? false : true;
}
if (config.startsWith("trojan://")) {
m_configType = ConfigTypes::Xray;
m_config = extractXrayConfig(Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg),
QJsonDocument::JsonFormat::Compact), prefix);
return m_config.empty() ? false : true;
}
if (config.startsWith("ss://") && !config.contains("plugin=")) {
m_configType = ConfigTypes::ShadowSocks;
m_config = extractXrayConfig(Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg),
QJsonDocument::JsonFormat::Compact), prefix);
return m_config.empty() ? false : true;
}
if (config.startsWith("ssd://")) {
QStringList tmp;
QList<std::pair<QString, QJsonObject>> servers = serialization::ssd::Deserialize(config, &prefix, &tmp);
m_configType = ConfigTypes::ShadowSocks;
// Took only first config from list
if (!servers.isEmpty()) {
m_config = extractXrayConfig(servers.first().first);
}
return m_config.empty() ? false : true;
}
m_configType = checkConfigFormat(config);
if (m_configType == ConfigTypes::Invalid) {
data.replace("vpn://", "");
@@ -221,7 +274,7 @@ void ImportController::importConfig()
} else if (m_config.contains(config_key::configVersion)) {
quint16 crc = qChecksum(QJsonDocument(m_config).toJson());
if (m_serversModel->isServerFromApiAlreadyExists(crc)) {
emit importErrorOccurred(errorString(ErrorCode::ApiConfigAlreadyAdded), true);
emit importErrorOccurred(ErrorCode::ApiConfigAlreadyAdded, true);
} else {
m_config.insert(config_key::crc, crc);
@@ -231,7 +284,7 @@ void ImportController::importConfig()
} else {
qDebug() << "Failed to import profile";
qDebug().noquote() << QJsonDocument(m_config).toJson();
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
}
m_config = {};
@@ -308,7 +361,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
hostName = hostNameAndPortMatch.captured(1);
} else {
qDebug() << "Key parameter 'Endpoint' is missing";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
return QJsonObject();
}
@@ -334,7 +387,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
lastConfig[config_key::server_pub_key] = configMap.value("PublicKey");
} else {
qDebug() << "One of the key parameters is missing (PrivateKey, Address, PublicKey)";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
return QJsonObject();
}
@@ -398,7 +451,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
return config;
}
QJsonObject ImportController::extractXrayConfig(const QString &data)
QJsonObject ImportController::extractXrayConfig(const QString &data, const QString &description)
{
QJsonParseError parserErr;
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
@@ -410,8 +463,13 @@ QJsonObject ImportController::extractXrayConfig(const QString &data)
lastConfig[config_key::isThirdPartyConfig] = true;
QJsonObject containers;
containers.insert(config_key::container, QJsonValue("amnezia-xray"));
containers.insert(config_key::xray, QJsonValue(lastConfig));
if (m_configType == ConfigTypes::ShadowSocks) {
containers.insert(config_key::ssxray, QJsonValue(lastConfig));
containers.insert(config_key::container, QJsonValue("amnezia-ssxray"));
} else {
containers.insert(config_key::container, QJsonValue("amnezia-xray"));
containers.insert(config_key::xray, QJsonValue(lastConfig));
}
QJsonArray arr;
arr.push_back(containers);
@@ -426,9 +484,17 @@ QJsonObject ImportController::extractXrayConfig(const QString &data)
QJsonObject config;
config[config_key::containers] = arr;
config[config_key::defaultContainer] = "amnezia-xray";
config[config_key::description] = m_settings->nextAvailableServerName();
if (m_configType == ConfigTypes::ShadowSocks) {
config[config_key::defaultContainer] = "amnezia-ssxray";
} else {
config[config_key::defaultContainer] = "amnezia-xray";
}
if (description.isEmpty()) {
config[config_key::description] = m_settings->nextAvailableServerName();
} else {
config[config_key::description] = description;
}
config[config_key::hostName] = hostName;
return config;

View File

@@ -14,6 +14,7 @@ namespace
WireGuard,
Awg,
Xray,
ShadowSocks,
Backup,
Invalid
};
@@ -54,6 +55,7 @@ public slots:
signals:
void importFinished();
void importErrorOccurred(const QString &errorMessage, bool goToPageHome);
void importErrorOccurred(ErrorCode errorCode, bool goToPageHome);
void qrDecodingFinished();
@@ -62,7 +64,7 @@ signals:
private:
QJsonObject extractOpenVpnConfig(const QString &data);
QJsonObject extractWireGuardConfig(const QString &data);
QJsonObject extractXrayConfig(const QString &data);
QJsonObject extractXrayConfig(const QString &data, const QString &description = "");
void checkForMaliciousStrings(const QJsonObject &protocolConfig);

View File

@@ -9,7 +9,6 @@
#include "core/controllers/serverController.h"
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "core/networkUtilities.h"
#include "logger.h"
#include "ui/models/protocols/awgConfigModel.h"
@@ -149,7 +148,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
QMap<DockerContainer, QJsonObject> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(serverCredentials, serverController, installedContainers);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
@@ -158,7 +157,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
if (!installedContainers.contains(container)) {
errorCode = serverController->setupContainer(serverCredentials, container, config);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
@@ -169,7 +168,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
}
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
@@ -204,7 +203,7 @@ void InstallController::installServer(const DockerContainer container, const QMa
auto errorCode = vpnConfigurationController.createProtocolConfigForContainer(m_processedServerCredentials, iterator.key(),
containerConfig);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
containerConfigs.append(containerConfig);
@@ -212,7 +211,7 @@ void InstallController::installServer(const DockerContainer container, const QMa
errorCode = m_clientManagementModel->appendClient(iterator.key(), serverCredentials, containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
} else {
@@ -244,7 +243,7 @@ void InstallController::installContainer(const DockerContainer container, const
auto errorCode =
vpnConfigurationController.createProtocolConfigForContainer(serverCredentials, iterator.key(), containerConfig);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
m_serversModel->addContainerConfig(iterator.key(), containerConfig);
@@ -252,7 +251,7 @@ void InstallController::installContainer(const DockerContainer container, const
errorCode = m_clientManagementModel->appendClient(iterator.key(), serverCredentials, containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
} else {
@@ -310,7 +309,7 @@ void InstallController::scanServerForInstalledContainers()
auto errorCode =
vpnConfigurationController.createProtocolConfigForContainer(serverCredentials, container, containerConfig);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
m_serversModel->addContainerConfig(container, containerConfig);
@@ -319,7 +318,7 @@ void InstallController::scanServerForInstalledContainers()
QString("Admin [%1]").arg(QSysInfo::prettyProductName()),
serverController);
if (errorCode) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return;
}
} else {
@@ -334,7 +333,7 @@ void InstallController::scanServerForInstalledContainers()
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentials &credentials,
@@ -515,7 +514,7 @@ void InstallController::updateContainer(QJsonObject config)
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
void InstallController::rebootProcessedServer()
@@ -528,7 +527,7 @@ void InstallController::rebootProcessedServer()
if (errorCode == ErrorCode::NoError) {
emit rebootProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName));
} else {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
}
@@ -552,7 +551,7 @@ void InstallController::removeAllContainers()
emit removeAllContainersFinished(tr("All containers from server '%1' have been removed").arg(serverName));
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
void InstallController::removeProcessedContainer()
@@ -570,7 +569,7 @@ void InstallController::removeProcessedContainer()
emit removeProcessedContainerFinished(tr("%1 has been removed from the server '%2'").arg(containerName, serverName));
return;
}
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
}
void InstallController::removeApiConfig(const int serverIndex)
@@ -738,7 +737,7 @@ bool InstallController::checkSshConnection(QSharedPointer<ServerController> serv
if (errorCode == ErrorCode::NoError) {
m_processedServerCredentials.secretData = decryptedPrivateKey;
} else {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return false;
}
}
@@ -747,7 +746,7 @@ bool InstallController::checkSshConnection(QSharedPointer<ServerController> serv
output = serverController->checkSshConnection(m_processedServerCredentials, errorCode);
if (errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(errorString(errorCode));
emit installationErrorOccurred(errorCode);
return false;
} else {
if (output.contains(tr("Please login as the user"))) {

View File

@@ -64,6 +64,7 @@ signals:
void removeProcessedContainerFinished(const QString &finishedMessage);
void installationErrorOccurred(const QString &errorMessage);
void installationErrorOccurred(ErrorCode errorCode);
void serverAlreadyExists(int serverIndex);

View File

@@ -1,4 +1,6 @@
#include "pageController.h"
#include "utils/converter.h"
#include "core/errorstrings.h"
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#include <QGuiApplication>
@@ -38,6 +40,8 @@ PageController::PageController(const QSharedPointer<ServersModel> &serversModel,
connect(this, &PageController::raiseMainWindow, []() { setDockIconVisible(true); });
connect(this, &PageController::hideMainWindow, []() { setDockIconVisible(false); });
#endif
connect(this, qOverload<ErrorCode>(&PageController::showErrorMessage), this, &PageController::onShowErrorMessage);
m_isTriggeredByConnectButton = false;
}
@@ -161,3 +165,13 @@ int PageController::getDrawerDepth()
{
return m_drawerDepth;
}
void PageController::onShowErrorMessage(ErrorCode errorCode)
{
const auto fullErrorMessage = errorString(errorCode);
const auto errorMessage = fullErrorMessage.mid(fullErrorMessage.indexOf(". ") + 1); // remove ErrorCode %1.
const auto errorUrl = QStringLiteral("https://docs.amnezia.org/troubleshooting/error-codes/#error-%1-%2").arg(static_cast<int>(errorCode)).arg(utils::enumToString(errorCode).toLower());
const auto fullMessage = QStringLiteral("<a href=\"%1\" style=\"color: #FBB26A;\">ErrorCode: %2</a>. %3").arg(errorUrl).arg(static_cast<int>(errorCode)).arg(errorMessage);
emit showErrorMessage(fullMessage);
}

View File

@@ -4,6 +4,7 @@
#include <QObject>
#include <QQmlEngine>
#include "core/defs.h"
#include "ui/models/servers_model.h"
namespace PageLoader
@@ -93,6 +94,9 @@ public slots:
void setDrawerDepth(const int depth);
int getDrawerDepth();
private slots:
void onShowErrorMessage(amnezia::ErrorCode errorCode);
signals:
void goToPage(PageLoader::PageEnum page, bool slide = true);
void goToStartPage();
@@ -107,6 +111,7 @@ signals:
void restorePageHomeState(bool isContainerInstalled = false);
void replaceStartPage();
void showErrorMessage(amnezia::ErrorCode);
void showErrorMessage(const QString &errorMessage);
void showNotificationMessage(const QString &message);

View File

@@ -51,7 +51,14 @@ void SystemController::saveFile(QString fileName, const QString &data)
return;
#else
QFileInfo fi(fileName);
QDesktopServices::openUrl(fi.absoluteDir().absolutePath());
#ifdef Q_OS_MAC
const auto url = "file://" + fi.absoluteDir().absolutePath();
#else
const auto url = fi.absoluteDir().absolutePath();
#endif
QDesktopServices::openUrl(url);
#endif
}

View File

@@ -580,6 +580,9 @@ bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
if (ContainerProps::containerService(container) == ServiceType::Vpn) {
return true;
}
if (container == DockerContainer::SSXray) {
return true;
}
}
return false;
}
@@ -623,4 +626,4 @@ bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
}
return false;
}
}

View File

@@ -57,7 +57,17 @@ Popup {
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
text: root.text
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
Item {

View File

@@ -76,9 +76,9 @@ PageType {
Connections {
target: InstallController
function onInstallationErrorOccurred(errorMessage) {
function onInstallationErrorOccurred(error) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(errorMessage)
PageController.showErrorMessage(error)
var currentPageName = stackView.currentItem.objectName
@@ -97,8 +97,8 @@ PageType {
PageController.showBusyIndicator(false)
}
function onImportErrorOccurred(errorMessage, goToPageHome) {
PageController.showErrorMessage(errorMessage)
function onImportErrorOccurred(error, goToPageHome) {
PageController.showErrorMessage(error)
}
}

View File

@@ -102,9 +102,10 @@ PageType {
PageController.showBusyIndicator(false)
}
function onExportErrorOccurred(errorMessage) {
function onExportErrorOccurred(error) {
shareConnectionDrawer.close()
PageController.showErrorMessage(errorMessage)
PageController.showErrorMessage(error)
}
}

View File

@@ -99,9 +99,10 @@ PageType {
Connections {
target: InstallController
function onInstallationErrorOccurred(errorMessage) {
function onInstallationErrorOccurred(error) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(errorMessage)
PageController.showErrorMessage(error)
var needCloseCurrentPage = false
var currentPageName = tabBarStackView.currentItem.objectName
@@ -146,8 +147,8 @@ PageType {
Connections {
target: ImportController
function onImportErrorOccurred(errorMessage, goToPageHome) {
PageController.showErrorMessage(errorMessage)
function onImportErrorOccurred(error, goToPageHome) {
PageController.showErrorMessage(error)
}
}

View File

@@ -6,6 +6,8 @@
#include <QRegularExpression>
#include <QStandardPaths>
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include "utilities.h"
#include "version.h"
@@ -23,6 +25,50 @@ QString Utils::getRandomString(int len)
return randomString;
}
QString Utils::VerifyJsonString(const QString &source)
{
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(source.toUtf8(), &error);
Q_UNUSED(doc)
if (error.error == QJsonParseError::NoError) {
return "";
} else {
qDebug() << "WARNING: Json parse returns: " + error.errorString();
return error.errorString();
}
}
QJsonObject Utils::JsonFromString(const QString &string)
{
auto removeComment = string.trimmed();
if (removeComment != string.trimmed()) {
qDebug() << "Some comments have been removed from the json.";
}
QJsonDocument doc = QJsonDocument::fromJson(removeComment.toUtf8());
return doc.object();
}
QString Utils::SafeBase64Decode(QString string)
{
QByteArray ba = string.replace(QChar('-'), QChar('+')).replace(QChar('_'), QChar('/')).toUtf8();
return QByteArray::fromBase64(ba, QByteArray::Base64Option::OmitTrailingEquals);
}
QString Utils::JsonToString(const QJsonObject &json, QJsonDocument::JsonFormat format)
{
QJsonDocument doc;
doc.setObject(json);
return doc.toJson(format);
}
QString Utils::JsonToString(const QJsonArray &array, QJsonDocument::JsonFormat format)
{
QJsonDocument doc;
doc.setArray(array);
return doc.toJson(format);
}
QString Utils::systemLogPath()
{
#ifdef Q_OS_WIN

View File

@@ -4,6 +4,7 @@
#include <QRegExp>
#include <QRegularExpression>
#include <QString>
#include <QJsonDocument>
#ifdef Q_OS_WIN
#include "Windows.h"
@@ -15,7 +16,11 @@ class Utils : public QObject
public:
static QString getRandomString(int len);
static QString SafeBase64Decode(QString string);
static QString VerifyJsonString(const QString &source);
static QString JsonToString(const QJsonObject &json, QJsonDocument::JsonFormat format);
static QString JsonToString(const QJsonArray &array, QJsonDocument::JsonFormat format);
static QJsonObject JsonFromString(const QString &string);
static QString executable(const QString &baseName, bool absPath);
static QString usrExecutable(const QString &baseName);
static QString systemLogPath();

25
client/utils/converter.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <QMetaEnum>
namespace utils
{
template<typename Enum>
QString enumToString(Enum value)
{
auto metaEnum = QMetaEnum::fromType<Enum>();
return metaEnum.valueToKey(static_cast<int>(value));
}
template<typename Enum>
Enum enumFromString(const QString &str, Enum defaultValue = {})
{
auto metaEnum = QMetaEnum::fromType<Enum>();
bool isOk;
auto value = metaEnum.keyToValue(str.toLatin1(), &isOk);
if (isOk) {
return static_cast<Enum>(value);
}
return defaultValue;
}
}

View File

@@ -1,5 +1,5 @@
sc stop WireGuardTunnel$AmneziaVPN
sc delete WireGuardTunnel$AmneziaVPN
sc stop AmneziaWGTunnel$AmneziaVPN
sc delete AmneziaWGTunnel$AmneziaVPN
taskkill /IM "AmneziaVPN-service.exe" /F
taskkill /IM "AmneziaVPN.exe" /F
exit /b 0

View File

@@ -5,8 +5,8 @@ echo %AmneziaPath%
timeout /t 1
sc stop AmneziaVPN-service
sc delete AmneziaVPN-service
sc stop WireGuardTunnel$AmneziaVPN
sc delete WireGuardTunnel$AmneziaVPN
sc stop AmneziaWGTunnel$AmneziaVPN
sc delete AmneziaWGTunnel$AmneziaVPN
taskkill /IM "AmneziaVPN-service.exe" /F
taskkill /IM "AmneziaVPN.exe" /F
exit /b 0

View File

@@ -1,5 +1,5 @@
sc stop WireGuardTunnel$AmneziaVPN
sc delete WireGuardTunnel$AmneziaVPN
sc stop AmneziaWGTunnel$AmneziaVPN
sc delete AmneziaWGTunnel$AmneziaVPN
taskkill /IM "AmneziaVPN-service.exe" /F
taskkill /IM "AmneziaVPN.exe" /F
exit /b 0

View File

@@ -5,8 +5,8 @@ echo %AmneziaPath%
timeout /t 1
sc stop AmneziaVPN-service
sc delete AmneziaVPN-service
sc stop WireGuardTunnel$AmneziaVPN
sc delete WireGuardTunnel$AmneziaVPN
sc stop AmneziaWGTunnel$AmneziaVPN
sc delete AmneziaWGTunnel$AmneziaVPN
taskkill /IM "AmneziaVPN-service.exe" /F
taskkill /IM "AmneziaVPN.exe" /F
exit /b 0

View File

@@ -0,0 +1,4 @@
What's Changed:
* Added app split tunneling
* Added a tile to the quick settings panel
* Bug fixes and improvements

View File

@@ -0,0 +1,15 @@
Get your own self-hosted VPN!
AmneziaVPN is a multi-protocol, open-source VPN client that offers the option to set up your own server.
Free open-source software to create a personal VPN on your server. Helps to access blocked content without revealing privacy even to VPN providers. Specify IP address, credentials for your server and Amnezia will configure it to connect via VPN automatically.
We are not a VPN service, you don't have to connect to our servers and pay us anything. You free to use your own self-hosted VPS or you could purchase any VPS from any provider. To connect, use your own or purchase any VPS from any provider.
Useful links
• https://amnezia.org - project website
• https://www.reddit.com/r/AmneziaVPN - Reddit
• https://t.me/amnezia_vpn_en - Telegram support channel (English)
• https://t.me/amnezia_vpn - Telegram support channel (Russian)
• https://t.me/amnezia_free_myanmar_bot - Telegram support channel (Burmese)

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 KiB

View File

@@ -0,0 +1 @@
A multi-protocol, open-source VPN client that offers the option to set up your own server.

1
metadata/en-US/title.txt Normal file
View File

@@ -0,0 +1 @@
Amnezia VPN