Xray with Reality protocol (#494)

* Xray with Reality for desktops
This commit is contained in:
Mykola Baibuz 2024-03-27 11:02:34 +00:00 committed by GitHub
parent f6acec53c0
commit ba4237f1dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1933 additions and 336 deletions

View file

@ -337,6 +337,38 @@ void ExportController::generateCloakConfig()
emit exportConfigChanged();
}
void ExportController::generateXrayConfig()
{
clearPreviousConfig();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
containerConfig.insert(config_key::container, ContainerProps::containerToString(container));
ErrorCode errorCode = ErrorCode::NoError;
QString clientId;
QString config =
m_configurator->genVpnProtocolConfig(credentials, container, containerConfig, Proto::Xray, clientId, &errorCode);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
return;
}
config = m_configurator->processConfigWithExportSettings(serverIndex, container, Proto::Xray, config);
QJsonObject configJson = QJsonDocument::fromJson(config.toUtf8()).object();
QStringList lines = QString(QJsonDocument(configJson).toJson()).replace("\r", "").split("\n");
for (const QString &line : lines) {
m_config.append(line + "\n");
}
emit exportConfigChanged();
}
QString ExportController::getConfig()
{
return m_config;

View file

@ -37,6 +37,7 @@ public slots:
void generateAwgConfig(const QString &clientName);
void generateShadowSocksConfig();
void generateCloakConfig();
void generateXrayConfig();
QString getConfig();
QString getNativeConfigString();

View file

@ -19,6 +19,7 @@ namespace
Amnezia,
OpenVpn,
WireGuard,
Xray,
Backup,
Invalid
};
@ -34,6 +35,9 @@ namespace
const QString wireguardConfigPatternSectionInterface = "[Interface]";
const QString wireguardConfigPatternSectionPeer = "[Peer]";
const QString xrayConfigPatternInbound = "inbounds";
const QString xrayConfigPatternOutbound = "outbounds";
const QString amneziaConfigPattern = "containers";
const QString amneziaFreeConfigPattern = "api_key";
const QString backupPattern = "Servers/serversList";
@ -49,6 +53,8 @@ namespace
} else if (config.contains(wireguardConfigPatternSectionInterface)
&& config.contains(wireguardConfigPatternSectionPeer)) {
return ConfigTypes::WireGuard;
} else if ((config.contains(xrayConfigPatternInbound)) && (config.contains(xrayConfigPatternOutbound))) {
return ConfigTypes::Xray;
}
return ConfigTypes::Invalid;
}
@ -109,6 +115,10 @@ bool ImportController::extractConfigFromData(QString data)
m_config = extractWireGuardConfig(config);
return m_config.empty() ? false : true;
}
case ConfigTypes::Xray: {
m_config = extractXrayConfig(config);
return m_config.empty() ? false : true;
}
case ConfigTypes::Amnezia: {
m_config = QJsonDocument::fromJson(config.toUtf8()).object();
return m_config.empty() ? false : true;
@ -349,6 +359,42 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
return config;
}
QJsonObject ImportController::extractXrayConfig(const QString &data)
{
QJsonParseError parserErr;
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
QJsonObject xrayVpnConfig;
xrayVpnConfig[config_key::config] = jsonConf.toJson().constData();
QJsonObject lastConfig;
lastConfig[config_key::last_config] = jsonConf.toJson().constData();
lastConfig[config_key::isThirdPartyConfig] = true;
QJsonObject containers;
containers.insert(config_key::container, QJsonValue("amnezia-xray"));
containers.insert(config_key::xray, QJsonValue(lastConfig));
QJsonArray arr;
arr.push_back(containers);
QString hostName;
const static QRegularExpression hostNameRegExp("\"address\":\\s*\"([^\"]+)");
QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data);
if (hostNameMatch.hasMatch()) {
hostName = hostNameMatch.captured(1);
}
QJsonObject config;
config[config_key::containers] = arr;
config[config_key::defaultContainer] = "amnezia-xray";
config[config_key::description] = m_settings->nextAvailableServerName();
config[config_key::hostName] = hostName;
return config;
}
#ifdef Q_OS_ANDROID
static QMutex qrDecodeMutex;

View file

@ -47,6 +47,7 @@ signals:
private:
QJsonObject extractOpenVpnConfig(const QString &data);
QJsonObject extractWireGuardConfig(const QString &data);
QJsonObject extractXrayConfig(const QString &data);
#if defined Q_OS_ANDROID || defined Q_OS_IOS
void stopDecodingQr();

View file

@ -9,6 +9,7 @@
#include "core/errorstrings.h"
#include "core/controllers/serverController.h"
#include "core/networkUtilities.h"
#include "utilities.h"
#include "ui/models/protocols/awgConfigModel.h"
#include "ui/models/protocols/wireguardConfigModel.h"
@ -352,12 +353,12 @@ void InstallController::removeCurrentlyProcessedContainer()
QRegularExpression InstallController::ipAddressPortRegExp()
{
return Utils::ipAddressPortRegExp();
return NetworkUtilities::ipAddressPortRegExp();
}
QRegularExpression InstallController::ipAddressRegExp()
{
return Utils::ipAddressRegExp();
return NetworkUtilities::ipAddressRegExp();
}
void InstallController::setCurrentlyInstalledServerCredentials(const QString &hostName, const QString &userName,
@ -450,7 +451,6 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw
process->write((password + "\n").toUtf8());
}
// qDebug().noquote() << "onPushButtonSftpMountDriveClicked" << args;
#endif
}

View file

@ -48,6 +48,7 @@ namespace PageLoader
PageProtocolOpenVpnSettings,
PageProtocolShadowSocksSettings,
PageProtocolCloakSettings,
PageProtocolXraySettings,
PageProtocolWireGuardSettings,
PageProtocolAwgSettings,
PageProtocolIKev2Settings,

View file

@ -5,7 +5,7 @@
#include <QStandardPaths>
#include "systemController.h"
#include "utilities.h"
#include "core/networkUtilities.h"
SitesController::SitesController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<VpnConnection> &vpnConnection,
@ -25,7 +25,7 @@ void SitesController::addSite(QString hostname)
return;
}
if (!Utils::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
if (!NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
// get domain name if it present
hostname.replace("https://", "");
hostname.replace("http://", "");
@ -40,7 +40,7 @@ void SitesController::addSite(QString hostname)
if (!ip.isEmpty()) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << ip));
} else if (Utils::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
} else if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
}
@ -57,7 +57,7 @@ void SitesController::addSite(QString hostname)
}
};
if (Utils::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
processSite(hostname, "");
} else {
processSite(hostname, "");
@ -110,7 +110,7 @@ void SitesController::importSites(const QString &fileName, bool replaceExisting)
auto hostname = jsonObject.value("hostname").toString("");
auto ip = jsonObject.value("ip").toString("");
if (!hostname.contains(".") && !Utils::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
if (!hostname.contains(".") && !NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
qDebug() << hostname << " not look like ip adress or domain name";
continue;
}

View file

@ -0,0 +1,69 @@
#include "xrayConfigModel.h"
#include "protocols/protocols_defs.h"
XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent)
{
}
int XrayConfigModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerProps::allContainers().size()) {
return false;
}
switch (role) {
case Roles::SiteRole: m_protocolConfig.insert(config_key::site, value.toString()); break;
}
emit dataChanged(index, index, QList { role });
return true;
}
QVariant XrayConfigModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
return false;
}
switch (role) {
case Roles::SiteRole: return m_protocolConfig.value(config_key::site).toString(protocols::xray::defaultSite);
}
return QVariant();
}
void XrayConfigModel::updateModel(const QJsonObject &config)
{
beginResetModel();
m_container = ContainerProps::containerFromString(config.value(config_key::container).toString());
m_fullConfig = config;
QJsonObject protocolConfig = config.value(config_key::xray).toObject();
m_protocolConfig.insert(config_key::site,
protocolConfig.value(config_key::site).toString(protocols::xray::defaultSite));
endResetModel();
}
QJsonObject XrayConfigModel::getConfig()
{
m_fullConfig.insert(config_key::xray, m_protocolConfig);
return m_fullConfig;
}
QHash<int, QByteArray> XrayConfigModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[SiteRole] = "site";
return roles;
}

View file

@ -0,0 +1,38 @@
#ifndef XRAYCONFIGMODEL_H
#define XRAYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QJsonObject>
#include "containers/containers_defs.h"
class XrayConfigModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
SiteRole
};
explicit XrayConfigModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(const QJsonObject &config);
QJsonObject getConfig();
protected:
QHash<int, QByteArray> roleNames() const override;
private:
DockerContainer m_container;
QJsonObject m_protocolConfig;
QJsonObject m_fullConfig;
};
#endif // XRAYCONFIGMODEL_H

View file

@ -79,6 +79,8 @@ PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const
case Proto::WireGuard: return PageLoader::PageEnum::PageProtocolWireGuardSettings;
case Proto::Ikev2: return PageLoader::PageEnum::PageProtocolIKev2Settings;
case Proto::L2tp: return PageLoader::PageEnum::PageProtocolIKev2Settings;
case Proto::Xray: return PageLoader::PageEnum::PageProtocolXraySettings;
// non-vpn
case Proto::TorWebSite: return PageLoader::PageEnum::PageServiceTorWebsiteSettings;
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;

View file

@ -57,6 +57,12 @@ ListView {
PageController.goToPage(PageEnum.PageProtocolOpenVpnSettings)
break
}
case ContainerEnum.Xray: {
XrayConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageProtocolXraySettings)
break
}
case ContainerEnum.WireGuard: {
WireGuardConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageProtocolWireGuardSettings)

View file

@ -0,0 +1,156 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ContainerEnum 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
ColumnLayout {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
BackButtonType {
}
}
FlickableType {
id: fl
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
contentHeight: content.implicitHeight
Column {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess()
ListView {
id: listview
width: parent.width
height: listview.contentItem.height
clip: true
interactive: false
model: XrayConfigModel
delegate: Item {
implicitWidth: listview.width
implicitHeight: col.implicitHeight
ColumnLayout {
id: col
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 0
HeaderType {
Layout.fillWidth: true
headerText: qsTr("XRay settings")
}
TextFieldWithHeaderType {
Layout.fillWidth: true
Layout.topMargin: 32
headerText: qsTr("Disguised as traffic from")
textFieldText: site
textField.onEditingFinished: {
if (textFieldText !== site) {
var tmpText = textFieldText
tmpText = tmpText.toLocaleLowerCase()
var indexHttps = tmpText.indexOf("https://")
if (indexHttps === 0) {
tmpText = textFieldText.substring(8)
} else {
site = textFieldText
}
}
}
}
BasicButtonType {
Layout.topMargin: 24
Layout.leftMargin: -8
implicitHeight: 32
visible: ContainersModel.getCurrentlyProcessedContainerIndex() === ContainerEnum.Xray
defaultColor: "transparent"
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
pressedColor: Qt.rgba(1, 1, 1, 0.12)
textColor: "#EB5757"
text: qsTr("Remove XRay")
onClicked: {
questionDrawer.headerText = qsTr("Remove XRay from server?")
questionDrawer.descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.")
questionDrawer.yesButtonText = qsTr("Continue")
questionDrawer.noButtonText = qsTr("Cancel")
questionDrawer.yesButtonFunction = function() {
questionDrawer.visible = false
PageController.goToPage(PageEnum.PageDeinstalling)
InstallController.removeCurrentlyProcessedContainer()
}
questionDrawer.noButtonFunction = function() {
questionDrawer.visible = false
}
questionDrawer.visible = true
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
text: qsTr("Save and Restart Amnezia")
onClicked: {
forceActiveFocus()
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateContainer(XrayConfigModel.getConfig())
}
}
}
}
}
}
QuestionDrawer {
id: questionDrawer
}
}
}

View file

@ -84,6 +84,7 @@ PageType {
case ProtocolEnum.OpenVpn: OpenVpnConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.ShadowSocks: ShadowSocksConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.Cloak: CloakConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.Xray: XrayConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.WireGuard: WireGuardConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.Ipsec: Ikev2ConfigModel.updateModel(ProtocolsModel.getConfig()); break;
}

View file

@ -69,8 +69,8 @@ PageType {
leftImageSource: "qrc:/images/controls/folder-open.svg"
clickedFunction: function() {
var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.backup)" :
"Config files (*.vpn *.ovpn *.conf)"
var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.json *.backup)" :
"Config files (*.vpn *.ovpn *.conf *.json)"
var fileName = SystemController.getFileName(qsTr("Open config file"), nameFilter)
if (fileName !== "") {
if (ImportController.extractConfigFromFile(fileName)) {

View file

@ -24,7 +24,8 @@ PageType {
WireGuard,
Awg,
ShadowSocks,
Cloak
Cloak,
Xray
}
signal revokeConfig(int index)
@ -88,6 +89,13 @@ PageType {
shareConnectionDrawer.configFileName = "amnezia_for_cloak"
break
}
case PageShare.ConfigType.Xray: {
ExportController.generateXrayConfig()
shareConnectionDrawer.configCaption = qsTr("Save XRay config")
shareConnectionDrawer.configExtension = ".json"
shareConnectionDrawer.configFileName = "amnezia_for_xray"
break
}
}
PageController.showBusyIndicator(false)
@ -136,6 +144,11 @@ PageType {
property string name: qsTr("Cloak native format")
property var type: PageShare.ConfigType.Cloak
}
QtObject {
id: xrayConnectionFormat
property string name: qsTr("XRay native format")
property var type: PageShare.ConfigType.Xray
}
FlickableType {
anchors.top: parent.top
@ -435,6 +448,8 @@ PageType {
root.connectionTypesModel.push(openVpnConnectionFormat)
root.connectionTypesModel.push(shadowSocksConnectionFormat)
root.connectionTypesModel.push(cloakConnectionFormat)
} else if (index === ContainerProps.containerFromString("amnezia-xray")) {
root.connectionTypesModel.push(xrayConnectionFormat)
}
}
}