Compare commits

...

9 Commits

Author SHA1 Message Date
yp
db054b2449 fix: trim SSH server IP and submit credentials on Enter 2026-08-06 12:13:45 +03:00
yp
d702dc0b2a fix: various fixes for Telemt, XRay (VLESS), Captcha (#2903)
* fix worker & fix captha

* fixed read config (xxd) snapshot

* remove comment

* fix snapshot

* fixed xray save

* move subtitleText -> hint (Xray)

* xray config apply via reinstall

* apply server config via container reinstall

* transport compatibility and xray-core v26 config

* seed reality defaults on fresh deploy

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

* placeholder defaults and hint tooltip control

* wire placeholders and hints into settings pages

* gate flow and security by transport in UI

* encode raw tg link in QR

* remove comment

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

* remove drop SessionKey, add edit

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

* feat: add awg3 support to selfhosted

* chore: remove HeaderProtectionKey from ui

* fix: fixed regexp in configure_container.sh for awg

* chore: return server protocol version to config

* fix: fixed native wg obfuscation

* fix: fixed range validator for awg text field

* feat: add warning for awg 1/2

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

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

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

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

* chore: return error messages

---------

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

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

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

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

* feat: replace runpath for prebuilt openvpn

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

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

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

* fix: fix scrolling on user list

* fix: fixed container installation after adding a new server

* refactor: qt6.9.3 support for ios
2026-08-05 12:30:02 +08:00
111 changed files with 2305 additions and 1740 deletions

2
.gitignore vendored
View File

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

View File

@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 5.0.0.5)
set(AMNEZIAVPN_VERSION 5.0.1.0)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2138)
set(APP_ANDROID_VERSION_CODE 2141)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -26,6 +26,9 @@
#include "version.h"
#include "platforms/ios/QRCodeReaderBase.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ioscontextmenu.h"
#endif
bool AmneziaApplication::m_forceQuit = false;
@@ -139,6 +142,10 @@ void AmneziaApplication::init()
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
#endif
#ifdef Q_OS_IOS
m_engine->rootContext()->setContextProperty("IosContextMenu", new IosContextMenu(this));
#endif
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
m_vpnConnectionThread.start();

View File

@@ -31,6 +31,7 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -42,6 +43,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
@@ -49,6 +51,12 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
)
# The context menu helper uses ARC-only constructs (weak references); the
# rest of the Objective-C++ sources build with manual reference counting.
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
)
target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
@@ -69,7 +77,6 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
@@ -78,7 +85,7 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_EMBED_APP_EXTENSIONS networkextension
)
if(DEFINED DEPLOY)
if(DEPLOY)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"

View File

@@ -5,6 +5,7 @@ set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
enable_language(OBJC)
enable_language(OBJCXX)
enable_language(Swift)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Widgets)
@@ -33,7 +34,6 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -44,19 +44,12 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
)
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
set(MACOSX_BUNDLE_ICON_FILE app.icns)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})
target_include_directories(${PROJECT} PRIVATE
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
${Qt6Widgets_PRIVATE_INCLUDE_DIRS}
@@ -69,6 +62,7 @@ set_target_properties(${PROJECT} PROPERTIES
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
@@ -79,14 +73,10 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY "NO"
XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY "YES"
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks"
XCODE_EMBED_APP_EXTENSIONS AmneziaVPNNetworkExtension
)

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,16 @@ public:
bool appendNewClient,
QString *outClientId = nullptr);
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings);
private:
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
const QString &path, QString &out) const;
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);

View File

@@ -188,24 +188,14 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
// TODO looks like this block can be removed after v1 configs EOL
serverProtocolConfig[configKey::junkPacketCount] = clientProtocolConfig.value(configKey::junkPacketCount);
serverProtocolConfig[configKey::junkPacketMinSize] = clientProtocolConfig.value(configKey::junkPacketMinSize);
serverProtocolConfig[configKey::junkPacketMaxSize] = clientProtocolConfig.value(configKey::junkPacketMaxSize);
serverProtocolConfig[configKey::initPacketJunkSize] = clientProtocolConfig.value(configKey::initPacketJunkSize);
serverProtocolConfig[configKey::responsePacketJunkSize] = clientProtocolConfig.value(configKey::responsePacketJunkSize);
serverProtocolConfig[configKey::initPacketMagicHeader] = clientProtocolConfig.value(configKey::initPacketMagicHeader);
serverProtocolConfig[configKey::responsePacketMagicHeader] = clientProtocolConfig.value(configKey::responsePacketMagicHeader);
serverProtocolConfig[configKey::underloadPacketMagicHeader] = clientProtocolConfig.value(configKey::underloadPacketMagicHeader);
serverProtocolConfig[configKey::transportPacketMagicHeader] = clientProtocolConfig.value(configKey::transportPacketMagicHeader);
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
serverProtocolConfig[configKey::cookieReplyPacketJunkSize] = clientProtocolConfig.value(configKey::cookieReplyPacketJunkSize);
serverProtocolConfig[configKey::transportPacketJunkSize] = clientProtocolConfig.value(configKey::transportPacketJunkSize);
serverProtocolConfig[configKey::specialJunk1] = clientProtocolConfig.value(configKey::specialJunk1);
serverProtocolConfig[configKey::specialJunk2] = clientProtocolConfig.value(configKey::specialJunk2);
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
serverProtocolConfig[configKey::headerProtectionKey] = clientProtocolConfig.value(configKey::headerProtectionKey);
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = clientProtocolConfig.value(key);
if (value.isString() && !value.toString().isEmpty()) {
serverProtocolConfig[key] = value;
}
}
//

View File

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

View File

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

View File

@@ -572,47 +572,19 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
QString protocolName = configKey::wireguard;
QString protocolVersion;
ConfigTypes detectedType = ConfigTypes::WireGuard;
const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize,
configKey::junkPacketMaxSize, configKey::initPacketJunkSize,
configKey::responsePacketJunkSize, configKey::initPacketMagicHeader,
configKey::responsePacketMagicHeader, configKey::underloadPacketMagicHeader,
configKey::transportPacketMagicHeader };
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
configKey::transportPacketJunkSize,
configKey::specialJunk1, configKey::specialJunk2, configKey::specialJunk3,
configKey::specialJunk4, configKey::specialJunk5
};
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAllRequiredFields) {
for (const QString &field : requiredJunkFields) {
lastConfig[field] = configMap.value(field);
}
for (const QString &field : optionalJunkFields) {
if (!configMap.value(field).isEmpty()) {
lastConfig[field] = configMap.value(field);
bool hasAwgKeys = std::any_of(awgProtocolKeys.begin(), awgProtocolKeys.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAwgKeys) {
for (const QString &key : awgProtocolKeys) {
if (!configMap.value(key).isEmpty()) {
lastConfig[key] = configMap.value(key);
}
}
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
!configMap.value(configKey::specialJunk2).isEmpty() ||
!configMap.value(configKey::specialJunk3).isEmpty() ||
!configMap.value(configKey::specialJunk4).isEmpty() ||
!configMap.value(configKey::specialJunk5).isEmpty();
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
protocolVersion = "2";
} else if (hasSpecialJunk && !hasCookieReplyPacketJunkSize && !hasTransportPacketJunkSize) {
protocolVersion = "1.5";
}
protocolName = configKey::awg;
detectedType = ConfigTypes::Awg;
}
@@ -630,9 +602,6 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
wireguardConfig[configKey::isThirdPartyConfig] = true;
wireguardConfig[configKey::port] = port;
wireguardConfig[configKey::transportProto] = protocols::openvpn::defaultTransportProto;
if (protocolName == configKey::awg && !protocolVersion.isEmpty()) {
wireguardConfig[configKey::protocolVersion] = protocolVersion;
}
QJsonObject containers;
QString containerName = (protocolName == configKey::awg) ? configKey::amneziaAwg : configKey::amneziaWireguard;

View File

@@ -11,6 +11,7 @@
#include <QtConcurrent>
#include "core/configurators/configuratorBase.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -152,6 +153,15 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e;
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
if (e)
return e;
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
}
setupServerFirewall(credentials, sshSession);
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
@@ -191,11 +201,21 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
SshSession sshSession;
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
reinstallRequired = true;
}
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
errorCode = setupContainer(credentials, container, newConfig, true);
// Reinstall pulls the latest container image, so the server runs the latest protocol version
if (errorCode == ErrorCode::NoError && container == DockerContainer::Awg2) {
if (auto* awgConfig = newConfig.getAwgProtocolConfig()) {
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
}
}
} else {
errorCode = configureContainerWorker(credentials, container, newConfig, sshSession);
if (errorCode == ErrorCode::NoError) {
@@ -397,6 +417,11 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c
}
if (ContainerUtils::containerService(container) != ServiceType::Other) {
if ((container == DockerContainer::Xray || container == DockerContainer::SSXray)
&& containerConfig.protocolConfig.hasClientConfig()) {
return ErrorCode::NoError;
}
Proto protocol = ContainerUtils::defaultProtocol(container);
DnsSettings dnsSettings = {
@@ -484,6 +509,12 @@ ErrorCode InstallController::buildContainerWorker(const ServerCredentials &crede
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
return ErrorCode::DockerPullRateLimit;
if (stdOut.contains("returned a non-zero code")
|| stdOut.contains("failed to solve")
|| stdOut.contains("Unable to find image")
|| stdOut.contains("Couldn't connect to server"))
return ErrorCode::ServerDockerFailedError;
return error;
}
@@ -508,6 +539,8 @@ ErrorCode InstallController::runContainerWorker(const ServerCredentials &credent
return ErrorCode::ServerPortAlreadyAllocatedError;
if (stdOut.contains("invalid publish"))
return ErrorCode::ServerDockerFailedError;
if (stdOut.contains("Unable to find image") || stdOut.contains("No such image"))
return ErrorCode::ServerDockerFailedError;
return e;
}

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
@@ -20,6 +21,8 @@ namespace {
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxySecretPath("/data/secret");
constexpr QLatin1String kMtProxyMetaPath("/data/mtproxy-meta");
constexpr QLatin1String kMtProxyStartScriptPath("/opt/amnezia/start.sh");
}
MtProxyInstaller::MtProxyInstaller(QObject *parent)
@@ -53,14 +56,98 @@ ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container
}
}
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
const auto addExtra = [&](const QString &s) {
if (hex32.match(s).hasMatch() && !mt->additionalSecrets.contains(s)) {
mt->additionalSecrets.append(s);
}
};
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32) {
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (hex32.match(sec).hasMatch()) {
mt->secret = sec;
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
mt->secret = sec;
}
bool metaRestored = false;
ErrorCode metaErr = ErrorCode::NoError;
const QByteArray metaRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyMetaPath), metaErr);
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
QString mode, domain, workersMode, workers, natInternal, natExternal;
bool natEnabled = false;
const QList<QByteArray> lines = metaRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq);
const QString val = line.mid(eq + 1).trimmed();
if (key == QLatin1String("mode")) mode = val;
else if (key == QLatin1String("domain")) domain = val;
else if (key == QLatin1String("tag")) { if (mt->tag.isEmpty()) mt->tag = val; }
else if (key == QLatin1String("additional")) {
for (const QString &s : val.split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
else if (key == QLatin1String("workers_mode")) workersMode = val;
else if (key == QLatin1String("workers")) workers = val;
else if (key == QLatin1String("nat_enabled")) natEnabled = (val == QLatin1String("1"));
else if (key == QLatin1String("nat_internal")) natInternal = val;
else if (key == QLatin1String("nat_external")) natExternal = val;
else if (key == QLatin1String("public_host")) { if (mt->publicHost.isEmpty()) mt->publicHost = val; }
}
if (!mode.isEmpty()) {
mt->transportMode = mode;
if (!domain.isEmpty()) mt->tlsDomain = domain;
if (!workersMode.isEmpty()) mt->workersMode = workersMode;
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual) && !workers.isEmpty()) {
mt->workers = workers;
}
if (natEnabled) {
mt->natEnabled = true;
mt->natInternalIp = natInternal;
mt->natExternalIp = natExternal;
}
metaRestored = true;
}
}
if (!metaRestored) {
ErrorCode startErr = ErrorCode::NoError;
const QByteArray startRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyStartScriptPath), startErr);
if (startErr == ErrorCode::NoError && !startRaw.trimmed().isEmpty()) {
const QString start = QString::fromUtf8(startRaw);
static const QRegularExpression modeRe(QStringLiteral("\\[ \"(standard|faketls)\" = \"faketls\" \\]"));
const QRegularExpressionMatch m = modeRe.match(start);
if (m.hasMatch()) {
mt->transportMode = m.captured(1);
if (m.captured(1) == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) {
static const QRegularExpression domRe(QStringLiteral("--domain ([A-Za-z0-9.\\-]+)"));
const QRegularExpressionMatch dm = domRe.match(start);
if (dm.hasMatch()) mt->tlsDomain = dm.captured(1);
}
}
static const QRegularExpression tagRe(QStringLiteral("-P ([0-9a-fA-F]{32})"));
const QRegularExpressionMatch tm = tagRe.match(start);
if (tm.hasMatch() && mt->tag.isEmpty()) mt->tag = tm.captured(1);
static const QRegularExpression addRe(QStringLiteral("echo \"([0-9a-fA-F,]+)\" \\| tr ',' ' '"));
const QRegularExpressionMatch am = addRe.match(start);
if (am.hasMatch()) {
for (const QString &s : am.captured(1).split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
static const QRegularExpression natRe(QStringLiteral("NAT_VALUE=\"([0-9.]+):([0-9.]+)\""));
const QRegularExpressionMatch nm = natRe.match(start);
if (nm.hasMatch()) {
mt->natEnabled = true;
mt->natInternalIp = nm.captured(1);
mt->natExternalIp = nm.captured(2);
}
}
}

View File

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

View File

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

View File

@@ -2,6 +2,10 @@
#include <QJsonDocument>
#include <QJsonArray>
#include <QObject>
#include <QSet>
#include <algorithm>
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
@@ -13,6 +17,41 @@ using namespace ProtocolUtils;
namespace amnezia
{
namespace
{
template <typename T>
QString awgVersionOf(const T &config)
{
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
const QStringList awg3Params = { config.headerProtectionKey, config.contentPaddingAddition,
config.rekeyAfterTime, config.rekeyTimeout,
config.rejectAfterTime, config.keepaliveTimeout,
config.maxHandshakeAttempts };
if (std::any_of(awg3Params.begin(), awg3Params.end(), hasValue)) {
return protocols::awg::awgV3;
}
const QStringList junkSizes = { config.cookieReplyPacketJunkSize, config.transportPacketJunkSize };
const QStringList magicHeaders = { config.initPacketMagicHeader, config.responsePacketMagicHeader,
config.underloadPacketMagicHeader, config.transportPacketMagicHeader };
bool hasJunkSizes = std::any_of(junkSizes.begin(), junkSizes.end(), hasValue);
bool hasHeaderRanges = std::any_of(magicHeaders.begin(), magicHeaders.end(),
[](const QString &header) { return header.contains('-'); });
if (hasJunkSizes || hasHeaderRanges) {
return protocols::awg::awgV2;
}
const QStringList specialJunk = { config.specialJunk1, config.specialJunk2, config.specialJunk3,
config.specialJunk4, config.specialJunk5 };
if (std::any_of(specialJunk.begin(), specialJunk.end(), hasValue)) {
return protocols::awg::awgV1_5;
}
return QString();
}
} // namespace
QJsonObject AwgServerConfig::toJson() const
{
QJsonObject obj;
@@ -95,7 +134,7 @@ QJsonObject AwgServerConfig::toJson() const
if (!maxHandshakeAttempts.isEmpty()) {
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
}
if (isThirdPartyConfig) {
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
}
@@ -139,7 +178,7 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
return config;
@@ -263,11 +302,7 @@ QJsonObject AwgClientConfig::toJson() const
if (!maxHandshakeAttempts.isEmpty()) {
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
}
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
}
return obj;
}
@@ -318,21 +353,18 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
return config;
}
QJsonObject AwgProtocolConfig::toJson() const
{
QJsonObject obj = serverConfig.toJson();
if (clientConfig.has_value()) {
QJsonObject clientJson = clientConfig->toJson();
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
}
return obj;
}
@@ -353,6 +385,24 @@ AwgProtocolConfig AwgProtocolConfig::fromJson(const QJsonObject& json)
return config;
}
QString AwgProtocolConfig::serverProtocolVersion() const
{
return awgVersionOf(serverConfig);
}
QString AwgProtocolConfig::clientProtocolVersion() const
{
return clientConfig.has_value() ? awgVersionOf(clientConfig.value()) : QString();
}
QString AwgProtocolConfig::protocolVersionString(const QString &version)
{
if (version == protocols::awg::awgV3) return QObject::tr(" (version 3)");
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}
bool AwgProtocolConfig::hasClientConfig() const
{
return clientConfig.has_value();
@@ -381,24 +431,30 @@ bool AwgServerConfig::hasEqualServerSettings(const AwgServerConfig& other) const
specialJunk1 != other.specialJunk1 || specialJunk2 != other.specialJunk2 ||
specialJunk3 != other.specialJunk3 || specialJunk4 != other.specialJunk4 ||
specialJunk5 != other.specialJunk5 ||
headerProtectionKey != other.headerProtectionKey) {
cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize ||
headerProtectionKey != other.headerProtectionKey ||
contentPaddingAddition != other.contentPaddingAddition ||
rekeyAfterTime != other.rekeyAfterTime || rekeyTimeout != other.rekeyTimeout ||
rejectAfterTime != other.rejectAfterTime || keepaliveTimeout != other.keepaliveTimeout ||
maxHandshakeAttempts != other.maxHandshakeAttempts) {
return false;
}
bool isV2 = protocolVersion == protocols::awg::awgV2;
if (isV2) {
if (cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize) {
return false;
}
}
return true;
}
bool AwgProtocolConfig::isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4)
{
return (h1 == h2) || (h1 == h3) || (h1 == h4) || (h2 == h3) || (h2 == h4) || (h3 == h4);
QSet<QString> uniqueHeaders;
int filledHeaders = 0;
for (const QString &header : { h1, h2, h3, h4 }) {
if (!header.trimmed().isEmpty()) {
++filledHeaders;
uniqueHeaders.insert(header);
}
}
return uniqueHeaders.size() != filledHeaders;
}
bool AwgProtocolConfig::isPacketSizeEqual(int s1, int s2, int s3, int s4)

View File

@@ -90,8 +90,6 @@ struct AwgClientConfig {
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
bool isObfuscationEnabled = false;
QJsonObject toJson() const;
static AwgClientConfig fromJson(const QJsonObject& json);
};
@@ -103,6 +101,10 @@ struct AwgProtocolConfig {
QJsonObject toJson() const;
static AwgProtocolConfig fromJson(const QJsonObject& json);
QString serverProtocolVersion() const;
QString clientProtocolVersion() const;
static QString protocolVersionString(const QString &version);
bool hasClientConfig() const;
void setClientConfig(const AwgClientConfig& config);
void clearClientConfig();

View File

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

View File

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

View File

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

View File

@@ -81,7 +81,6 @@ QJsonObject XrayXhttpConfig::toJson() const
if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode;
if (!host.isEmpty()) obj[configKey::xhttpHost] = host;
if (!path.isEmpty()) obj[configKey::xhttpPath] = path;
if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate;
if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod;
obj[configKey::xhttpDisableGrpc] = disableGrpc;
obj[configKey::xhttpDisableSse] = disableSse;
@@ -116,7 +115,6 @@ namespace
c.mode = QString();
c.host = QString();
c.path = QString();
c.headersTemplate = QString();
c.uplinkMethod = QString();
c.disableGrpc = false;
c.disableSse = false;
@@ -155,9 +153,6 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
if (json.contains(configKey::xhttpPath)) {
c.path = json.value(configKey::xhttpPath).toString();
}
if (json.contains(configKey::xhttpHeadersTemplate)) {
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
}
if (json.contains(configKey::xhttpUplinkMethod)) {
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
}

View File

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

View File

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

View File

@@ -222,6 +222,18 @@ ErrorCode OpenVpnProtocol::start()
}
#endif
#ifdef Q_OS_WIN
// In "all except sites" mode the config uses redirect-gateway !ipv4, so OpenVPN
// never reports net_route_v4_best_gw and m_routeGateway would stay empty
const QString winGateway = NetworkUtilities::getGatewayAndIface().first;
if (!winGateway.isEmpty()) {
m_routeGateway = winGateway;
qDebug() << "Set VPN route gateway" << m_routeGateway;
} else {
qWarning() << "Unable to detect physical default gateway";
}
#endif
uint mgmtPort = selectMgmtPort();
qDebug() << "OpenVpnProtocol::start mgmt port selected:" << mgmtPort;

View File

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

View File

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

View File

@@ -120,34 +120,60 @@ QVariantMap SecureAppSettingsRepository::vpnSites(RouteMode mode) const
return value("Conf/" + routeModeString(mode)).toMap();
}
QStringList SecureAppSettingsRepository::siteIpList(const QVariant &value)
{
// QVariant::toStringList() handles both a QStringList/QVariantList and a single QString
// (a single string is returned as a one-element list), which covers the legacy format.
QStringList result = value.toStringList();
result.removeAll(QString());
result.removeDuplicates();
return result;
}
void SecureAppSettingsRepository::setVpnSites(RouteMode mode, const QVariantMap &sites)
{
setValue("Conf/" + routeModeString(mode), sites);
}
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
bool SecureAppSettingsRepository::addVpnSite(RouteMode mode, const QString &site, const QStringList &ips)
{
QVariantMap sites = vpnSites(mode);
if (sites.contains(site) && ip.isEmpty())
const bool siteExisted = sites.contains(site);
if (siteExisted && ips.isEmpty())
return false;
sites.insert(site, ip);
QStringList mergedIps = siteIpList(sites.value(site));
bool changed = !siteExisted;
for (const QString &ip : ips) {
if (!ip.isEmpty() && !mergedIps.contains(ip)) {
mergedIps.append(ip);
changed = true;
}
}
if (!changed)
return false;
sites.insert(site, mergedIps);
setVpnSites(mode, sites);
emit sitesChanged(mode);
return true;
}
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QString> &sites)
void SecureAppSettingsRepository::addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites)
{
QVariantMap allSites = vpnSites(mode);
for (auto i = sites.constBegin(); i != sites.constEnd(); ++i) {
const QString &site = i.key();
const QString &ip = i.value();
if (allSites.contains(site) && allSites.value(site) == ip)
continue;
QStringList mergedIps = siteIpList(allSites.value(site));
for (const QString &ip : i.value()) {
if (!ip.isEmpty() && !mergedIps.contains(ip))
mergedIps.append(ip);
}
allSites.insert(site, ip);
allSites.insert(site, mergedIps);
}
setVpnSites(mode, allSites);

View File

@@ -38,11 +38,15 @@ public:
RouteMode routeMode() const;
void setRouteMode(RouteMode mode);
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites);
bool addVpnSite(RouteMode mode, const QString &site, const QStringList &ips = {});
void addVpnSites(RouteMode mode, const QMap<QString, QStringList> &sites);
void removeVpnSite(RouteMode mode, const QString &site);
void removeAllVpnSites(RouteMode mode);
QVariantMap vpnSites(RouteMode mode) const;
// Normalizes a stored vpn site value into a list of IPs.
// Supports both the legacy format (a single IP string) and the current one (a list of IPs).
static QStringList siteIpList(const QVariant &value);
bool isSitesSplitTunnelingEnabled() const;
void setSitesSplitTunnelingEnabled(bool enabled);

View File

@@ -2,6 +2,7 @@
#define CONFIGKEYS_H
#include <QLatin1String>
#include <QStringList>
namespace amnezia
{
@@ -70,6 +71,8 @@ namespace amnezia
constexpr QLatin1String lastConfig("last_config");
constexpr QLatin1String protocolVersion("protocol_version");
constexpr QLatin1String isThirdPartyConfig("isThirdPartyConfig");
constexpr QLatin1String isObfuscationEnabled("isObfuscationEnabled");
@@ -98,7 +101,32 @@ namespace amnezia
constexpr QLatin1String keepaliveTimeout("KeepaliveTimeout");
constexpr QLatin1String maxHandshakeAttempts("MaxHandshakeAttempts");
constexpr QLatin1String protocolVersion("protocol_version");
inline QStringList awgProtocolKeys()
{
return { junkPacketCount,
junkPacketMinSize,
junkPacketMaxSize,
initPacketJunkSize,
responsePacketJunkSize,
cookieReplyPacketJunkSize,
transportPacketJunkSize,
initPacketMagicHeader,
responsePacketMagicHeader,
underloadPacketMagicHeader,
transportPacketMagicHeader,
specialJunk1,
specialJunk2,
specialJunk3,
specialJunk4,
specialJunk5,
headerProtectionKey,
contentPaddingAddition,
rekeyAfterTime,
rekeyTimeout,
rejectAfterTime,
keepaliveTimeout,
maxHandshakeAttempts };
}
constexpr QLatin1String openvpn("openvpn");
constexpr QLatin1String wireguard("wireguard");
@@ -158,7 +186,6 @@ namespace amnezia
constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one
constexpr QLatin1String xhttpHost("xhttp_host");
constexpr QLatin1String xhttpPath("xhttp_path");
constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None
constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH
constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool
constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool

View File

@@ -65,13 +65,12 @@ namespace amnezia
constexpr char defaultTransport[] = "raw";
constexpr char defaultFingerprint[] = "chrome";
constexpr char defaultSni[] = "www.googletagmanager.com";
constexpr char defaultAlpn[] = "HTTP/2";
constexpr char defaultAlpn[] = "h2";
constexpr char defaultXhttpMode[] = "Auto";
constexpr char defaultXhttpHeadersTemplate[] = "HTTP";
constexpr char defaultXhttpUplinkMethod[] = "POST";
constexpr char defaultXhttpSessionPlacement[] = "Path";
constexpr char defaultXhttpSessionKey[] = "Path";
constexpr char defaultXhttpSessionKey[] = "";
constexpr char defaultXhttpSeqPlacement[] = "Path";
constexpr char defaultXhttpUplinkDataPlacement[] = "Body";
@@ -86,6 +85,10 @@ namespace amnezia
constexpr char defaultXPaddingPlacement[] = "Cookie";
constexpr char defaultXPaddingMethod[] = "Repeat-x";
constexpr char defaultXPaddingKey[] = "x_padding";
constexpr char defaultXPaddingHeader[] = "X-Padding";
constexpr char defaultXPaddingBytesMin[] = "1";
constexpr char defaultXPaddingBytesMax[] = "256";
constexpr char defaultMkcpTti[] = "50";
constexpr char defaultMkcpUplinkCapacity[] = "5";
@@ -183,15 +186,16 @@ namespace amnezia
constexpr char defaultJunkPacketCount[] = "3";
constexpr char defaultJunkPacketMinSize[] = "10";
constexpr char defaultJunkPacketMaxSize[] = "30";
constexpr char defaultInitPacketJunkSize[] = "15";
constexpr char defaultResponsePacketJunkSize[] = "18";
constexpr char defaultCookieReplyPacketJunkSize[] = "20";
constexpr char defaultTransportPacketJunkSize[] = "23";
constexpr int junkPacketSizeMin = 12;
constexpr int initPacketJunkSizeMax = 150;
constexpr int responsePacketJunkSizeMax = 150;
constexpr int cookieReplyPacketJunkSizeMax = 64;
constexpr int defaultTransportPacketJunkSize = 12;
constexpr char defaultInitPacketMagicHeader[] = "1020325451";
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
constexpr char defaultInitPacketMagicHeader[] = "1";
constexpr char defaultResponsePacketMagicHeader[] = "2";
constexpr char defaultUnderloadPacketMagicHeader[] = "3";
constexpr char defaultTransportPacketMagicHeader[] = "4";
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
constexpr char defaultSpecialJunk2[] = "";
constexpr char defaultSpecialJunk3[] = "";
@@ -200,6 +204,17 @@ namespace amnezia
constexpr char awgV1_5[] = "1.5";
constexpr char awgV2[] = "2";
constexpr char awgV3[] = "3";
constexpr char defaultContentPaddingAddition[] = "10-100";
constexpr char defaultRekeyAfterTime[] = "100-120";
constexpr char defaultRekeyTimeout[] = "3-7";
constexpr char defaultRejectAfterTime[] = "150-180";
constexpr char defaultKeepaliveTimeout[] = "5-15";
constexpr char defaultMaxHandshakeAttempts[] = "15-20";
constexpr char defaultPersistentKeepAlive[] = "25-35";
}
namespace socks5Proxy
@@ -235,7 +250,8 @@ namespace amnezia
constexpr char defaultPort[] = "443";
constexpr char defaultWorkers[] = "2";
constexpr int maxWorkers = 32;
// mtproto-proxy loses connectivity with -M >= 20; keep the cap at the highest known-good value.
constexpr int maxWorkers = 19;
constexpr int botTagHexLength = 32;
constexpr char defaultTlsDomain[] = "googletagmanager.com";
}
@@ -254,7 +270,6 @@ namespace amnezia
constexpr char tlsEmulationKey[] = "telemt_tls_emulation";
constexpr char useMiddleProxyKey[] = "telemt_use_middle_proxy";
constexpr char userNameKey[] = "telemt_user_name";
// Stored for UI only (Telemt server ignores these; same controls as MTProxy page)
constexpr char additionalSecretsKey[] = "telemt_additional_secrets";
constexpr char workersKey[] = "telemt_workers";
constexpr char workersModeKey[] = "telemt_workers_mode";

View File

@@ -24,6 +24,13 @@ QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
return chunks;
}
QString qrCodeUtils::generatePlainQrCodeImage(const QByteArray &data)
{
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data, qrcodegen::QrCode::Ecc::LOW);
QString svg = QString::fromStdString(toSvgString(qr, 1));
return svgToBase64(svg);
}
QString qrCodeUtils::svgToBase64(const QString &image)
{
return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data());

View File

@@ -10,6 +10,7 @@ namespace qrCodeUtils
constexpr const qint16 qrMagicCode = 1984;
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
QString generatePlainQrCodeImage(const QByteArray &data);
qrcodegen::QrCode generateQrCode(const QByteArray &data);
QString svgToBase64(const QString &image);
};

View File

@@ -254,8 +254,20 @@ amnezia::ScriptVars amnezia::genAwgVars(const ContainerConfig &containerConfig)
vars.append({ { "$SPECIAL_JUNK_3", config.specialJunk3 } });
vars.append({ { "$SPECIAL_JUNK_4", config.specialJunk4 } });
vars.append({ { "$SPECIAL_JUNK_5", config.specialJunk5 } });
const bool isAwg3 = config.protocolVersion == protocols::awg::awgV3;
vars.append({ { "$PERSISTENT_KEEPALIVE", isAwg3 ? QString(protocols::awg::defaultPersistentKeepAlive)
: QString(protocols::wireguard::defaultPersistentKeepAlive) } });
vars.append({ { "$HEADER_PROTECTION_KEY", config.headerProtectionKey } });
vars.append({ { "$CONTENT_PADDING_ADDITION", config.contentPaddingAddition } });
vars.append({ { "$REKEY_AFTER_TIME", config.rekeyAfterTime } });
vars.append({ { "$REKEY_TIMEOUT", config.rekeyTimeout } });
vars.append({ { "$REJECT_AFTER_TIME", config.rejectAfterTime } });
vars.append({ { "$KEEPALIVE_TIMEOUT", config.keepaliveTimeout } });
vars.append({ { "$MAX_HANDSHAKE_ATTEMPTS", config.maxHandshakeAttempts } });
}
return vars;
}
@@ -329,6 +341,7 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf
workers = (transportMode == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) ? QStringLiteral("0")
: QStringLiteral("2");
}
vars.append({{"$MTPROXY_WORKERS_MODE", workersMode}});
vars.append({{"$MTPROXY_WORKERS", workers}});
vars.append({{"$MTPROXY_NAT_ENABLED", c.natEnabled ? QStringLiteral("1") : QStringLiteral("0")}});
@@ -375,6 +388,12 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
}
}
vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } });
QString middleProxyNatIp;
if (c.natEnabled && !c.natExternalIp.isEmpty()) {
middleProxyNatIp = c.natExternalIp;
}
vars.append({ { "$TELEMT_MIDDLE_PROXY_NAT_IP", middleProxyNatIp } });
}
return vars;

View File

@@ -176,7 +176,8 @@ QByteArray SshSession::getTextFileFromContainer(DockerContainer container, const
errorCode = ErrorCode::NoError;
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2'\"").arg(ContainerUtils::containerToString(container), path);
QString script = QStringLiteral("sudo docker exec -i %1 sh -c \"xxd -p '%2' 2>/dev/null || od -An -v -tx1 '%2'\"")
.arg(ContainerUtils::containerToString(container), path);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {

View File

@@ -118,42 +118,44 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
out << "DNS = " << dnsServers.join(", ") << "\n";
}
if (!m_junkPacketCount.isNull()) {
if (!m_junkPacketCount.isEmpty()) {
out << "Jc = " << m_junkPacketCount << "\n";
}
if (!m_junkPacketMinSize.isNull()) {
if (!m_junkPacketMinSize.isEmpty()) {
out << "JMin = " << m_junkPacketMinSize << "\n";
}
if (!m_junkPacketMaxSize.isNull()) {
if (!m_junkPacketMaxSize.isEmpty()) {
out << "JMax = " << m_junkPacketMaxSize << "\n";
}
if (!m_initPacketJunkSize.isNull()) {
if (!m_initPacketJunkSize.isEmpty()) {
out << "S1 = " << m_initPacketJunkSize << "\n";
}
if (!m_responsePacketJunkSize.isNull()) {
if (!m_responsePacketJunkSize.isEmpty()) {
out << "S2 = " << m_responsePacketJunkSize << "\n";
}
if (!m_cookieReplyPacketJunkSize.isNull()) {
if (!m_cookieReplyPacketJunkSize.isEmpty()) {
out << "S3 = " << m_cookieReplyPacketJunkSize << "\n";
}
if (!m_transportPacketJunkSize.isNull()) {
if (!m_transportPacketJunkSize.isEmpty()) {
out << "S4 = " << m_transportPacketJunkSize << "\n";
}
if (!m_initPacketMagicHeader.isNull()) {
if (!m_initPacketMagicHeader.isEmpty()) {
out << "H1 = " << m_initPacketMagicHeader << "\n";
}
if (!m_responsePacketMagicHeader.isNull()) {
if (!m_responsePacketMagicHeader.isEmpty()) {
out << "H2 = " << m_responsePacketMagicHeader << "\n";
}
if (!m_underloadPacketMagicHeader.isNull()) {
if (!m_underloadPacketMagicHeader.isEmpty()) {
out << "H3 = " << m_underloadPacketMagicHeader << "\n";
}
if (!m_transportPacketMagicHeader.isNull()) {
if (!m_transportPacketMagicHeader.isEmpty()) {
out << "H4 = " << m_transportPacketMagicHeader << "\n";
}
for (const QString& key : m_specialJunk.keys()) {
out << key << " = " << m_specialJunk[key] << "\n";
if (!m_specialJunk[key].isEmpty()) {
out << key << " = " << m_specialJunk[key] << "\n";
}
}
if (!m_headerProtectionKey.isEmpty()) {

View File

@@ -18,11 +18,10 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"

View File

@@ -31,8 +31,6 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(DEVELOPMENT_TEAM).*</string>

View File

@@ -7,21 +7,14 @@ add_executable(AmneziaVPNNetworkExtension)
message("executable_path is: @executable_path/../../Frameworks")
set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
XCODE_PRODUCT_TYPE com.apple.product-type.app-extension
# MACOSX_BUNDLE YES
BUNDLE_EXTENSION appex
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPNNetworkExtension"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPNNetworkExtension"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks"

View File

@@ -24,7 +24,7 @@
<false/>
<key>LSMinimumSystemVersion</key>
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>CFBundleDisplayName</key>
<string>AmneziaVPNNetworkExtension</string>

View File

@@ -18,6 +18,7 @@
#include <QLocalSocket>
#include <QObject>
#include <QStandardPaths>
#include <QStringList>
#include <QTimer>
#include "leakdetector.h"
@@ -251,64 +252,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert(amnezia::configKey::killSwitchOption, rawConfig.value(amnezia::configKey::killSwitchOption));
if (protocolName == amnezia::configKey::awg) {
json.insert(amnezia::configKey::junkPacketCount, wgConfig.value(amnezia::configKey::junkPacketCount));
json.insert(amnezia::configKey::junkPacketMinSize, wgConfig.value(amnezia::configKey::junkPacketMinSize));
json.insert(amnezia::configKey::junkPacketMaxSize, wgConfig.value(amnezia::configKey::junkPacketMaxSize));
json.insert(amnezia::configKey::initPacketJunkSize, wgConfig.value(amnezia::configKey::initPacketJunkSize));
json.insert(amnezia::configKey::responsePacketJunkSize, wgConfig.value(amnezia::configKey::responsePacketJunkSize));
json.insert(amnezia::configKey::cookieReplyPacketJunkSize, wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize));
json.insert(amnezia::configKey::transportPacketJunkSize, wgConfig.value(amnezia::configKey::transportPacketJunkSize));
json.insert(amnezia::configKey::initPacketMagicHeader, wgConfig.value(amnezia::configKey::initPacketMagicHeader));
json.insert(amnezia::configKey::responsePacketMagicHeader, wgConfig.value(amnezia::configKey::responsePacketMagicHeader));
json.insert(amnezia::configKey::underloadPacketMagicHeader, wgConfig.value(amnezia::configKey::underloadPacketMagicHeader));
json.insert(amnezia::configKey::transportPacketMagicHeader, wgConfig.value(amnezia::configKey::transportPacketMagicHeader));
json.insert(amnezia::configKey::specialJunk1, wgConfig.value(amnezia::configKey::specialJunk1));
json.insert(amnezia::configKey::specialJunk2, wgConfig.value(amnezia::configKey::specialJunk2));
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
} else if (!wgConfig.value(amnezia::configKey::junkPacketCount).isUndefined()
&& !wgConfig.value(amnezia::configKey::junkPacketMinSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::junkPacketMaxSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::initPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::responsePacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::transportPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::configKey::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::configKey::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::configKey::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::configKey::transportPacketMagicHeader).isUndefined()) {
json.insert(amnezia::configKey::junkPacketCount, wgConfig.value(amnezia::configKey::junkPacketCount));
json.insert(amnezia::configKey::junkPacketMinSize, wgConfig.value(amnezia::configKey::junkPacketMinSize));
json.insert(amnezia::configKey::junkPacketMaxSize, wgConfig.value(amnezia::configKey::junkPacketMaxSize));
json.insert(amnezia::configKey::initPacketJunkSize, wgConfig.value(amnezia::configKey::initPacketJunkSize));
json.insert(amnezia::configKey::responsePacketJunkSize, wgConfig.value(amnezia::configKey::responsePacketJunkSize));
json.insert(amnezia::configKey::cookieReplyPacketJunkSize, wgConfig.value(amnezia::configKey::cookieReplyPacketJunkSize));
json.insert(amnezia::configKey::transportPacketJunkSize, wgConfig.value(amnezia::configKey::transportPacketJunkSize));
json.insert(amnezia::configKey::initPacketMagicHeader, wgConfig.value(amnezia::configKey::initPacketMagicHeader));
json.insert(amnezia::configKey::responsePacketMagicHeader, wgConfig.value(amnezia::configKey::responsePacketMagicHeader));
json.insert(amnezia::configKey::underloadPacketMagicHeader, wgConfig.value(amnezia::configKey::underloadPacketMagicHeader));
json.insert(amnezia::configKey::transportPacketMagicHeader, wgConfig.value(amnezia::configKey::transportPacketMagicHeader));
json.insert(amnezia::configKey::specialJunk1, wgConfig.value(amnezia::configKey::specialJunk1));
json.insert(amnezia::configKey::specialJunk2, wgConfig.value(amnezia::configKey::specialJunk2));
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
const QStringList awgProtocolKeys = amnezia::configKey::awgProtocolKeys();
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = wgConfig.value(key);
if (value.isString() && !value.toString().isEmpty()) {
json.insert(key, value);
}
}
write(json);

View File

@@ -7,10 +7,6 @@ struct OpenVPNConfig: Decodable {
let config: String
let splitTunnelType: Int
let splitTunnelSites: [String]
var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
}
}
extension PacketTunnelProvider {
@@ -30,11 +26,6 @@ extension PacketTunnelProvider {
do {
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
let ovpnPreview = String(openVPNConfig.config.prefix(512))
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
splitTunnelType = openVPNConfig.splitTunnelType
splitTunnelSites = openVPNConfig.splitTunnelSites
@@ -105,12 +96,6 @@ extension PacketTunnelProvider {
let hasTlsAuthClose = configString.contains("</tls-auth>")
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
let lines = configString.split(separator: "\n")
let head = lines.prefix(10).joined(separator: "\n")
let tail = lines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigHead", message: head)
ovpnLog(.debug, title: "ConfigTail", message: tail)
if hasTlsAuthOpen && hasTlsAuthClose {
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
}
@@ -155,8 +140,6 @@ extension PacketTunnelProvider {
normalizedConfig.append("\n")
}
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
let redirectLines = normalizedLines
.map(String.init)
.filter { $0.lowercased().contains("redirect-gateway") }

View File

@@ -16,7 +16,6 @@ extension PacketTunnelProvider {
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)

View File

@@ -148,24 +148,4 @@ struct WGConfig: Decodable {
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
"""
}
var redux: String {
"""
[Interface]
Address = \(clientIP)
DNS = \(dns1), \(dns2)
MTU = \(mtu)
PrivateKey = ***
\(settings)
[Peer]
PublicKey = ***
PresharedKey = ***
AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(hostName):\(port)
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
SplitTunnelType = \(splitTunnelType)
SplitTunnelSites = \(splitTunnelSites.joined(separator: ", "))
"""
}
}

View File

@@ -552,6 +552,18 @@ bool IosController::setupOpenVPN()
return startOpenVPN(openVPNConfigStr);
}
static void insertNonEmptyAwgParams(QJsonObject &wgConfig, const QJsonObject &config)
{
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = config.value(key);
if (value.isString() && !value.toString().isEmpty()) {
wgConfig.insert(key, value);
}
}
}
bool IosController::setupWireGuard()
{
QJsonObject config = m_rawConfig[ProtocolUtils::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
@@ -593,29 +605,7 @@ bool IosController::setupWireGuard()
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
}
if (config.contains(configKey::isObfuscationEnabled) && config.value(configKey::isObfuscationEnabled).toBool()) {
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
wgConfig.insert(configKey::underloadPacketMagicHeader, config[configKey::underloadPacketMagicHeader]);
wgConfig.insert(configKey::transportPacketMagicHeader, config[configKey::transportPacketMagicHeader]);
wgConfig.insert(configKey::initPacketJunkSize, config[configKey::initPacketJunkSize]);
wgConfig.insert(configKey::responsePacketJunkSize, config[configKey::responsePacketJunkSize]);
wgConfig.insert(configKey::cookieReplyPacketJunkSize, config[configKey::cookieReplyPacketJunkSize]);
wgConfig.insert(configKey::transportPacketJunkSize, config[configKey::transportPacketJunkSize]);
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
}
insertNonEmptyAwgParams(wgConfig, config);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
@@ -705,33 +695,7 @@ bool IosController::setupAwg()
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
}
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
wgConfig.insert(configKey::underloadPacketMagicHeader, config[configKey::underloadPacketMagicHeader]);
wgConfig.insert(configKey::transportPacketMagicHeader, config[configKey::transportPacketMagicHeader]);
wgConfig.insert(configKey::initPacketJunkSize, config[configKey::initPacketJunkSize]);
wgConfig.insert(configKey::responsePacketJunkSize, config[configKey::responsePacketJunkSize]);
wgConfig.insert(configKey::cookieReplyPacketJunkSize, config[configKey::cookieReplyPacketJunkSize]);
wgConfig.insert(configKey::transportPacketJunkSize, config[configKey::transportPacketJunkSize]);
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
wgConfig.insert(configKey::specialJunk1, config[configKey::specialJunk1]);
wgConfig.insert(configKey::specialJunk2, config[configKey::specialJunk2]);
wgConfig.insert(configKey::specialJunk3, config[configKey::specialJunk3]);
wgConfig.insert(configKey::specialJunk4, config[configKey::specialJunk4]);
wgConfig.insert(configKey::specialJunk5, config[configKey::specialJunk5]);
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
insertNonEmptyAwgParams(wgConfig, config);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));

View File

@@ -0,0 +1,24 @@
#ifndef IOSCONTEXTMENU_H
#define IOSCONTEXTMENU_H
#include <QObject>
#include <QQuickItem>
// Presents the native iOS edit menu (UIEditMenuInteraction) for a text
// control. The menu items (Cut/Copy/Paste/Select All) are provided by the
// system based on the first responder, which is Qt's text input responder
// for the focused control.
//
// Needed because the ContextMenu attached type is backed by a native menu
// on iOS only since Qt 6.10 — on Qt 6.9 it opens a Qt-drawn menu instead.
class IosContextMenu : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
Q_INVOKABLE bool isAvailable() const;
Q_INVOKABLE void present(QQuickItem *target, qreal x, qreal y);
};
#endif // IOSCONTEXTMENU_H

View File

@@ -0,0 +1,215 @@
#import "ioscontextmenu.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#include <QtCore/QCoreApplication>
#include <QtCore/QPointer>
#include <QtGui/QGuiApplication>
#include <QtGui/QInputMethod>
#include <QtQuick/QQuickWindow>
namespace
{
// Keys for attaching the helper objects to the UIView.
const void *kEditMenuInteractionKey = &kEditMenuInteractionKey;
const void *kEditMenuDelegateKey = &kEditMenuDelegateKey;
const void *kEditMenuResponderKey = &kEditMenuResponderKey;
// Menu titles reuse the ContextMenuType translations; the accelerator
// ampersands are meaningless on iOS and get stripped.
NSString *menuTitle(const char *sourceText)
{
QString title = QCoreApplication::translate("ContextMenuType", sourceText);
title.remove(QLatin1Char('&'));
return title.toNSString();
}
// The handler outlives the delegate call, so it must own its own copy of the
// guarded pointer: the local variable here is captured by the block by value
// (copy-constructed when the block is copied to the heap). Capturing a
// C++ lambda's reference capture instead would leave the block with a
// dangling pointer into the delegate method's stack frame.
API_AVAILABLE(ios(16.0))
UIAction *makeEditAction(NSString *title, const QPointer<QQuickItem> &target, const char *slot)
{
const QPointer<QQuickItem> guardedTarget = target;
return [UIAction actionWithTitle:title
image:nil
identifier:nil
handler:^(UIAction *) {
if (QQuickItem *item = guardedTarget.data()) {
// Queued: the handler fires mid-dismissal
// of the menu, let UIKit unwind first.
QMetaObject::invokeMethod(item, slot, Qt::QueuedConnection);
}
}];
}
}
// UIKit presents an edit menu only when the first responder is inside the
// interaction view's hierarchy. While the virtual keyboard is up that is
// Qt's text input responder, but for read-only fields (or before the
// keyboard appears) nothing suitable is first responder and the present is
// silently ignored ("did not have performable commands and/or actions").
// This zero-sized subview steps in as the first responder for those cases.
@interface AmneziaEditMenuResponderView : UIView
@end
@implementation AmneziaEditMenuResponderView
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end
// Builds the edit menu from the state of the focused QML text control and
// invokes its slots directly. The system's suggested actions can't be used:
// they are collected from the first responder, and Qt's text responder is
// first responder only while the virtual keyboard is up (never for read-only
// fields), which would leave the menu empty.
API_AVAILABLE(ios(16.0))
@interface AmneziaEditMenuDelegate : NSObject <UIEditMenuInteractionDelegate>
@property (nonatomic, weak) UIView *responderView;
- (void)setTargetItem:(QQuickItem *)item;
@end
@implementation AmneziaEditMenuDelegate {
QPointer<QQuickItem> m_target;
}
- (void)setTargetItem:(QQuickItem *)item
{
m_target = item;
}
- (UIMenu *)editMenuInteraction:(UIEditMenuInteraction *)interaction
menuForConfiguration:(UIEditMenuConfiguration *)configuration
suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions
{
QQuickItem *item = m_target.data();
if (!item) {
return nil;
}
// The system's suggested actions are UICommands and get re-validated
// against the first responder right before the menu shows, which makes
// them hostage to Qt's input-method state. UIActions with handlers skip
// that validation entirely, so the menu is always built by hand from the
// QML control's state.
const bool hasSelection = !item->property("selectedText").toString().isEmpty();
const bool readOnly = item->property("readOnly").toBool();
const bool canPaste = item->property("canPaste").toBool();
const bool hasText = item->property("length").toInt() > 0;
NSMutableArray<UIMenuElement *> *actions = [NSMutableArray array];
if (hasSelection && !readOnly) {
[actions addObject:makeEditAction(menuTitle("C&ut"), m_target, "cut")];
}
if (hasSelection) {
[actions addObject:makeEditAction(menuTitle("&Copy"), m_target, "copy")];
}
if (canPaste && !readOnly) {
[actions addObject:makeEditAction(menuTitle("&Paste"), m_target, "paste")];
}
if (hasText) {
[actions addObject:makeEditAction(menuTitle("&SelectAll"), m_target, "selectAll")];
}
if (actions.count == 0) {
return nil;
}
return [UIMenu menuWithTitle:@"" children:actions];
}
- (void)editMenuInteraction:(UIEditMenuInteraction *)interaction
willDismissMenuForConfiguration:(UIEditMenuConfiguration *)configuration
animator:(id<UIEditMenuInteractionAnimating>)animator
{
// Give the borrowed first-responder status back once the menu goes away.
if (self.responderView.isFirstResponder) {
[self.responderView resignFirstResponder];
}
}
@end
bool IosContextMenu::isAvailable() const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
// Since Qt 6.10 the ContextMenu attached type is backed by a native menu
// on iOS, so the helper must stay out of the way.
return false;
#else
// UIEditMenuInteraction needs iOS 16, which is the deployment target.
return true;
#endif
}
void IosContextMenu::present(QQuickItem *target, qreal x, qreal y)
{
if (!target || !target->window()) {
return;
}
// On iOS QWindow::winId() is the backing UIView.
UIView *view = (__bridge UIView *)reinterpret_cast<void *>(target->window()->winId());
if (!view) {
return;
}
// Scene coordinates match the backing view's coordinate space.
const QPointF scenePos = target->mapToScene(QPointF(x, y));
AmneziaEditMenuDelegate *delegate = objc_getAssociatedObject(view, kEditMenuDelegateKey);
UIEditMenuInteraction *interaction = objc_getAssociatedObject(view, kEditMenuInteractionKey);
AmneziaEditMenuResponderView *responderView = objc_getAssociatedObject(view, kEditMenuResponderKey);
if (!interaction) {
responderView = [[AmneziaEditMenuResponderView alloc] initWithFrame:CGRectZero];
[view addSubview:responderView];
delegate = [[AmneziaEditMenuDelegate alloc] init];
delegate.responderView = responderView;
interaction = [[UIEditMenuInteraction alloc] initWithDelegate:delegate];
[view addInteraction:interaction];
objc_setAssociatedObject(view, kEditMenuResponderKey, responderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(view, kEditMenuDelegateKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(view, kEditMenuInteractionKey, interaction, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[delegate setTargetItem:target];
// While the keyboard is up for the target field, Qt's text input
// responder is the first responder and lives in this view's responder
// chain, which satisfies UIKit. Otherwise (read-only fields never raise
// the keyboard) borrow first-responder status. Qt's idea of the keyboard
// state is not trustworthy here, so key off the field being editable.
const bool readOnly = target->property("readOnly").toBool();
if (readOnly || !QGuiApplication::inputMethod()->isVisible()) {
[responderView becomeFirstResponder];
}
// Defer the actual present to the next main-loop iteration: the request
// arrives while the long-press touch is still active, and presenting
// mid-gesture makes UIKit discard the menu. Requests are also coalesced —
// a double tap asks for the menu twice (the TapHandler and the ContextMenu
// attached type both fire), and re-presenting makes the menu flicker.
static BOOL presentPending = NO;
if (presentPending) {
return;
}
presentPending = YES;
UIEditMenuConfiguration *configuration =
[UIEditMenuConfiguration configurationWithIdentifier:nil
sourcePoint:CGPointMake(scenePos.x(), scenePos.y())];
dispatch_async(dispatch_get_main_queue(), ^{
presentPending = NO;
[interaction presentEditMenuWithConfiguration:configuration];
});
}

View File

@@ -5,8 +5,10 @@
#ifndef MACOSSTATUSICON_H
#define MACOSSTATUSICON_H
#include <QMenu>
#include <QObject>
#include <QString>
class QMenu;
class MacOSStatusIcon final : public QObject {
Q_OBJECT
@@ -16,12 +18,12 @@ class MacOSStatusIcon final : public QObject {
explicit MacOSStatusIcon(QObject* parent);
~MacOSStatusIcon();
public:
void setIcon(const QString& iconUrl);
void setIndicatorColor(const QColor& indicatorColor);
void setMenu(NSMenu* statusBarMenu);
void setToolTip(const QString& tooltip);
void setIcon(const QString& iconPath);
void setMenu(QMenu* menu);
void showMessage(const QString& title, const QString& message);
private:
void* m_statusItem = nullptr;
};
#endif // MACOSSTATUSICON_H

View File

@@ -4,201 +4,79 @@
#include "macosstatusicon.h"
#include "leakdetector.h"
#include "logger.h"
#include <QDebug>
#include <QMenu>
#import <Cocoa/Cocoa.h>
#import <UserNotifications/UserNotifications.h>
#import <QResource>
/**
* Creates a NSStatusItem with that can hold an icon. Additionally a NSView is
* set as a subview to the button item of the status item. The view serves as
* an indicator that can be displayed in color eventhough the icon is set as a
* template. In that way we give the system control over its effective
* appearance.
*/
@interface MacOSStatusIconDelegate : NSObject
@property(assign) NSStatusItem* statusItem;
@property(assign) NSView* statusIndicator;
- (void)setIcon:(NSData*)imageData;
- (void)setIndicator;
- (void)setIndicatorColor:(NSColor*)color;
- (void)setMenu:(NSMenu*)statusBarMenu;
- (void)setToolTip:(NSString*)tooltip;
@end
@implementation MacOSStatusIconDelegate
/**
* Initializes and sets the status item and indicator objects.
*
* @return An instance of MacOSStatusIconDelegate.
*/
- (id)init {
self = [super init];
// Create status item
self.statusItem =
[[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
self.statusItem.visible = true;
// Add the indicator as a subview
[self setIndicator];
return self;
}
/**
* Sets the image for the status icon.
*
* @param iconPath The data for the icon image.
*/
- (void)setIcon:(NSData*)imageData {
NSImage* image = [[NSImage alloc] initWithData:imageData];
[image setTemplate:true];
[self.statusItem.button setImage:image];
[image release];
}
/**
* Adds status indicator as a subview to the status item button.
*/
- (void)setIndicator {
float viewHeight = NSHeight([self.statusItem.button bounds]);
float dotSize = viewHeight * 0.35;
float dotOrigin = (viewHeight - dotSize) * 0.8;
NSView* dot = [[NSView alloc] initWithFrame:NSMakeRect(dotOrigin, dotOrigin, dotSize, dotSize)];
self.statusIndicator = dot;
self.statusIndicator.wantsLayer = true;
self.statusIndicator.layer.cornerRadius = dotSize * 0.5;
[self.statusItem.button addSubview:self.statusIndicator];
[dot release];
}
/**
* Sets the color if the indicator.
*
* @param color The indicator background color.
*/
- (void)setIndicatorColor:(NSColor*)color {
if (self.statusIndicator) {
self.statusIndicator.layer.backgroundColor = color.CGColor;
}
}
/**
* Sets the status bar menu to the status item.
*
* @param statusBarMenu The menu object that is passed from QT.
*/
- (void)setMenu:(NSMenu*)statusBarMenu {
[self.statusItem setMenu:statusBarMenu];
}
/**
* Sets the tooltip string for the status item.
*
* @param tooltip The tooltip string.
*/
- (void)setToolTip:(NSString*)tooltip {
[self.statusItem.button setToolTip:tooltip];
}
@end
namespace {
Logger logger("MacOSStatusIcon");
MacOSStatusIconDelegate* m_statusBarIcon = nullptr;
}
MacOSStatusIcon::MacOSStatusIcon(QObject* parent) : QObject(parent) {
MZ_COUNT_CTOR(MacOSStatusIcon);
logger.debug() << "Register delegate";
Q_ASSERT(!m_statusBarIcon);
m_statusBarIcon = [[MacOSStatusIconDelegate alloc] init];
NSStatusItem* item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
item.visible = YES;
m_statusItem = [item retain];
}
MacOSStatusIcon::~MacOSStatusIcon() {
MZ_COUNT_DTOR(MacOSStatusIcon);
logger.debug() << "Remove delegate";
Q_ASSERT(m_statusBarIcon);
[static_cast<MacOSStatusIconDelegate*>(m_statusBarIcon) dealloc];
m_statusBarIcon = nullptr;
NSStatusItem* item = static_cast<NSStatusItem*>(m_statusItem);
item.menu = nil;
[[NSStatusBar systemStatusBar] removeStatusItem:item];
[item release];
m_statusItem = nullptr;
}
void MacOSStatusIcon::setIcon(const QString& iconPath) {
logger.debug() << "Set icon" << iconPath;
QResource imageResource = QResource(iconPath);
Q_ASSERT(imageResource.isValid());
[m_statusBarIcon setIcon:imageResource.uncompressedData().toNSData()];
}
void MacOSStatusIcon::setIndicatorColor(const QColor& indicatorColor) {
logger.debug() << "Set indicator color";
if (!indicatorColor.isValid()) {
[m_statusBarIcon setIndicatorColor:[NSColor clearColor]];
QResource resource(iconPath);
if (!resource.isValid()) {
qWarning() << "MacOSStatusIcon: invalid icon resource" << iconPath;
return;
}
NSColor* color = [NSColor colorWithCalibratedRed:indicatorColor.red() / 255.0f
green:indicatorColor.green() / 255.0f
blue:indicatorColor.blue() / 255.0f
alpha:indicatorColor.alpha() / 255.0f];
[m_statusBarIcon setIndicatorColor:color];
NSImage* image = [[NSImage alloc] initWithData:resource.uncompressedData().toNSData()];
CGFloat side = [[NSStatusBar systemStatusBar] thickness] - 4.0;
image.size = NSMakeSize(side, side);
static_cast<NSStatusItem*>(m_statusItem).button.image = image;
[image release];
}
void MacOSStatusIcon::setMenu(NSMenu* statusBarMenu) {
logger.debug() << "Set menu";
[m_statusBarIcon setMenu:statusBarMenu];
}
void MacOSStatusIcon::setToolTip(const QString& tooltip) {
logger.debug() << "Set tooltip";
[m_statusBarIcon setToolTip:tooltip.toNSString()];
void MacOSStatusIcon::setMenu(QMenu* menu) {
static_cast<NSStatusItem*>(m_statusItem).menu = menu ? menu->toNSMenu() : nil;
}
void MacOSStatusIcon::showMessage(const QString& title, const QString& message) {
logger.debug() << "Show message";
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
// This is a no-op is authorization has been granted.
// This is a no-op if authorization has already been granted.
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError* _Nullable error) {
completionHandler:^(BOOL, NSError* _Nullable error) {
if (error) {
// Note: This error may happen if the application is not signed.
NSLog(@"Error asking for permission to send notifications %@", error);
return;
// Note: this error may happen if the application is not signed.
qWarning() << "MacOSStatusIcon: notification authorization error:"
<< QString::fromNSString(error.localizedDescription);
}
}];
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [title.toNSString() autorelease];
content.body = [message.toNSString() autorelease];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound];
UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn"
content:content
trigger:trigger];
trigger:nil];
[content release];
[center addNotificationRequest:request
withCompletionHandler:^(NSError* _Nullable error) {
if (error) {
logger.error() << "Local Notification failed" << error;
qWarning() << "MacOSStatusIcon: local notification failed:"
<< QString::fromNSString(error.localizedDescription);
}
}];
}

View File

@@ -25,9 +25,19 @@ H1 = $INIT_PACKET_MAGIC_HEADER
H2 = $RESPONSE_PACKET_MAGIC_HEADER
H3 = $UNDERLOAD_PACKET_MAGIC_HEADER
H4 = $TRANSPORT_PACKET_MAGIC_HEADER
HeaderProtectionKey = $HEADER_PROTECTION_KEY
ContentPaddingAddition = $CONTENT_PADDING_ADDITION
RekeyAfterTime = $REKEY_AFTER_TIME
RekeyTimeout = $REKEY_TIMEOUT
RejectAfterTime = $REJECT_AFTER_TIME
KeepaliveTimeout = $KEEPALIVE_TIMEOUT
MaxHandshakeAttempts = $MAX_HANDSHAKE_ATTEMPTS
# I1 = $SPECIAL_JUNK_1
# I2 = $SPECIAL_JUNK_2
# I3 = $SPECIAL_JUNK_3
# I4 = $SPECIAL_JUNK_4
# I5 = $SPECIAL_JUNK_5
EOF
# Every AWG parameter is optional - drop the lines whose value came out empty
sed -i '/^[^=]*= *$/d' /opt/amnezia/awg/awg0.conf

View File

@@ -18,10 +18,17 @@ I2 = $SPECIAL_JUNK_2
I3 = $SPECIAL_JUNK_3
I4 = $SPECIAL_JUNK_4
I5 = $SPECIAL_JUNK_5
HeaderProtectionKey = $HEADER_PROTECTION_KEY
ContentPaddingAddition = $CONTENT_PADDING_ADDITION
RekeyAfterTime = $REKEY_AFTER_TIME
RekeyTimeout = $REKEY_TIMEOUT
RejectAfterTime = $REJECT_AFTER_TIME
KeepaliveTimeout = $KEEPALIVE_TIMEOUT
MaxHandshakeAttempts = $MAX_HANDSHAKE_ATTEMPTS
[Peer]
PublicKey = $WIREGUARD_SERVER_PUBLIC_KEY
PresharedKey = $WIREGUARD_PSK
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = $SERVER_IP_ADDRESS:$AWG_SERVER_PORT
PersistentKeepalive = 25
PersistentKeepalive = $PERSISTENT_KEEPALIVE

View File

@@ -47,6 +47,20 @@ else
FAKETLS_SECRET=""
fi
# Persist deployment state for restore on re-scan.
{
printf 'mode=%s\n' "$TRANSPORT_MODE"
printf 'domain=%s\n' "$MTPROXY_TLS_DOMAIN"
printf 'tag=%s\n' "$MTPROXY_TAG"
printf 'additional=%s\n' "$MTPROXY_ADDITIONAL_SECRETS"
printf 'workers_mode=%s\n' "$MTPROXY_WORKERS_MODE"
printf 'workers=%s\n' "$MTPROXY_WORKERS"
printf 'nat_enabled=%s\n' "$MTPROXY_NAT_ENABLED"
printf 'nat_internal=%s\n' "$MTPROXY_NAT_INTERNAL_IP"
printf 'nat_external=%s\n' "$MTPROXY_NAT_EXTERNAL_IP"
printf 'public_host=%s\n' "$MTPROXY_PUBLIC_HOST"
} > /data/mtproxy-meta
# Active link secret depends on transport mode
if [ "$TRANSPORT_MODE" = "faketls" ] && [ -n "$FAKETLS_SECRET" ]; then
LINK_SECRET="$FAKETLS_SECRET"

View File

@@ -28,6 +28,9 @@ rm -f /data/config.toml
if [ -n "$TELEMT_TAG" ]; then
echo "ad_tag = \"$TELEMT_TAG\""
fi
if [ -n "$TELEMT_MIDDLE_PROXY_NAT_IP" ]; then
echo "middle_proxy_nat_ip = \"$TELEMT_MIDDLE_PROXY_NAT_IP\""
fi
echo ""
echo "[general.modes]"
echo "classic = false"

View File

@@ -1,7 +1,7 @@
FROM alpine:3.15
LABEL maintainer="AmneziaVPN"
ARG XRAY_RELEASE="v25.8.3"
ARG XRAY_RELEASE="v26.7.28"
RUN apk add --no-cache curl unzip bash openssl netcat-openbsd dumb-init rng-tools xz
RUN apk --update upgrade --no-cache

View File

@@ -2,17 +2,11 @@ cd /opt/amnezia/xray
XRAY_CLIENT_ID=$(xray uuid) && echo $XRAY_CLIENT_ID > /opt/amnezia/xray/xray_uuid.key
XRAY_SHORT_ID=$(openssl rand -hex 8) && echo $XRAY_SHORT_ID > /opt/amnezia/xray/xray_short_id.key
# Parse x25519 keypair by label (v26.7 output has an extra Hash32 line; line-index parsing breaks).
KEYPAIR=$(xray x25519)
LINE_NUM=1
while IFS= read -r line; do
if [[ $LINE_NUM -gt 1 ]]
then
IFS=":" read FIST XRAY_PUBLIC_KEY <<< "$line"
else
LINE_NUM=$((LINE_NUM + 1))
IFS=":" read FIST XRAY_PRIVATE_KEY <<< "$line"
fi
done <<< "$KEYPAIR"
XRAY_PRIVATE_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]rivate[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*(PublicKey):[[:space:]]*//p' | head -1)
[ -z "$XRAY_PUBLIC_KEY" ] && XRAY_PUBLIC_KEY=$(printf '%s\n' "$KEYPAIR" | sed -n 's/.*[Pp]ublic[ ]*[Kk]ey:[[:space:]]*//p' | head -1)
XRAY_PRIVATE_KEY=$(echo $XRAY_PRIVATE_KEY | tr -d ' ')
XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
@@ -21,47 +15,4 @@ XRAY_PUBLIC_KEY=$(echo $XRAY_PUBLIC_KEY | tr -d ' ')
echo $XRAY_PUBLIC_KEY > /opt/amnezia/xray/xray_public.key
echo $XRAY_PRIVATE_KEY > /opt/amnezia/xray/xray_private.key
cat > /opt/amnezia/xray/server.json <<EOF
{
"log": {
"loglevel": "error"
},
"inbounds": [
{
"port": $XRAY_SERVER_PORT,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "$XRAY_CLIENT_ID",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"dest": "$XRAY_SITE_NAME:443",
"serverNames": [
"$XRAY_SITE_NAME"
],
"privateKey": "$XRAY_PRIVATE_KEY",
"shortIds": [
"$XRAY_SHORT_ID"
]
}
}
}
],
"outbounds": [
{
"protocol": "freedom"
}
]
}
EOF
# server.json is written by the client (writeServerConfigForSetup); this script only makes keys.

View File

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

View File

@@ -10,6 +10,8 @@ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport $XRAY_SERVER_PORT -j ACCEPT
iptables -A INPUT -p udp --dport $XRAY_SERVER_PORT -j ACCEPT
iptables -P INPUT DROP
ip6tables -A INPUT -i lo -j ACCEPT

View File

@@ -69,6 +69,14 @@ void ExportUiController::generateQrFromString(const QString &text)
emit exportConfigChanged();
}
void ExportUiController::generateQrFromStringRaw(const QString &text)
{
clearPreviousConfig();
m_config = text;
m_qrCodes = { qrCodeUtils::generatePlainQrCodeImage(text.toUtf8()) };
emit exportConfigChanged();
}
QString ExportUiController::getConfig()
{
return m_config;

View File

@@ -26,6 +26,7 @@ public slots:
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
void generateXrayConfig(const QString &serverId, const QString &clientName);
void generateQrFromString(const QString &text);
void generateQrFromStringRaw(const QString &text);
QString getConfig();
QString getNativeConfigString();

View File

@@ -567,12 +567,12 @@ void InstallUiController::clearProcessedServerCredentials()
void InstallUiController::setProcessedServerCredentials(const QString &hostName, const QString &userName, const QString &secretData)
{
m_processedServerCredentials.hostName = hostName;
m_processedServerCredentials.hostName = hostName.trimmed();
if (m_processedServerCredentials.hostName.contains(":")) {
m_processedServerCredentials.port = m_processedServerCredentials.hostName.split(":").at(1).toInt();
m_processedServerCredentials.hostName = m_processedServerCredentials.hostName.split(":").at(0);
}
m_processedServerCredentials.userName = userName;
m_processedServerCredentials.userName = userName.trimmed();
m_processedServerCredentials.secretData = secretData;
}

View File

@@ -5,6 +5,8 @@
#include "core/utils/protocolEnum.h"
#include "core/models/protocolConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "core/utils/constants/protocolConstants.h"
using namespace amnezia;
@@ -385,6 +387,59 @@ bool ServersUiController::isDefaultServerCurrentlyProcessed() const
return m_serversController->getDefaultServerId() == m_processedServerId;
}
bool ServersUiController::serverHasOutdatedAwgContainer(const QString &serverId) const
{
// The warning suggests reinstalling the container, so it only makes sense
// for servers the user can administer
if (!isServerHasWriteAccess(serverId)) {
return false;
}
const QMap<DockerContainer, ContainerConfig> containers = m_serversController->getServerContainersMap(serverId);
for (DockerContainer container : { DockerContainer::Awg, DockerContainer::Awg2 }) {
if (!containers.contains(container)) {
continue;
}
if (const auto* awgConfig = containers.value(container).getAwgProtocolConfig()) {
if (awgConfig->serverConfig.protocolVersion != protocols::awg::awgV3) {
return true;
}
}
}
return false;
}
bool ServersUiController::defaultServerHasOutdatedAwgContainer() const
{
return serverHasOutdatedAwgContainer(getDefaultServerId());
}
bool ServersUiController::isContainerOutdatedAwg(int containerIndex) const
{
if (!isProcessedServerHasWriteAccess()) {
return false;
}
DockerContainer container = static_cast<DockerContainer>(containerIndex);
if (!ContainerUtils::isAwgContainer(container)) {
return false;
}
const QMap<DockerContainer, ContainerConfig> containers = m_serversController->getServerContainersMap(m_processedServerId);
if (!containers.contains(container)) {
return false;
}
if (const auto* awgConfig = containers.value(container).getAwgProtocolConfig()) {
return awgConfig->serverConfig.protocolVersion != protocols::awg::awgV3;
}
return false;
}
bool ServersUiController::isProcessedContainerOutdatedAwg() const
{
return isContainerOutdatedAwg(m_processedContainerIndex);
}
bool ServersUiController::isProcessedServerHasWriteAccess() const
{
return isServerHasWriteAccess(m_processedServerId);

View File

@@ -27,6 +27,7 @@ class ServersUiController : public QObject
Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool defaultServerHasOutdatedAwgContainer READ defaultServerHasOutdatedAwgContainer NOTIFY defaultServerIdChanged)
Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged)
Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged)
@@ -72,6 +73,11 @@ public slots:
bool isDefaultServerFromApi() const;
bool hasServerWithWriteAccess() const;
bool serverHasOutdatedAwgContainer(const QString &serverId) const;
bool defaultServerHasOutdatedAwgContainer() const;
bool isContainerOutdatedAwg(int containerIndex) const;
bool isProcessedContainerOutdatedAwg() const;
QString serverName(const QString &serverId) const;
QString serverHostName(const QString &serverId) const;
int serverDefaultContainer(const QString &serverId) const;

View File

@@ -22,7 +22,7 @@ QVariant IpSplitTunnelingModel::data(const QModelIndex &index, int role) const
break;
}
case IpRole: {
return m_sites.at(index.row()).second;
return m_sites.at(index.row()).second.join(", ");
break;
}
default: {
@@ -33,7 +33,7 @@ QVariant IpSplitTunnelingModel::data(const QModelIndex &index, int role) const
return QVariant();
}
void IpSplitTunnelingModel::updateModel(const QVector<QPair<QString, QString>> &sites)
void IpSplitTunnelingModel::updateModel(const QVector<QPair<QString, QStringList>> &sites)
{
beginResetModel();
m_sites = sites;

View File

@@ -4,6 +4,7 @@
#include <QAbstractListModel>
#include <QVector>
#include <QPair>
#include <QStringList>
class IpSplitTunnelingModel : public QAbstractListModel
{
@@ -22,13 +23,13 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(const QVector<QPair<QString, QString>> &sites);
void updateModel(const QVector<QPair<QString, QStringList>> &sites);
protected:
QHash<int, QByteArray> roleNames() const override;
private:
QVector<QPair<QString, QString>> m_sites;
QVector<QPair<QString, QStringList>> m_sites;
};
#endif // IPSPLITTUNNELINGMODEL_H

View File

@@ -2,6 +2,8 @@
#include <QJsonDocument>
#include "core/configurators/wireguardConfigurator.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
@@ -42,6 +44,12 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
case Roles::ClientSpecialJunk3Role: m_protocolConfig.clientConfig->specialJunk3 = strValue; break;
case Roles::ClientSpecialJunk4Role: m_protocolConfig.clientConfig->specialJunk4 = strValue; break;
case Roles::ClientSpecialJunk5Role: m_protocolConfig.clientConfig->specialJunk5 = strValue; break;
case Roles::ClientContentPaddingAdditionRole: m_protocolConfig.clientConfig->contentPaddingAddition = strValue; break;
case Roles::ClientRekeyAfterTimeRole: m_protocolConfig.clientConfig->rekeyAfterTime = strValue; break;
case Roles::ClientRekeyTimeoutRole: m_protocolConfig.clientConfig->rekeyTimeout = strValue; break;
case Roles::ClientRejectAfterTimeRole: m_protocolConfig.clientConfig->rejectAfterTime = strValue; break;
case Roles::ClientKeepaliveTimeoutRole: m_protocolConfig.clientConfig->keepaliveTimeout = strValue; break;
case Roles::ClientMaxHandshakeAttemptsRole: m_protocolConfig.clientConfig->maxHandshakeAttempts = strValue; break;
case Roles::ServerJunkPacketCountRole: m_protocolConfig.serverConfig.junkPacketCount = strValue; break;
case Roles::ServerJunkPacketMinSizeRole: m_protocolConfig.serverConfig.junkPacketMinSize = strValue; break;
case Roles::ServerJunkPacketMaxSizeRole: m_protocolConfig.serverConfig.junkPacketMaxSize = strValue; break;
@@ -58,6 +66,24 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in
case Roles::ServerSpecialJunk3Role: m_protocolConfig.serverConfig.specialJunk3 = strValue; break;
case Roles::ServerSpecialJunk4Role: m_protocolConfig.serverConfig.specialJunk4 = strValue; break;
case Roles::ServerSpecialJunk5Role: m_protocolConfig.serverConfig.specialJunk5 = strValue; break;
case Roles::ServerContentPaddingAdditionRole: m_protocolConfig.serverConfig.contentPaddingAddition = strValue; break;
case Roles::ServerRekeyAfterTimeRole: m_protocolConfig.serverConfig.rekeyAfterTime = strValue; break;
case Roles::ServerRekeyTimeoutRole: m_protocolConfig.serverConfig.rekeyTimeout = strValue; break;
case Roles::ServerRejectAfterTimeRole: m_protocolConfig.serverConfig.rejectAfterTime = strValue; break;
case Roles::ServerKeepaliveTimeoutRole: m_protocolConfig.serverConfig.keepaliveTimeout = strValue; break;
case Roles::ServerMaxHandshakeAttemptsRole: m_protocolConfig.serverConfig.maxHandshakeAttempts = strValue; break;
case Roles::ServerHeaderProtectionEnabledRole: {
if (value.toBool()) {
if (m_protocolConfig.serverConfig.headerProtectionKey.isEmpty()) {
const QString originalKey = m_originalProtocolConfig.serverConfig.headerProtectionKey;
m_protocolConfig.serverConfig.headerProtectionKey =
originalKey.isEmpty() ? WireguardConfigurator::genClientKeys().clientPrivKey : originalKey;
}
} else {
m_protocolConfig.serverConfig.headerProtectionKey.clear();
}
break;
}
default:
return false;
}
@@ -85,6 +111,13 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
case Roles::ClientSpecialJunk3Role: return m_protocolConfig.clientConfig->specialJunk3;
case Roles::ClientSpecialJunk4Role: return m_protocolConfig.clientConfig->specialJunk4;
case Roles::ClientSpecialJunk5Role: return m_protocolConfig.clientConfig->specialJunk5;
case Roles::ClientContentPaddingAdditionRole: return m_protocolConfig.clientConfig->contentPaddingAddition;
case Roles::ClientRekeyAfterTimeRole: return m_protocolConfig.clientConfig->rekeyAfterTime;
case Roles::ClientRekeyTimeoutRole: return m_protocolConfig.clientConfig->rekeyTimeout;
case Roles::ClientRejectAfterTimeRole: return m_protocolConfig.clientConfig->rejectAfterTime;
case Roles::ClientKeepaliveTimeoutRole: return m_protocolConfig.clientConfig->keepaliveTimeout;
case Roles::ClientMaxHandshakeAttemptsRole: return m_protocolConfig.clientConfig->maxHandshakeAttempts;
case Roles::ClientHeaderProtectionEnabledRole: return !m_protocolConfig.clientConfig->headerProtectionKey.isEmpty();
case Roles::ServerJunkPacketCountRole: return m_protocolConfig.serverConfig.junkPacketCount;
case Roles::ServerJunkPacketMinSizeRole: return m_protocolConfig.serverConfig.junkPacketMinSize;
@@ -103,7 +136,19 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const
case Roles::ServerSpecialJunk4Role: return m_protocolConfig.serverConfig.specialJunk4;
case Roles::ServerSpecialJunk5Role: return m_protocolConfig.serverConfig.specialJunk5;
case Roles::IsAwg2Role: return m_protocolConfig.serverConfig.protocolVersion == protocols::awg::awgV2;
case Roles::ServerContentPaddingAdditionRole: return m_protocolConfig.serverConfig.contentPaddingAddition;
case Roles::ServerRekeyAfterTimeRole: return m_protocolConfig.serverConfig.rekeyAfterTime;
case Roles::ServerRekeyTimeoutRole: return m_protocolConfig.serverConfig.rekeyTimeout;
case Roles::ServerRejectAfterTimeRole: return m_protocolConfig.serverConfig.rejectAfterTime;
case Roles::ServerKeepaliveTimeoutRole: return m_protocolConfig.serverConfig.keepaliveTimeout;
case Roles::ServerMaxHandshakeAttemptsRole: return m_protocolConfig.serverConfig.maxHandshakeAttempts;
case Roles::ServerHeaderProtectionEnabledRole: return !m_protocolConfig.serverConfig.headerProtectionKey.isEmpty();
case Roles::IsAwg2Role: {
QString version = serverProtocolVersion();
return version == protocols::awg::awgV2 || version == protocols::awg::awgV3;
}
case Roles::IsAwg3Role: return serverProtocolVersion() == protocols::awg::awgV3;
}
return QVariant();
@@ -116,8 +161,6 @@ void AwgConfigModel::updateModel(amnezia::DockerContainer container, const amnez
m_protocolConfig = protocolConfig;
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
if (!m_protocolConfig.clientConfig.has_value()) {
m_protocolConfig.clientConfig = amnezia::AwgClientConfig{};
}
@@ -128,68 +171,9 @@ void AwgConfigModel::updateModel(amnezia::DockerContainer container, const amnez
endResetModel();
}
void AwgConfigModel::applyDefaultsToServerConfig(amnezia::AwgServerConfig& config)
QString AwgConfigModel::serverProtocolVersion() const
{
if (config.subnetAddress.isEmpty()) {
config.subnetAddress = protocols::wireguard::defaultSubnetAddress;
}
if (config.port.isEmpty()) {
config.port = protocols::awg::defaultPort;
}
if (config.transportProto.isEmpty()) {
config.transportProto = ProtocolUtils::transportProtoToString(
ProtocolUtils::defaultTransportProto(amnezia::Proto::Awg), amnezia::Proto::Awg);
}
if (config.junkPacketCount.isEmpty()) {
config.junkPacketCount = protocols::awg::defaultJunkPacketCount;
}
if (config.junkPacketMinSize.isEmpty()) {
config.junkPacketMinSize = protocols::awg::defaultJunkPacketMinSize;
}
if (config.junkPacketMaxSize.isEmpty()) {
config.junkPacketMaxSize = protocols::awg::defaultJunkPacketMaxSize;
}
if (config.initPacketJunkSize.isEmpty()) {
config.initPacketJunkSize = protocols::awg::defaultInitPacketJunkSize;
}
if (config.responsePacketJunkSize.isEmpty()) {
config.responsePacketJunkSize = protocols::awg::defaultResponsePacketJunkSize;
}
if (config.protocolVersion == protocols::awg::awgV2) {
if (config.cookieReplyPacketJunkSize.isEmpty()) {
config.cookieReplyPacketJunkSize = protocols::awg::defaultCookieReplyPacketJunkSize;
}
if (config.transportPacketJunkSize.isEmpty()) {
config.transportPacketJunkSize = protocols::awg::defaultTransportPacketJunkSize;
}
}
if (config.initPacketMagicHeader.isEmpty()) {
config.initPacketMagicHeader = protocols::awg::defaultInitPacketMagicHeader;
}
if (config.responsePacketMagicHeader.isEmpty()) {
config.responsePacketMagicHeader = protocols::awg::defaultResponsePacketMagicHeader;
}
if (config.underloadPacketMagicHeader.isEmpty()) {
config.underloadPacketMagicHeader = protocols::awg::defaultUnderloadPacketMagicHeader;
}
if (config.transportPacketMagicHeader.isEmpty()) {
config.transportPacketMagicHeader = protocols::awg::defaultTransportPacketMagicHeader;
}
if (config.specialJunk1.isEmpty()) {
config.specialJunk1 = protocols::awg::defaultSpecialJunk1;
}
if (config.specialJunk2.isEmpty()) {
config.specialJunk2 = protocols::awg::defaultSpecialJunk2;
}
if (config.specialJunk3.isEmpty()) {
config.specialJunk3 = protocols::awg::defaultSpecialJunk3;
}
if (config.specialJunk4.isEmpty()) {
config.specialJunk4 = protocols::awg::defaultSpecialJunk4;
}
if (config.specialJunk5.isEmpty()) {
config.specialJunk5 = protocols::awg::defaultSpecialJunk5;
}
return m_protocolConfig.serverConfig.protocolVersion;
}
void AwgConfigModel::applyDefaultsToClientConfig(amnezia::AwgClientConfig& config)
@@ -197,46 +181,6 @@ void AwgConfigModel::applyDefaultsToClientConfig(amnezia::AwgClientConfig& confi
if (config.mtu.isEmpty()) {
config.mtu = protocols::awg::defaultMtu;
}
if (config.junkPacketCount.isEmpty()) {
config.junkPacketCount = m_protocolConfig.serverConfig.junkPacketCount.isEmpty()
? protocols::awg::defaultJunkPacketCount
: m_protocolConfig.serverConfig.junkPacketCount;
}
if (config.junkPacketMinSize.isEmpty()) {
config.junkPacketMinSize = m_protocolConfig.serverConfig.junkPacketMinSize.isEmpty()
? protocols::awg::defaultJunkPacketMinSize
: m_protocolConfig.serverConfig.junkPacketMinSize;
}
if (config.junkPacketMaxSize.isEmpty()) {
config.junkPacketMaxSize = m_protocolConfig.serverConfig.junkPacketMaxSize.isEmpty()
? protocols::awg::defaultJunkPacketMaxSize
: m_protocolConfig.serverConfig.junkPacketMaxSize;
}
if (config.specialJunk1.isEmpty()) {
config.specialJunk1 = m_protocolConfig.serverConfig.specialJunk1.isEmpty()
? protocols::awg::defaultSpecialJunk1
: m_protocolConfig.serverConfig.specialJunk1;
}
if (config.specialJunk2.isEmpty()) {
config.specialJunk2 = m_protocolConfig.serverConfig.specialJunk2.isEmpty()
? protocols::awg::defaultSpecialJunk2
: m_protocolConfig.serverConfig.specialJunk2;
}
if (config.specialJunk3.isEmpty()) {
config.specialJunk3 = m_protocolConfig.serverConfig.specialJunk3.isEmpty()
? protocols::awg::defaultSpecialJunk3
: m_protocolConfig.serverConfig.specialJunk3;
}
if (config.specialJunk4.isEmpty()) {
config.specialJunk4 = m_protocolConfig.serverConfig.specialJunk4.isEmpty()
? protocols::awg::defaultSpecialJunk4
: m_protocolConfig.serverConfig.specialJunk4;
}
if (config.specialJunk5.isEmpty()) {
config.specialJunk5 = m_protocolConfig.serverConfig.specialJunk5.isEmpty()
? protocols::awg::defaultSpecialJunk5
: m_protocolConfig.serverConfig.specialJunk5;
}
}
amnezia::AwgProtocolConfig AwgConfigModel::getProtocolConfig()
@@ -247,21 +191,6 @@ amnezia::AwgProtocolConfig AwgConfigModel::getProtocolConfig()
m_protocolConfig.clearClientConfig();
}
if (m_protocolConfig.serverConfig.protocolVersion.isEmpty() ||
m_protocolConfig.serverConfig.protocolVersion != protocols::awg::awgV2) {
bool hasSpecialJunk = !m_protocolConfig.serverConfig.specialJunk1.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk2.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk3.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk4.trimmed().isEmpty() ||
!m_protocolConfig.serverConfig.specialJunk5.trimmed().isEmpty();
if (hasSpecialJunk) {
m_protocolConfig.serverConfig.protocolVersion = protocols::awg::awgV1_5;
} else if (m_protocolConfig.serverConfig.protocolVersion.isEmpty()) {
m_protocolConfig.serverConfig.protocolVersion = QString();
}
}
return m_protocolConfig;
}
@@ -296,6 +225,13 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
roles[ClientSpecialJunk3Role] = "clientSpecialJunk3";
roles[ClientSpecialJunk4Role] = "clientSpecialJunk4";
roles[ClientSpecialJunk5Role] = "clientSpecialJunk5";
roles[ClientContentPaddingAdditionRole] = "clientContentPaddingAddition";
roles[ClientRekeyAfterTimeRole] = "clientRekeyAfterTime";
roles[ClientRekeyTimeoutRole] = "clientRekeyTimeout";
roles[ClientRejectAfterTimeRole] = "clientRejectAfterTime";
roles[ClientKeepaliveTimeoutRole] = "clientKeepaliveTimeout";
roles[ClientMaxHandshakeAttemptsRole] = "clientMaxHandshakeAttempts";
roles[ClientHeaderProtectionEnabledRole] = "clientHeaderProtectionEnabled";
roles[ServerJunkPacketCountRole] = "serverJunkPacketCount";
roles[ServerJunkPacketMinSizeRole] = "serverJunkPacketMinSize";
@@ -315,7 +251,16 @@ QHash<int, QByteArray> AwgConfigModel::roleNames() const
roles[ServerSpecialJunk4Role] = "serverSpecialJunk4";
roles[ServerSpecialJunk5Role] = "serverSpecialJunk5";
roles[ServerContentPaddingAdditionRole] = "serverContentPaddingAddition";
roles[ServerRekeyAfterTimeRole] = "serverRekeyAfterTime";
roles[ServerRekeyTimeoutRole] = "serverRekeyTimeout";
roles[ServerRejectAfterTimeRole] = "serverRejectAfterTime";
roles[ServerKeepaliveTimeoutRole] = "serverKeepaliveTimeout";
roles[ServerMaxHandshakeAttemptsRole] = "serverMaxHandshakeAttempts";
roles[ServerHeaderProtectionEnabledRole] = "serverHeaderProtectionEnabled";
roles[IsAwg2Role] = "isAwg2";
roles[IsAwg3Role] = "isAwg3";
return roles;
}

View File

@@ -26,6 +26,13 @@ public:
ClientSpecialJunk3Role,
ClientSpecialJunk4Role,
ClientSpecialJunk5Role,
ClientContentPaddingAdditionRole,
ClientRekeyAfterTimeRole,
ClientRekeyTimeoutRole,
ClientRejectAfterTimeRole,
ClientKeepaliveTimeoutRole,
ClientMaxHandshakeAttemptsRole,
ClientHeaderProtectionEnabledRole,
ServerJunkPacketCountRole,
ServerJunkPacketMinSizeRole,
@@ -45,7 +52,16 @@ public:
ServerSpecialJunk4Role,
ServerSpecialJunk5Role,
IsAwg2Role
ServerContentPaddingAdditionRole,
ServerRekeyAfterTimeRole,
ServerRekeyTimeoutRole,
ServerRejectAfterTimeRole,
ServerKeepaliveTimeoutRole,
ServerMaxHandshakeAttemptsRole,
ServerHeaderProtectionEnabledRole,
IsAwg2Role,
IsAwg3Role
};
explicit AwgConfigModel(QObject *parent = nullptr);
@@ -71,7 +87,7 @@ private:
amnezia::AwgProtocolConfig m_protocolConfig;
amnezia::AwgProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::AwgServerConfig& config);
QString serverProtocolVersion() const;
void applyDefaultsToClientConfig(amnezia::AwgClientConfig& config);
};

View File

@@ -71,8 +71,6 @@ void WireGuardConfigModel::updateModel(amnezia::DockerContainer container, const
m_protocolConfig = protocolConfig;
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
if (!m_protocolConfig.clientConfig.has_value()) {
m_protocolConfig.clientConfig = amnezia::WireGuardClientConfig{};
}
@@ -83,20 +81,6 @@ void WireGuardConfigModel::updateModel(amnezia::DockerContainer container, const
endResetModel();
}
void WireGuardConfigModel::applyDefaultsToServerConfig(amnezia::WireGuardServerConfig& config)
{
if (config.subnetAddress.isEmpty()) {
config.subnetAddress = protocols::wireguard::defaultSubnetAddress;
}
if (config.port.isEmpty()) {
config.port = protocols::wireguard::defaultPort;
}
if (config.transportProto.isEmpty()) {
config.transportProto = ProtocolUtils::transportProtoToString(
ProtocolUtils::defaultTransportProto(amnezia::Proto::WireGuard), amnezia::Proto::WireGuard);
}
}
void WireGuardConfigModel::applyDefaultsToClientConfig(amnezia::WireGuardClientConfig& config)
{
if (config.mtu.isEmpty()) {

View File

@@ -40,7 +40,6 @@ private:
amnezia::WireGuardProtocolConfig m_protocolConfig;
amnezia::WireGuardProtocolConfig m_originalProtocolConfig;
void applyDefaultsToServerConfig(amnezia::WireGuardServerConfig& config);
void applyDefaultsToClientConfig(amnezia::WireGuardClientConfig& config);
};

View File

@@ -70,8 +70,6 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i
break;
case Roles::XhttpPathRole: xhttp.path = str;
break;
case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str;
break;
case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str;
break;
case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool();
@@ -206,7 +204,6 @@ QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
case Roles::XhttpModeRole: return xhttp.mode;
case Roles::XhttpHostRole: return xhttp.host;
case Roles::XhttpPathRole: return xhttp.path;
case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate;
case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod;
case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc;
case Roles::XhttpDisableSseRole: return xhttp.disableSse;
@@ -335,9 +332,6 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con
if (config.xhttp.mode.isEmpty()) {
config.xhttp.mode = protocols::xray::defaultXhttpMode;
}
if (config.xhttp.headersTemplate.isEmpty()) {
config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate;
}
if (config.xhttp.uplinkMethod.isEmpty()) {
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
}
@@ -404,7 +398,6 @@ QHash<int, QByteArray> XrayConfigModel::roleNames() const
roles[XhttpModeRole] = "xhttpMode";
roles[XhttpHostRole] = "xhttpHost";
roles[XhttpPathRole] = "xhttpPath";
roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate";
roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod";
roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc";
roles[XhttpDisableSseRole] = "xhttpDisableSse";
@@ -481,7 +474,6 @@ void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverC
m_protocolConfig.serverConfig = serverConfig;
// Clear client config since server settings changed
m_protocolConfig.clearClientConfig();
m_originalProtocolConfig = m_protocolConfig;
endResetModel();
if (wasUnsavedChanges != hasUnsavedChanges()) {
@@ -515,7 +507,7 @@ QStringList XrayConfigModel::fingerprintOptions()
QStringList XrayConfigModel::alpnOptions()
{
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
return { "h2", "http/1.1", "h2,http/1.1" };
}
QStringList XrayConfigModel::xhttpModeOptions()
@@ -523,11 +515,6 @@ QStringList XrayConfigModel::xhttpModeOptions()
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
}
QStringList XrayConfigModel::xhttpHeadersTemplateOptions()
{
return { "HTTP", "None" };
}
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
{
return { "POST", "PUT", "PATCH" };
@@ -535,17 +522,12 @@ QStringList XrayConfigModel::xhttpUplinkMethodOptions()
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
{
return { "Path", "Header", "Cookie", "None" };
}
QStringList XrayConfigModel::xhttpSessionKeyOptions()
{
return { "Path", "Header", "None" };
return { "Path", "Header", "Cookie", "Query", "None" };
}
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
{
return { "Path", "Header", "Cookie", "None" };
return { "Path", "Header", "Cookie", "Query", "None" };
}
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
@@ -590,6 +572,76 @@ QString XrayConfigModel::mkcpDefaultWriteBufferSize()
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
}
QString XrayConfigModel::portDefault()
{
return QString::fromLatin1(protocols::xray::defaultPort);
}
QString XrayConfigModel::sniDefault()
{
return QString::fromLatin1(protocols::xray::defaultSni);
}
QString XrayConfigModel::xhttpHostDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpHost);
}
QString XrayConfigModel::xhttpUplinkChunkSizeDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpUplinkChunkSize);
}
QString XrayConfigModel::scMaxEachPostBytesMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMin);
}
QString XrayConfigModel::scMaxEachPostBytesMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMax);
}
QString XrayConfigModel::scMinPostsIntervalMsMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMin);
}
QString XrayConfigModel::scMinPostsIntervalMsMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMax);
}
QString XrayConfigModel::scStreamUpServerSecsMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMin);
}
QString XrayConfigModel::scStreamUpServerSecsMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMax);
}
QString XrayConfigModel::xPaddingKeyDefault()
{
return QString::fromLatin1(protocols::xray::defaultXPaddingKey);
}
QString XrayConfigModel::xPaddingHeaderDefault()
{
return QString::fromLatin1(protocols::xray::defaultXPaddingHeader);
}
QString XrayConfigModel::xPaddingBytesMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXPaddingBytesMin);
}
QString XrayConfigModel::xPaddingBytesMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXPaddingBytesMax);
}
namespace {
bool isValidSingleHost(const QString &t)
{

View File

@@ -33,7 +33,6 @@ public:
XhttpModeRole,
XhttpHostRole,
XhttpPathRole,
XhttpHeadersTemplateRole,
XhttpUplinkMethodRole,
XhttpDisableGrpcRole,
XhttpDisableSseRole,
@@ -102,10 +101,8 @@ public:
Q_INVOKABLE static QStringList fingerprintOptions();
Q_INVOKABLE static QStringList alpnOptions();
Q_INVOKABLE static QStringList xhttpModeOptions();
Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions();
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
Q_INVOKABLE static QStringList xhttpSessionPlacementOptions();
Q_INVOKABLE static QStringList xhttpSessionKeyOptions();
Q_INVOKABLE static QStringList xhttpSeqPlacementOptions();
Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions();
Q_INVOKABLE static QStringList xPaddingPlacementOptions();
@@ -118,6 +115,21 @@ public:
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
Q_INVOKABLE static QString portDefault();
Q_INVOKABLE static QString sniDefault();
Q_INVOKABLE static QString xhttpHostDefault();
Q_INVOKABLE static QString xhttpUplinkChunkSizeDefault();
Q_INVOKABLE static QString scMaxEachPostBytesMinDefault();
Q_INVOKABLE static QString scMaxEachPostBytesMaxDefault();
Q_INVOKABLE static QString scMinPostsIntervalMsMinDefault();
Q_INVOKABLE static QString scMinPostsIntervalMsMaxDefault();
Q_INVOKABLE static QString scStreamUpServerSecsMinDefault();
Q_INVOKABLE static QString scStreamUpServerSecsMaxDefault();
Q_INVOKABLE static QString xPaddingKeyDefault();
Q_INVOKABLE static QString xPaddingHeaderDefault();
Q_INVOKABLE static QString xPaddingBytesMinDefault();
Q_INVOKABLE static QString xPaddingBytesMaxDefault();
Q_INVOKABLE static bool isValidHost(const QString &host);
Q_INVOKABLE static bool isValidSni(const QString &sni);
Q_INVOKABLE static bool isValidPath(const QString &path);

View File

@@ -6,8 +6,36 @@ import QtQuick.Layouts
import "../Controls2"
TextFieldWithHeaderType {
id: root
property var scroller: null
property bool rangeValidation: false
signal edited(string text)
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
checkEmptyText: true
checkEmptyText: false
property RegularExpressionValidator rangeValidator: RegularExpressionValidator {
regularExpression: /^\d*(-\d*)?$/
}
textField.validator: rangeValidation ? rangeValidator : null
textField.onEditingFinished: {
if (root.rangeValidation) {
root.textField.text = root.textField.text.replace(/^-+|-+$/g, "")
}
root.edited(root.textField.text)
}
textField.onActiveFocusChanged: {
if (root.textField.activeFocus && root.scroller) {
root.scroller.scrollToItem(root)
}
}
}

View File

@@ -59,6 +59,7 @@ ListViewType {
var containerIndex = proxyDefaultServerContainersModel.mapToSource(index)
if (!isInstalled) {
ServersUiController.setProcessedServerId(ServersUiController.defaultServerId)
ServersUiController.processedContainerIndex = containerIndex
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
containersDropDown.closeTriggered()

View File

@@ -93,6 +93,20 @@ ListViewType {
Keys.onReturnPressed: serverRadioButton.clicked()
}
ImageButtonType {
id: outdatedContainerWarningIcon
objectName: "outdatedContainerWarningIcon"
visible: ServersUiController.serverHasOutdatedAwgContainer(serverId)
hoverEnabled: false
image: "qrc:/images/controls/alert-circle.svg"
imageColor: AmneziaStyle.color.goldenApricot
implicitWidth: 40
implicitHeight: 56
}
ImageButtonType {
id: serverInfoButton
objectName: "serverInfoButton"

View File

@@ -5,6 +5,7 @@ import QtQuick.Layouts
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
@@ -18,11 +19,14 @@ ListViewType {
delegate: ColumnLayout {
width: root.width
property bool isOutdatedAwgContainer: Boolean(isInstalled && ServersUiController.isContainerOutdatedAwg(root.model.mapToSource(index)))
LabelWithButtonType {
Layout.fillWidth: true
text: name
descriptionText: description
rightWarningImageSource: isOutdatedAwgContainer ? "qrc:/images/controls/alert-circle.svg" : ""
rightImageSource: isInstalled ? "qrc:/images/controls/chevron-right.svg" : "qrc:/images/controls/download.svg"
clickedFunction: function() {

View File

@@ -232,6 +232,7 @@ Popup {
textField.placeholderTextColor: AmneziaStyle.color.mutedGray
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
textField.maximumLength: 6
textField.validator: RegularExpressionValidator { regularExpression: /^[0-9]{0,6}$/ }
textField.font.letterSpacing: 2
textField.onAccepted: {

View File

@@ -6,6 +6,20 @@ Menu {
popupType: Popup.Native
// On Qt < 6.10 the ContextMenu attached type has no native backing on iOS
// and opens this Qt-drawn menu instead. In that case the native edit menu
// is presented through UIEditMenuInteraction (IosContextMenu is registered
// only in iOS builds; on Qt >= 6.10 isAvailable() returns false and the
// attached type shows the native menu itself).
readonly property bool useNativeEditMenu: typeof IosContextMenu !== "undefined" && IosContextMenu.isAvailable()
function requestNative(position) {
if (useNativeEditMenu && textObj) {
textObj.forceActiveFocus()
IosContextMenu.present(textObj, position.x, position.y)
}
}
property Item inputBlocker: null
Component {

View File

@@ -22,6 +22,8 @@ Item {
property string buttonImageSource
property string rightImageSource
property string leftImageSource
property string rightWarningImageSource
property string rightWarningImageColor: AmneziaStyle.color.goldenApricot
property bool isLeftImageHoverEnabled: true
property bool isSmallLeftImage: false
@@ -225,6 +227,33 @@ Item {
}
}
Rectangle {
id: rightWarningImageBackground
visible: rightWarningImageSource !== ""
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.alignment: Qt.AlignRight
radius: 12
color: AmneziaStyle.color.transparent
Image {
id: rightWarningImage
anchors.centerIn: parent
source: rightWarningImageSource
visible: false
}
ColorOverlay {
anchors.fill: rightWarningImage
source: rightWarningImage
color: rightWarningImageColor
}
}
ImageButtonType {
id: eyeImage
visible: buttonImageSource !== ""

View File

@@ -22,6 +22,8 @@ ListViewType {
property int selectedIndex: 0
property string currentValue: ""
width: rootWidth
height: root.contentItem.height
@@ -131,7 +133,7 @@ ListViewType {
}
ButtonGroup.group: buttonGroup
checked: root.selectedIndex === index
checked: root.currentValue !== "" ? (name === root.currentValue) : (root.selectedIndex === index)
onClicked: {
root.selectedIndex = index

View File

@@ -24,6 +24,9 @@ Item {
property int minLimit: 0
property int maxLimit: 2147483647
property string minPlaceholder: ""
property string maxPlaceholder: ""
property string hintText: root.minLimit > 0
? (root.minLimit + "" + root.maxLimit)
: ("≤ " + root.maxLimit)
@@ -73,6 +76,7 @@ Item {
property string lastValid: ""
Layout.fillWidth: true
headerText: qsTr("Min")
placeholderText: root.minPlaceholder
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onActiveFocusChanged: {
@@ -87,10 +91,9 @@ Item {
if (!isNaN(mx) && parseInt(v, 10) > mx)
root.maxChanged(v)
}
if (v !== root.minValue)
root.minChanged(v)
else if (minField.textField.text !== v)
if (minField.textField.text !== v)
minField.textField.text = v
root.minChanged(v)
}
Binding {
@@ -108,6 +111,7 @@ Item {
property string lastValid: ""
Layout.fillWidth: true
headerText: qsTr("Max")
placeholderText: root.maxPlaceholder
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onActiveFocusChanged: {
@@ -122,10 +126,9 @@ Item {
if (!isNaN(mn) && parseInt(v, 10) < mn)
v = String(mn)
}
if (v !== root.maxValue)
root.maxChanged(v)
else if (maxField.textField.text !== v)
if (maxField.textField.text !== v)
maxField.textField.text = v
root.maxChanged(v)
}
Binding {

View File

@@ -93,7 +93,23 @@ Rectangle {
wrapMode: Text.Wrap
ContextMenu.menu: contextMenu
ContextMenu.menu: contextMenu.useNativeEditMenu ? null : contextMenu
ContextMenu.onRequested: function(position) {
contextMenu.requestNative(position)
}
// Native iOS text fields select the word and show the edit
// menu on double tap; Qt does neither on touch.
TapHandler {
enabled: contextMenu.useNativeEditMenu
acceptedDevices: PointerDevice.TouchScreen
onDoubleTapped: function(eventPoint) {
textArea.forceActiveFocus()
textArea.cursorPosition = textArea.positionAt(eventPoint.position.x, eventPoint.position.y)
textArea.selectWord()
contextMenu.requestNative(eventPoint.position)
}
}
ContextMenuType {
id: contextMenu

View File

@@ -79,7 +79,23 @@ Rectangle {
wrapMode: Text.Wrap
ContextMenu.menu: contextMenu
ContextMenu.menu: contextMenu.useNativeEditMenu ? null : contextMenu
ContextMenu.onRequested: function(position) {
contextMenu.requestNative(position)
}
// Native iOS text fields select the word and show the edit
// menu on double tap; Qt does neither on touch.
TapHandler {
enabled: contextMenu.useNativeEditMenu
acceptedDevices: PointerDevice.TouchScreen
onDoubleTapped: function(eventPoint) {
textArea.forceActiveFocus()
textArea.cursorPosition = textArea.positionAt(eventPoint.position.x, eventPoint.position.y)
textArea.selectWord()
contextMenu.requestNative(eventPoint.position)
}
}
ContextMenuType {
id: contextMenu

View File

@@ -11,6 +11,7 @@ Item {
property string headerText
property string subtitleText // optional line under header (e.g. default value hint)
property string hintText // optional (i) info tooltip next to the header (e.g. range / max / default)
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
property string headerTextColor: AmneziaStyle.color.mutedGray
@@ -23,6 +24,7 @@ Item {
property var clickedFunc
property alias textField: textField
property alias placeholderText: textField.placeholderText
property string textFieldTextColor: AmneziaStyle.color.paleGray
property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray
@@ -144,7 +146,23 @@ Item {
}
}
ContextMenu.menu: contextMenu
ContextMenu.menu: contextMenu.useNativeEditMenu ? null : contextMenu
ContextMenu.onRequested: function(position) {
contextMenu.requestNative(position)
}
// Native iOS text fields select the word and show the
// edit menu on double tap; Qt does neither on touch.
TapHandler {
enabled: contextMenu.useNativeEditMenu
acceptedDevices: PointerDevice.TouchScreen
onDoubleTapped: function(eventPoint) {
textField.forceActiveFocus()
textField.cursorPosition = textField.positionAt(eventPoint.position.x, eventPoint.position.y)
textField.selectWord()
contextMenu.requestNative(eventPoint.position)
}
}
ContextMenuType {
id: contextMenu
@@ -215,6 +233,52 @@ Item {
}
}
// (i) hint: tap to reveal a tooltip with hintText
ImageButtonType {
id: hintButton
visible: root.hintText !== ""
focusPolicy: Qt.NoFocus
hoverEnabled: true
image: "qrc:/images/controls/info.svg"
imageColor: hintTooltip.opened ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray
anchors.top: content.top
anchors.right: content.right
anchors.topMargin: 10
anchors.rightMargin: 12
implicitWidth: 28
implicitHeight: 28
onClicked: hintTooltip.opened ? hintTooltip.close() : hintTooltip.open()
ToolTip {
id: hintTooltip
parent: hintButton
x: hintButton.width - width
y: -height - 6
width: Math.min(280, root.width - 24)
delay: 0
timeout: 8000
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
contentItem: Text {
text: root.hintText
color: AmneziaStyle.color.paleGray
wrapMode: Text.WordWrap
font.pixelSize: 14
font.family: "PT Root UI VF"
}
background: Rectangle {
color: AmneziaStyle.color.slateGray
radius: 12
border.color: AmneziaStyle.color.charcoalGray
border.width: 1
}
}
}
function getBackgroundBorderColor(noneFocusedColor) {
return textField.focus ? root.borderFocusedColor : noneFocusedColor
}

View File

@@ -299,6 +299,26 @@ PageType {
}
}
ImageButtonType {
id: outdatedContainerWarningIcon
objectName: "outdatedContainerWarningIcon"
Layout.rightMargin: 8
visible: drawer.isCollapsedStateActive() && ServersUiController.defaultServerHasOutdatedAwgContainer
hoverEnabled: false
image: "qrc:/images/controls/alert-circle.svg"
imageColor: AmneziaStyle.color.goldenApricot
icon.width: 18
icon.height: 18
backgroundRadius: 16
horizontalPadding: 4
topPadding: 4
bottomPadding: 3
}
Header1TextType {
id: collapsedButtonHeader
objectName: "collapsedButtonHeader"

View File

@@ -99,187 +99,168 @@ PageType {
AwgTextField {
id: junkPacketCountTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: "Jc - Junk packet count"
textField.text: clientJunkPacketCount
textField.onEditingFinished: {
if (textField.text !== clientJunkPacketCount) {
clientJunkPacketCount = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketCountTextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientJunkPacketCount = text }
}
AwgTextField {
id: junkPacketMinSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: "Jmin - Junk packet minimum size"
textField.text: clientJunkPacketMinSize
textField.onEditingFinished: {
if (textField.text !== clientJunkPacketMinSize) {
clientJunkPacketMinSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMinSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientJunkPacketMinSize = text }
}
AwgTextField {
id: junkPacketMaxSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: "Jmax - Junk packet maximum size"
textField.text: clientJunkPacketMaxSize
textField.onEditingFinished: {
if (textField.text !== clientJunkPacketMaxSize) {
clientJunkPacketMaxSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMaxSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientJunkPacketMaxSize = text }
}
AwgTextField {
id: specialJunk1TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I1 - First special junk packet")
textField.text: clientSpecialJunk1
textField.validator: null
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== clientSpecialJunk1) {
clientSpecialJunk1 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk1TextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientSpecialJunk1 = text }
}
AwgTextField {
id: specialJunk2TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I2 - Second special junk packet")
textField.text: clientSpecialJunk2
textField.validator: null
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== clientSpecialJunk2) {
clientSpecialJunk2 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk2TextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientSpecialJunk2 = text }
}
AwgTextField {
id: specialJunk3TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I3 - Third special junk packet")
textField.text: clientSpecialJunk3
textField.validator: null
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== clientSpecialJunk3) {
clientSpecialJunk3 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk3TextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientSpecialJunk3 = text }
}
AwgTextField {
id: specialJunk4TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I4 - Fourth special junk packet")
textField.text: clientSpecialJunk4
textField.validator: null
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== clientSpecialJunk4) {
clientSpecialJunk4 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk4TextField)
}
}
scroller: smartScroll
onEdited: (text) => { clientSpecialJunk4 = text }
}
AwgTextField {
id: specialJunk5TextField
headerText: qsTr("I5 - Fifth special junk packet")
textField.text: clientSpecialJunk5
scroller: smartScroll
onEdited: (text) => { clientSpecialJunk5 = text }
}
CheckBoxType {
id: headerProtectionCheckBox
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I5 - Fifth special junk packet")
textField.text: clientSpecialJunk5
textField.validator: null
checkEmptyText: false
enabled: false
textField.onEditingFinished: {
if (textField.text !== clientSpecialJunk5 ) {
clientSpecialJunk5 = textField.text
}
}
text: qsTr("HeaderProtectionKey")
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk5TextField)
}
}
checked: clientHeaderProtectionEnabled
}
AwgTextField {
id: contentPaddingAdditionTextField
rangeValidation: true
headerText: qsTr("ContentPaddingAddition - Content padding addition")
textField.text: clientContentPaddingAddition
scroller: smartScroll
onEdited: (text) => { clientContentPaddingAddition = text }
}
AwgTextField {
id: rekeyAfterTimeTextField
rangeValidation: true
headerText: qsTr("RekeyAfterTime - Rekey after time")
textField.text: clientRekeyAfterTime
scroller: smartScroll
onEdited: (text) => { clientRekeyAfterTime = text }
}
AwgTextField {
id: rekeyTimeoutTextField
rangeValidation: true
headerText: qsTr("RekeyTimeout - Rekey timeout")
textField.text: clientRekeyTimeout
scroller: smartScroll
onEdited: (text) => { clientRekeyTimeout = text }
}
AwgTextField {
id: rejectAfterTimeTextField
rangeValidation: true
headerText: qsTr("RejectAfterTime - Reject after time")
textField.text: clientRejectAfterTime
scroller: smartScroll
onEdited: (text) => { clientRejectAfterTime = text }
}
AwgTextField {
id: keepaliveTimeoutTextField
rangeValidation: true
headerText: qsTr("KeepaliveTimeout - Keepalive timeout")
textField.text: clientKeepaliveTimeout
scroller: smartScroll
onEdited: (text) => { clientKeepaliveTimeout = text }
}
AwgTextField {
id: maxHandshakeAttemptsTextField
rangeValidation: true
headerText: qsTr("MaxHandshakeAttempts - Max handshake attempts")
textField.text: clientMaxHandshakeAttempts
scroller: smartScroll
onEdited: (text) => { clientMaxHandshakeAttempts = text }
}
@@ -295,9 +276,6 @@ PageType {
AwgTextField {
id: portTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: qsTr("Port")
@@ -307,9 +285,6 @@ PageType {
AwgTextField {
id: initPacketJunkSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "S1 - Init packet junk size"
@@ -319,9 +294,6 @@ PageType {
AwgTextField {
id: responsePacketJunkSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "S2 - Response packet junk size"
@@ -332,10 +304,6 @@ PageType {
id: cookieReplyPacketJunkSizeTextField
visible: isAwg2
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "S3 - Cookie Reply packet junk size"
@@ -346,10 +314,6 @@ PageType {
id: transportPacketJunkSizeTextField
visible: isAwg2
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "S4 - Transport packet junk size"
@@ -359,9 +323,6 @@ PageType {
AwgTextField {
id: initPacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "H1 - Init packet magic header"
@@ -371,9 +332,6 @@ PageType {
AwgTextField {
id: responsePacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "H2 - Response packet magic header"
@@ -383,9 +341,6 @@ PageType {
AwgTextField {
id: underloadPacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "H3 - Underload packet magic header"
@@ -395,14 +350,12 @@ PageType {
AwgTextField {
id: transportPacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: false
headerText: "H4 - Transport packet magic header"
textField.text: serverTransportPacketMagicHeader
}
}
}

View File

@@ -128,374 +128,271 @@ PageType {
AwgTextField {
id: junkPacketCountTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Jc - Junk packet count")
textField.text: serverJunkPacketCount
textField.onEditingFinished: {
if (textField.text !== serverJunkPacketCount) {
serverJunkPacketCount = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketCountTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverJunkPacketCount = text }
}
AwgTextField {
id: junkPacketMinSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Jmin - Junk packet minimum size")
textField.text: serverJunkPacketMinSize
textField.onEditingFinished: {
if (textField.text !== serverJunkPacketMinSize) {
serverJunkPacketMinSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMinSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverJunkPacketMinSize = text }
}
AwgTextField {
id: junkPacketMaxSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Jmax - Junk packet maximum size")
textField.text: serverJunkPacketMaxSize
textField.onEditingFinished: {
if (textField.text !== serverJunkPacketMaxSize) {
serverJunkPacketMaxSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMaxSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverJunkPacketMaxSize = text }
}
AwgTextField {
id: initPacketJunkSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("S1 - Init packet junk size")
textField.text: serverInitPacketJunkSize
textField.onEditingFinished: {
if (textField.text !== serverInitPacketJunkSize) {
serverInitPacketJunkSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(initPacketJunkSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverInitPacketJunkSize = text }
}
AwgTextField {
id: responsePacketJunkSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("S2 - Response packet junk size")
textField.text: serverResponsePacketJunkSize
textField.onEditingFinished: {
if (textField.text !== serverResponsePacketJunkSize) {
serverResponsePacketJunkSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(responsePacketJunkSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverResponsePacketJunkSize = text }
}
AwgTextField {
id: cookieReplyPacketJunkSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: isAwg2
headerText: qsTr("S3 - Cookie reply packet junk size")
textField.text: serverCookieReplyPacketJunkSize
textField.onEditingFinished: {
if (textField.text !== serverCookieReplyPacketJunkSize) {
serverCookieReplyPacketJunkSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(cookieReplyPacketJunkSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverCookieReplyPacketJunkSize = text }
}
AwgTextField {
id: transportPacketJunkSizeTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: isAwg2
headerText: qsTr("S4 - Transport packet junk size")
textField.text: serverTransportPacketJunkSize
textField.onEditingFinished: {
if (textField.text !== serverTransportPacketJunkSize) {
serverTransportPacketJunkSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(transportPacketJunkSizeTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverTransportPacketJunkSize = text }
}
AwgTextField {
id: initPacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
rangeValidation: true
headerText: qsTr("H1 - Init packet magic header")
textField.text: serverInitPacketMagicHeader
textField.validator: RegularExpressionValidator {
regularExpression: /^(\d+)(-\d+)?$/
}
textField.onEditingFinished: {
if (textField.text !== serverInitPacketMagicHeader) {
serverInitPacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(initPacketMagicHeaderTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverInitPacketMagicHeader = text }
}
AwgTextField {
id: responsePacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
rangeValidation: true
headerText: qsTr("H2 - Response packet magic header")
textField.text: serverResponsePacketMagicHeader
textField.validator: RegularExpressionValidator {
regularExpression: /^(\d+)(-\d+)?$/
}
textField.onEditingFinished: {
if (textField.text !== serverResponsePacketMagicHeader) {
serverResponsePacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(responsePacketMagicHeaderTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverResponsePacketMagicHeader = text }
}
AwgTextField {
id: underloadPacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
rangeValidation: true
headerText: qsTr("H3 - Underload packet magic header")
textField.text: serverUnderloadPacketMagicHeader
textField.validator: RegularExpressionValidator {
regularExpression: /^(\d+)(-\d+)?$/
}
textField.onEditingFinished: {
if (textField.text !== serverUnderloadPacketMagicHeader) {
serverUnderloadPacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(underloadPacketMagicHeaderTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverUnderloadPacketMagicHeader = text }
}
AwgTextField {
id: transportPacketMagicHeaderTextField
Layout.leftMargin: 16
Layout.rightMargin: 16
rangeValidation: true
headerText: qsTr("H4 - Transport packet magic header")
textField.text: serverTransportPacketMagicHeader
textField.validator: RegularExpressionValidator {
regularExpression: /^(\d+)(-\d+)?$/
}
textField.onEditingFinished: {
if (textField.text !== serverTransportPacketMagicHeader) {
serverTransportPacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(transportPacketMagicHeaderTextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverTransportPacketMagicHeader = text }
}
AwgTextField {
id: specialJunk1TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I1 - Special junk 1")
textField.text: serverSpecialJunk1
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk1) {
serverSpecialJunk1 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk1TextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverSpecialJunk1 = text }
}
AwgTextField {
id: specialJunk2TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I2 - Special junk 2")
textField.text: serverSpecialJunk2
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk2) {
serverSpecialJunk2 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk2TextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverSpecialJunk2 = text }
}
AwgTextField {
id: specialJunk3TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I3 - Special junk 3")
textField.text: serverSpecialJunk3
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk3) {
serverSpecialJunk3 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk3TextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverSpecialJunk3 = text }
}
AwgTextField {
id: specialJunk4TextField
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I4 - Special junk 4")
textField.text: serverSpecialJunk4
checkEmptyText: false
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk4) {
serverSpecialJunk4 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk4TextField)
}
}
scroller: smartScroll
onEdited: (text) => { serverSpecialJunk4 = text }
}
AwgTextField {
id: specialJunk5TextField
headerText: qsTr("I5 - Special junk 5")
textField.text: serverSpecialJunk5
scroller: smartScroll
onEdited: (text) => { serverSpecialJunk5 = text }
}
CheckBoxType {
id: headerProtectionCheckBox
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("I5 - Special junk 5")
textField.text: serverSpecialJunk5
checkEmptyText: false
visible: isAwg3
textField.onEditingFinished: {
if (textField.text !== serverSpecialJunk5) {
serverSpecialJunk5 = textField.text
text: qsTr("HeaderProtectionKey")
checked: serverHeaderProtectionEnabled
onCheckedChanged: {
if (checked !== serverHeaderProtectionEnabled) {
serverHeaderProtectionEnabled = checked
}
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk5TextField)
}
}
AwgTextField {
id: contentPaddingAdditionTextField
visible: isAwg3
rangeValidation: true
headerText: qsTr("ContentPaddingAddition - Content padding addition")
textField.text: serverContentPaddingAddition
scroller: smartScroll
onEdited: (text) => { serverContentPaddingAddition = text }
}
AwgTextField {
id: rekeyAfterTimeTextField
visible: isAwg3
rangeValidation: true
headerText: qsTr("RekeyAfterTime - Rekey after time")
textField.text: serverRekeyAfterTime
scroller: smartScroll
onEdited: (text) => { serverRekeyAfterTime = text }
}
AwgTextField {
id: rekeyTimeoutTextField
visible: isAwg3
rangeValidation: true
headerText: qsTr("RekeyTimeout - Rekey timeout")
textField.text: serverRekeyTimeout
scroller: smartScroll
onEdited: (text) => { serverRekeyTimeout = text }
}
AwgTextField {
id: rejectAfterTimeTextField
visible: isAwg3
rangeValidation: true
headerText: qsTr("RejectAfterTime - Reject after time")
textField.text: serverRejectAfterTime
scroller: smartScroll
onEdited: (text) => { serverRejectAfterTime = text }
}
AwgTextField {
id: keepaliveTimeoutTextField
visible: isAwg3
rangeValidation: true
headerText: qsTr("KeepaliveTimeout - Keepalive timeout")
textField.text: serverKeepaliveTimeout
scroller: smartScroll
onEdited: (text) => { serverKeepaliveTimeout = text }
}
AwgTextField {
id: maxHandshakeAttemptsTextField
visible: isAwg3
rangeValidation: true
headerText: qsTr("MaxHandshakeAttempts - Max handshake attempts")
textField.text: serverMaxHandshakeAttempts
scroller: smartScroll
onEdited: (text) => { serverMaxHandshakeAttempts = text }
}
BasicButtonType {
@@ -511,6 +408,12 @@ PageType {
transportPacketMagicHeaderTextField.errorText === "" &&
responsePacketMagicHeaderTextField.errorText === "" &&
initPacketMagicHeaderTextField.errorText === "" &&
contentPaddingAdditionTextField.errorText === "" &&
rekeyAfterTimeTextField.errorText === "" &&
rekeyTimeoutTextField.errorText === "" &&
rejectAfterTimeTextField.errorText === "" &&
keepaliveTimeoutTextField.errorText === "" &&
maxHandshakeAttemptsTextField.errorText === "" &&
responsePacketJunkSizeTextField.errorText === "" &&
cookieReplyPacketJunkSizeTextField.errorText === "" &&
transportPacketJunkSizeTextField.errorText === "" &&
@@ -540,10 +443,10 @@ PageType {
return
}
if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text),
parseInt(responsePacketJunkSizeTextField.textField.text),
parseInt(cookieReplyPacketJunkSizeTextField.textField.text),
parseInt(transportPacketJunkSizeTextField.textField.text))) {
if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text) || 0,
parseInt(responsePacketJunkSizeTextField.textField.text) || 0,
parseInt(cookieReplyPacketJunkSizeTextField.textField.text) || 0,
parseInt(transportPacketJunkSizeTextField.textField.text) || 0)) {
PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + S3 + cookie reply size (64) + S4 + transport packet size (32)"))
return
}

View File

@@ -37,6 +37,9 @@ PageType {
width: listView.width
spacing: 0
// xtls-rprx-vision requires the RAW (TCP) transport; it is invalid with XHTTP / mKCP.
readonly property bool visionAllowed: transport === "" || transport === "raw"
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
@@ -51,7 +54,7 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Empty")
checked: flow === ""
checked: flow === "" || !visionAllowed
onClicked: flow = ""
}
@@ -63,7 +66,8 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: "xtls-rprx-vision"
checked: flow === "xtls-rprx-vision"
enabled: visionAllowed
checked: flow === "xtls-rprx-vision" && visionAllowed
onClicked: flow = "xtls-rprx-vision"
}
@@ -75,13 +79,24 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: "xtls-rprx-vision-udp443"
checked: flow === "xtls-rprx-vision-udp443"
enabled: visionAllowed
checked: flow === "xtls-rprx-vision-udp443" && visionAllowed
onClicked: flow = "xtls-rprx-vision-udp443"
}
DividerType {
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: !visionAllowed
color: AmneziaStyle.color.goldenApricot
text: qsTr("xtls-rprx-vision is available only with the RAW (TCP) transport.")
}
Item {
Layout.preferredHeight: 16
}

View File

@@ -39,6 +39,9 @@ PageType {
width: listView.width
spacing: 0
// REALITY is not supported with the mKCP transport (xray refuses reality+mkcp).
readonly property bool realityAllowed: transport !== "mkcp"
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
@@ -53,7 +56,7 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("None")
checked: security === "none"
checked: security === "none" || (security === "reality" && !realityAllowed)
onClicked: security = "none"
}
@@ -77,10 +80,21 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Reality")
checked: security === "reality"
enabled: realityAllowed
checked: security === "reality" && realityAllowed
onClicked: security = "reality"
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: !realityAllowed
color: AmneziaStyle.color.goldenApricot
text: qsTr("REALITY is not supported with the mKCP transport. Use None or TLS.")
}
DividerType {
}
@@ -103,6 +117,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: alpn
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.alpnOptions()
@@ -155,6 +170,7 @@ PageType {
}
}
}
currentValue: fingerprint
clickedFunction: function () {
fingerprint = selectedText
tlsFingerprintDropDown.text = selectedText
@@ -185,6 +201,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)")
placeholderText: XrayConfigModel.sniDefault()
textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
@@ -200,7 +217,7 @@ PageType {
// ── Reality fields ────────────────────────────────────────
ColumnLayout {
visible: security === "reality"
visible: security === "reality" && realityAllowed
Layout.fillWidth: true
spacing: 0
@@ -225,6 +242,7 @@ PageType {
}
}
}
currentValue: fingerprint
clickedFunction: function () {
fingerprint = selectedText
realityFingerprintDropDown.text = selectedText
@@ -255,6 +273,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)")
placeholderText: XrayConfigModel.sniDefault()
textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)

View File

@@ -35,6 +35,10 @@ PageType {
return value
}
function formatFlow(value) {
return value === "" ? qsTr("Empty") : value
}
BackButtonType {
id: backButton
@@ -109,7 +113,8 @@ PageType {
Layout.rightMargin: 16
enabled: listView.enabled
headerText: qsTr("Port")
subtitleText: qsTr("165535")
hintText: qsTr("Valid range: 165535.")
placeholderText: XrayConfigModel.portDefault()
Binding {
target: textFieldWithHeaderType.textField
@@ -168,7 +173,7 @@ PageType {
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Security")
descriptionText: root.formatSecurity(security)
descriptionText: root.formatSecurity((security === "reality" && transport === "mkcp") ? "none" : security)
rightImageSource: "qrc:/images/controls/chevron-right.svg"
enabled: listView.enabled
clickedFunction: function() {
@@ -182,7 +187,7 @@ PageType {
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Flow")
descriptionText: flow
descriptionText: root.formatFlow((transport === "" || transport === "raw") ? flow : "")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
enabled: listView.enabled
clickedFunction: function() {
@@ -247,9 +252,9 @@ PageType {
visible: listView.enabled
clickedFunction: function() {
var yesButtonFunction = function() {
XrayConfigModel.resetToDefaults()
PageController.showNotificationMessage(
qsTr("Settings were reset to defaults. Tap Save to apply them on the server."))
XrayConfigModel.resetToDefaults()
}
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {

View File

@@ -123,7 +123,8 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("TTI")
subtitleText: qsTr("Range 10100, default %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
hintText: qsTr("Transmission time interval (ms). Valid range: 10100.")
placeholderText: XrayConfigModel.mkcpDefaultTti()
textField.text: mkcpTti
textField.maximumLength: 3
textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ }
@@ -142,7 +143,8 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("uplinkCapacity")
subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
hintText: qsTr("Uplink capacity (MB/s). Maximum: 2147483647.")
placeholderText: XrayConfigModel.mkcpDefaultUplinkCapacity()
textField.text: mkcpUplinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
@@ -161,7 +163,8 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("downlinkCapacity")
subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
hintText: qsTr("Downlink capacity (MB/s). Maximum: 2147483647.")
placeholderText: XrayConfigModel.mkcpDefaultDownlinkCapacity()
textField.text: mkcpDownlinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
@@ -180,7 +183,8 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("readBufferSize")
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
hintText: qsTr("Read buffer size (MB). Range: 12147483647.")
placeholderText: XrayConfigModel.mkcpDefaultReadBufferSize()
textField.text: mkcpReadBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
@@ -199,7 +203,8 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("writeBufferSize")
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
hintText: qsTr("Write buffer size (MB). Range: 12147483647.")
placeholderText: XrayConfigModel.mkcpDefaultWriteBufferSize()
textField.text: mkcpWriteBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
@@ -243,6 +248,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xhttpMode
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpModeOptions()
@@ -291,6 +297,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("Host")
placeholderText: XrayConfigModel.xhttpHostDefault()
textField.text: xhttpHost
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost)
@@ -322,50 +329,6 @@ PageType {
}
}
DropDownType {
id: headersDropDown
fitContent: true
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpHeadersTemplate
descriptionText: qsTr("Headers template")
headerText: qsTr("Headers template")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpHeadersTemplateOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpHeadersTemplate = selectedText
headersDropDown.text = selectedText
headersDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpHeadersTemplate) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
headersDropDown.text = xhttpHeadersTemplate
}
}
}
DropDownType {
id: uplinkMethodDropDown
fitContent: true
@@ -379,6 +342,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xhttpUplinkMethod
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpUplinkMethodOptions()
@@ -459,6 +423,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xhttpSessionPlacement
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpSessionPlacementOptions()
@@ -490,47 +455,20 @@ PageType {
}
}
DropDownType {
id: sessionKeyDropDown
fitContent: true
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpSessionKey
descriptionText: qsTr("SessionKey")
Layout.topMargin: 8
headerText: qsTr("SessionKey")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpSessionKeyOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
clickedFunction: function () {
xhttpSessionKey = selectedText
sessionKeyDropDown.text = selectedText
sessionKeyDropDown.closeTriggered()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === xhttpSessionKey) {
selectedIndex = i;
break
}
}
}
}
Connections {
target: XrayConfigModel
function onDataChanged() {
sessionKeyDropDown.text = xhttpSessionKey
}
textField.text: xhttpSessionKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpSessionKey)
textField.onEditingFinished: {
var v = textField.text.trim()
if (v !== xhttpSessionKey) xhttpSessionKey = v
else if (textField.text !== v) textField.text = v
root.editDirty = false
}
}
@@ -547,6 +485,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xhttpSeqPlacement
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpSeqPlacementOptions()
@@ -603,11 +542,12 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: xhttpUplinkDataPlacement
descriptionText: qsTr("UplinkDataPlacement")
descriptionText: qsTr("Header/Cookie apply only in Packet-up mode")
headerText: qsTr("UplinkDataPlacement")
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xhttpUplinkDataPlacement
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions()
@@ -673,7 +613,8 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("UplinkChunkSize")
subtitleText: qsTr("≥ 0 (0 = off)")
hintText: qsTr("Uplink chunk size in bytes. Maximum: 2147483647. 0 = off.")
placeholderText: XrayConfigModel.xhttpUplinkChunkSizeDefault()
textField.text: xhttpUplinkChunkSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
@@ -692,7 +633,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("scMaxBufferedPosts")
subtitleText: qsTr("≥ 0")
hintText: qsTr("Max buffered POSTs. Range: 02147483647.")
textField.text: xhttpScMaxBufferedPosts
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
@@ -720,6 +661,8 @@ PageType {
Layout.rightMargin: 16
minValue: xhttpScMaxEachPostBytesMin
maxValue: xhttpScMaxEachPostBytesMax
minPlaceholder: XrayConfigModel.scMaxEachPostBytesMinDefault()
maxPlaceholder: XrayConfigModel.scMaxEachPostBytesMaxDefault()
onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -740,6 +683,8 @@ PageType {
Layout.rightMargin: 16
minValue: xhttpScStreamUpServerSecsMin
maxValue: xhttpScStreamUpServerSecsMax
minPlaceholder: XrayConfigModel.scStreamUpServerSecsMinDefault()
maxPlaceholder: XrayConfigModel.scStreamUpServerSecsMaxDefault()
onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -760,6 +705,8 @@ PageType {
Layout.rightMargin: 16
minValue: xhttpScMinPostsIntervalMsMin
maxValue: xhttpScMinPostsIntervalMsMax
minPlaceholder: XrayConfigModel.scMinPostsIntervalMsMinDefault()
maxPlaceholder: XrayConfigModel.scMinPostsIntervalMsMaxDefault()
onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false }
onEdited: root.editDirty = true

View File

@@ -63,6 +63,8 @@ PageType {
Layout.rightMargin: 16
minValue: xPaddingBytesMin
maxValue: xPaddingBytesMax
minPlaceholder: XrayConfigModel.xPaddingBytesMinDefault()
maxPlaceholder: XrayConfigModel.xPaddingBytesMaxDefault()
onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true

View File

@@ -79,6 +79,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 16
headerText: qsTr("xPaddingKey")
placeholderText: XrayConfigModel.xPaddingKeyDefault()
textField.text: xPaddingKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey)
@@ -96,6 +97,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 8
headerText: qsTr("xPaddingHeader")
placeholderText: XrayConfigModel.xPaddingHeaderDefault()
textField.text: xPaddingHeader
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader)
@@ -120,6 +122,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xPaddingPlacement
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xPaddingPlacementOptions()
@@ -164,6 +167,7 @@ PageType {
drawerParent: root
listView: ListViewWithRadioButtonType {
rootWidth: root.width
currentValue: xPaddingMethod
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.xPaddingMethodOptions()

View File

@@ -93,6 +93,8 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxMaxConcurrencyMin
maxValue: xmuxMaxConcurrencyMax
minPlaceholder: "0"
maxPlaceholder: "0"
onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -114,6 +116,8 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxMaxConnectionsMin
maxValue: xmuxMaxConnectionsMax
minPlaceholder: "0"
maxPlaceholder: "0"
onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -135,6 +139,8 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxCMaxReuseTimesMin
maxValue: xmuxCMaxReuseTimesMax
minPlaceholder: "0"
maxPlaceholder: "0"
onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -156,6 +162,8 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxHMaxRequestTimesMin
maxValue: xmuxHMaxRequestTimesMax
minPlaceholder: "0"
maxPlaceholder: "0"
onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -177,6 +185,8 @@ PageType {
Layout.rightMargin: 16
minValue: xmuxHMaxReusableSecsMin
maxValue: xmuxHMaxReusableSecsMax
minPlaceholder: "0"
maxPlaceholder: "0"
onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false }
onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
@@ -188,7 +198,7 @@ PageType {
Layout.rightMargin: 16
Layout.topMargin: 16
headerText: qsTr("hKeepAlivePeriod")
subtitleText: qsTr("Integer, may be negative")
hintText: qsTr("HTTP keep-alive period. Integer, may be negative.")
textField.text: xmuxHKeepAlivePeriod
textField.maximumLength: 11
textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ }

View File

@@ -562,22 +562,6 @@ PageType {
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle.color.paleGray
visible: secret !== ""
onClicked: {
ExportController.generateQrFromString(tmeLink())
PageController.goToShareConnectionPage(
qsTr("Telegram connection link"),
qsTr("MTProxy connection link"),
"", "", "")
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
@@ -630,7 +614,7 @@ PageType {
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: {
ExportController.generateQrFromString(tgLink())
ExportController.generateQrFromStringRaw(tgLink())
PageController.goToShareConnectionPage(
qsTr("Telegram connection link"),
qsTr("MTProxy connection link"),
@@ -877,7 +861,7 @@ PageType {
}
function mtProxyShareQr(link) {
ExportController.generateQrFromString(link)
ExportController.generateQrFromStringRaw(link)
PageController.goToShareConnectionPage(qsTr("Telegram connection link"),
qsTr("MTProxy connection link"), "", "", "")
}
@@ -1462,15 +1446,6 @@ PageType {
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTmeLinkForAdditional(modelData))
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36

View File

@@ -562,22 +562,6 @@ PageType {
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle.color.paleGray
visible: secret !== ""
onClicked: {
ExportController.generateQrFromString(tmeLink())
PageController.goToShareConnectionPage(
qsTr("Telegram connection link"),
qsTr("Telemt connection link"),
"", "", "")
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
@@ -630,7 +614,7 @@ PageType {
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: {
ExportController.generateQrFromString(tgLink())
ExportController.generateQrFromStringRaw(tgLink())
PageController.goToShareConnectionPage(
qsTr("Telegram connection link"),
qsTr("Telemt connection link"),
@@ -877,7 +861,7 @@ PageType {
}
function telemtShareQr(link) {
ExportController.generateQrFromString(link)
ExportController.generateQrFromStringRaw(link)
PageController.goToShareConnectionPage(qsTr("Telegram connection link"),
qsTr("Telemt connection link"), "", "", "")
}
@@ -1462,15 +1446,6 @@ PageType {
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTmeLinkForAdditional(modelData))
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
@@ -1549,121 +1524,13 @@ PageType {
Layout.bottomMargin: 8
}
LabelTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.bottomMargin: 4
text: qsTr("Worker mode")
}
ButtonGroup {
id: workerModeGroup
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 4
spacing: 0
visible: transportMode !== "faketls"
HorizontalRadioButton {
Layout.fillWidth: true
text: qsTr("Auto")
ButtonGroup.group: workerModeGroup
checked: workersMode === "auto"
onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") }
}
HorizontalRadioButton {
Layout.fillWidth: true
text: qsTr("Manual")
ButtonGroup.group: workerModeGroup
checked: workersMode === "manual"
onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") }
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
visible: transportMode === "faketls"
text: qsTr("Workers are set to 0 automatically for FakeTLS mode.")
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
wrapMode: Text.WordWrap
}
TextFieldWithHeaderType {
id: workersTextField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: workersMode === "manual" && transportMode !== "faketls"
headerText: qsTr("Workers count")
textField.placeholderText: "2"
textField.text: workers
textField.maximumLength: 2
textField.inputMethodHints: Qt.ImhDigitsOnly
textField.validator: IntValidator {
bottom: 0
top: TelemtConfigModel.maxWorkers()
}
textField.onTextChanged: {
var cur = workersTextField.textField.text
if (cur === "") {
return
}
var n = parseInt(cur, 10)
var maxW = TelemtConfigModel.maxWorkers()
if (isNaN(n) || n < 0) {
n = 0
}
if (n > maxW) {
n = maxW
}
var clamped = String(n)
if (clamped !== cur) {
textField.text = clamped
textField.cursorPosition = clamped.length
}
}
textField.onEditingFinished: {
var v = workersTextField.textField.text
if (v !== "") {
var m = parseInt(v, 10)
var maxW2 = TelemtConfigModel.maxWorkers()
if (isNaN(m) || m < 0) {
m = 0
}
if (m > maxW2) {
m = maxW2
}
v = String(m)
textField.text = v
}
if (v !== workers) {
workers = v
TelemtConfigModel.setWorkers(workers)
}
}
}
DividerType {
Layout.fillWidth: true
Layout.bottomMargin: 8
}
SwitcherType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 4
text: qsTr("Server is behind NAT / Docker bridge")
descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network")
text: qsTr("Set public IP manually")
descriptionText: qsTr("By default the proxy auto-detects its public IP. Enable to override it manually, e.g. when the server is behind NAT / Docker bridge")
checked: natEnabled
onToggled: function () {
if (checked !== natEnabled) {
@@ -1673,41 +1540,6 @@ PageType {
}
}
TextFieldWithHeaderType {
id: natInternalIpTextField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: natEnabled
headerText: qsTr("Internal IP")
textField.placeholderText: "172.17.0.2"
textField.text: natInternalIp
textField.maximumLength: 15
textField.validator: RegularExpressionValidator {
regularExpression: root.natIpv4InputFormat
}
textField.onTextChanged: {
if (root.natIpv4FieldShowInvalidError(textField.text)) {
natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address")
} else {
natInternalIpTextField.errorText = ""
}
}
textField.onEditingFinished: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) {
natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address")
return
}
natInternalIpTextField.errorText = ""
if (textField.text !== natInternalIp) {
natInternalIp = textField.text
TelemtConfigModel.setNatInternalIp(natInternalIp)
}
}
}
TextFieldWithHeaderType {
id: natExternalIpTextField
Layout.fillWidth: true
@@ -1715,7 +1547,7 @@ PageType {
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: natEnabled
headerText: qsTr("External IP")
headerText: qsTr("Public IP")
textField.placeholderText: "1.2.3.4"
textField.text: natExternalIp
textField.maximumLength: 15
@@ -1912,7 +1744,6 @@ PageType {
publicHostTextField.errorText = ""
tagTextField.errorText = ""
tlsDomainTextField.errorText = ""
natInternalIpTextField.errorText = ""
natExternalIpTextField.errorText = ""
portTextField.errorText = ""
@@ -1948,13 +1779,9 @@ PageType {
errorLines.push(bullet + tlsErr)
}
var natIpErr = qsTr("Enter a valid IPv4 address")
if (!TelemtConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) {
natInternalIpTextField.errorText = natIpErr
errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address"))
}
if (!TelemtConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) {
natExternalIpTextField.errorText = natIpErr
errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address"))
errorLines.push(bullet + qsTr("Public IP: enter a valid IPv4 address"))
}
if (errorLines.length > 0) {
PageController.showErrorMessage(errorLines.join("\n"))
@@ -1969,15 +1796,7 @@ PageType {
: tlsDomainTextField.textField.text
TelemtConfigModel.setTlsDomain(domainValue)
if (transportMode === "faketls") {
workers = "0"
TelemtConfigModel.setWorkers("0")
} else {
TelemtConfigModel.setWorkersMode(workersMode)
TelemtConfigModel.setWorkers(workers)
}
TelemtConfigModel.setNatEnabled(natEnabled)
TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text)
TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text)
previousPort = port

View File

@@ -19,6 +19,7 @@ PageType {
property bool isUnsupportedContainer: ContainerProps.isUnsupportedContainer(ServersUiController.processedContainerIndex)
property bool isClearCacheVisible: !isUnsupportedContainer && ServersUiController.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ServersUiController.processedContainerIndex)
property bool isOutdatedAwgContainer: ServersUiController.isProcessedContainerOutdatedAwg()
BackButtonType {
id: backButton
@@ -50,11 +51,25 @@ PageType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
Layout.bottomMargin: root.isOutdatedAwgContainer ? 16 : 32
headerText: ContainersModel.getProcessedContainerName() + qsTr(" settings")
descriptionText: root.isUnsupportedContainer ? qsTr("This protocol is no longer supported.") : ""
}
WarningType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: root.isOutdatedAwgContainer
iconPath: "qrc:/images/controls/alert-circle.svg"
imageColor: AmneziaStyle.color.goldenApricot
textColor: AmneziaStyle.color.goldenApricot
textString: qsTr("AmneziaWG 2.0 is outdated and does not include the latest security improvements, but it will continue to work. Moving to AmneziaWG 3.0 by deploying a new container on the server is recommended for stronger protocol security")
}
}
model: root.isUnsupportedContainer ? null : ProtocolsModel

View File

@@ -70,6 +70,11 @@ PageType {
text: name
leftImageSource: ServersUiController.serverHasOutdatedAwgContainer(serverId) ? "qrc:/images/controls/alert-circle.svg" : ""
leftImageColor: AmneziaStyle.color.goldenApricot
isSmallLeftImage: true
isLeftImageHoverEnabled: false
descriptionText: {
var servicesNameString = ""
var servicesName = ServersUiController.getAllInstalledServicesName(index)

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