Connection string support for XRay protocol (#777)

* Connection string support for XRay protocol
This commit is contained in:
Mykola Baibuz 2024-05-27 16:15:55 +01:00 committed by GitHub
parent d8020878d5
commit e6ee9085a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 20709 additions and 11 deletions

View file

@ -691,6 +691,30 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
for (auto &port : fixedPorts) {
script = script.append("|:%1").arg(port);
}
if (transportProto == "tcpandudp") {
QString tcpProtoScript = script;
QString udpProtoScript = script;
tcpProtoScript.append("' | grep -i tcp");
udpProtoScript.append("' | grep -i udp");
tcpProtoScript.append(" | grep LISTEN");
ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
errorCode = runScript(credentials, replaceVars(udpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (!stdOut.isEmpty()) {
return ErrorCode::ServerPortAlreadyAllocatedError;
}
return ErrorCode::NoError;
}
script = script.append("' | grep -i %1").arg(transportProto);
if (transportProto == "tcp") {

View file

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

View file

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

View file

@ -0,0 +1,122 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this file. If not, see <https://www.gnu.org/licenses/>.
#include <QString>
#include <QJsonObject>
#include <QList>
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "transfer.h"
#include "serialization.h"
namespace amnezia::serialization::outbounds
{
QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect)
{
QJsonObject root;
JADD(domainStrategy, redirect)
return root;
}
QJsonObject GenerateBlackHoleOUT(bool useHTTP)
{
QJsonObject root;
QJsonObject resp;
resp.insert("type", useHTTP ? "http" : "none");
root.insert("response", resp);
return root;
}
QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password)
{
QJsonObject root;
JADD(address, port, method, password)
return root;
}
QJsonObject GenerateShadowSocksOUT(const QList<ShadowSocksServerObject> &_servers)
{
QJsonObject root;
QJsonArray x;
for (const auto &server : _servers)
{
x.append(GenerateShadowSocksServerOUT(server.address, server.port, server.method, server.password));
}
root.insert("servers", x);
return root;
}
QJsonObject GenerateHTTPSOCKSOut(const QString &addr, int port, bool useAuth, const QString &username, const QString &password)
{
QJsonObject root;
QJsonIO::SetValue(root, addr, "servers", 0, "address");
QJsonIO::SetValue(root, port, "servers", 0, "port");
if (useAuth)
{
QJsonIO::SetValue(root, username, "servers", 0, "users", 0, "user");
QJsonIO::SetValue(root, password, "servers", 0, "users", 0, "pass");
}
return root;
}
QJsonObject GenerateOutboundEntry(const QString &tag, const QString &protocol, const QJsonObject &settings, const QJsonObject &streamSettings,
const QJsonObject &mux, const QString &sendThrough)
{
QJsonObject root;
JADD(sendThrough, protocol, settings, tag, streamSettings, mux)
return root;
}
QJsonObject GenerateTrojanOUT(const QList<TrojanObject> &_servers)
{
QJsonObject root;
QJsonArray x;
for (const auto &server : _servers)
{
x.append(GenerateTrojanServerOUT(server.address, server.port, server.password));
}
root.insert("servers", x);
return root;
}
QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password)
{
QJsonObject root;
JADD(address, port, password)
return root;
}
} // namespace amnezia::serialization::outbounds

View file

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

View file

@ -0,0 +1,142 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#include "utilities.h"
#include "serialization.h"
#define OUTBOUND_TAG_PROXY "PROXY"
#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__)
namespace amnezia::serialization::ss
{
QJsonObject Deserialize(const QString &ssUri, QString *alias, QString *errMessage)
{
ShadowSocksServerObject server;
QString d_name;
// auto ssUri = _ssUri.toStdString();
if (ssUri.length() < 5)
{
*errMessage = QObject::tr("SS URI is too short");
}
auto uri = ssUri.mid(5);
auto hashPos = uri.lastIndexOf("#");
if (hashPos >= 0)
{
// Get the name/remark
d_name = uri.mid(uri.lastIndexOf("#") + 1);
uri.truncate(hashPos);
}
auto atPos = uri.indexOf('@');
if (atPos < 0)
{
// Old URI scheme
QString decoded = QByteArray::fromBase64(uri.toUtf8(), QByteArray::Base64Option::OmitTrailingEquals);
auto colonPos = decoded.indexOf(':');
if (colonPos < 0)
{
*errMessage = QObject::tr("Can't find the colon separator between method and password");
}
server.method = decoded.left(colonPos);
decoded.remove(0, colonPos + 1);
atPos = decoded.lastIndexOf('@');
if (atPos < 0)
{
*errMessage = QObject::tr("Can't find the at separator between password and hostname");
}
server.password = decoded.mid(0, atPos);
decoded.remove(0, atPos + 1);
colonPos = decoded.lastIndexOf(':');
if (colonPos < 0)
{
*errMessage = QObject::tr("Can't find the colon separator between hostname and port");
}
server.address = decoded.mid(0, colonPos);
server.port = decoded.mid(colonPos + 1).toInt();
}
else
{
// SIP002 URI scheme
auto x = QUrl::fromUserInput(uri);
server.address = x.host();
server.port = x.port();
const auto userInfo = Utils::SafeBase64Decode(x.userName());
const auto userInfoSp = userInfo.indexOf(':');
if (userInfoSp < 0)
{
*errMessage = QObject::tr("Can't find the colon separator between method and password");
return QJsonObject{};
}
const auto method = userInfo.mid(0, userInfoSp);
server.method = method;
server.password = userInfo.mid(userInfoSp + 1);
}
d_name = QUrl::fromPercentEncoding(d_name.toUtf8());
QJsonObject root;
QJsonArray outbounds;
outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ server }), {}));
JADD(outbounds)
QJsonObject inbound = inbounds::GenerateInboundEntry();
root["inbounds"] = QJsonArray{ inbound };
*alias = alias->isEmpty() ? d_name : *alias + "_" + d_name;
return root;
}
const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool)
{
QUrl url;
const auto plainUserInfo = server.method + ":" + server.password;
const auto userinfo = plainUserInfo.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
url.setUserInfo(userinfo);
url.setScheme("ss");
url.setHost(server.address);
url.setPort(server.port);
url.setFragment(alias);
return url.toString(QUrl::ComponentFormattingOption::FullyEncoded);
}
} // namespace amnezia::serialization::ss

View file

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

View file

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

View file

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

View file

@ -0,0 +1,256 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include <QUrlQuery>
#include "serialization.h"
namespace amnezia::serialization::vless
{
QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
{
// must start with vless://
if (!str.startsWith("vless://"))
{
*errMessage = QObject::tr("VLESS link should start with vless://");
return QJsonObject();
}
// parse url
QUrl url(str);
if (!url.isValid())
{
*errMessage = QObject::tr("link parse failed: %1").arg(url.errorString());
return QJsonObject();
}
// fetch host
const auto hostRaw = url.host();
if (hostRaw.isEmpty())
{
*errMessage = QObject::tr("empty host");
return QJsonObject();
}
const auto host = (hostRaw.startsWith('[') && hostRaw.endsWith(']')) ? hostRaw.mid(1, hostRaw.length() - 2) : hostRaw;
// fetch port
const auto port = url.port();
if (port == -1)
{
*errMessage = QObject::tr("missing port");
return QJsonObject();
}
// fetch remarks
const auto remarks = url.fragment();
if (!remarks.isEmpty())
{
*alias = remarks;
}
// fetch uuid
const auto uuid = url.userInfo();
if (uuid.isEmpty())
{
*errMessage = QObject::tr("missing uuid");
return QJsonObject();
}
// initialize QJsonObject with basic info
QJsonObject outbound;
QJsonObject stream;
QJsonIO::SetValue(outbound, "vless", "protocol");
QJsonIO::SetValue(outbound, host, { "settings", "vnext", 0, "address" });
QJsonIO::SetValue(outbound, port, { "settings", "vnext", 0, "port" });
QJsonIO::SetValue(outbound, uuid, { "settings", "vnext", 0, "users", 0, "id" });
// parse query
QUrlQuery query(url.query());
// handle type
const auto hasType = query.hasQueryItem("type");
const auto type = hasType ? query.queryItemValue("type") : "tcp";
if (type != "tcp")
QJsonIO::SetValue(stream, type, "network");
// handle encryption
const auto hasEncryption = query.hasQueryItem("encryption");
const auto encryption = hasEncryption ? query.queryItemValue("encryption") : "none";
QJsonIO::SetValue(outbound, encryption, { "settings", "vnext", 0, "users", 0, "encryption" });
// type-wise settings
if (type == "kcp")
{
const auto hasSeed = query.hasQueryItem("seed");
if (hasSeed)
QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" });
const auto hasHeaderType = query.hasQueryItem("headerType");
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
if (headerType != "none")
QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" });
}
else if (type == "http")
{
const auto hasPath = query.hasQueryItem("path");
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
if (path != "/")
QJsonIO::SetValue(stream, path, { "httpSettings", "path" });
const auto hasHost = query.hasQueryItem("host");
if (hasHost)
{
const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(","));
QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" });
}
}
else if (type == "ws")
{
const auto hasPath = query.hasQueryItem("path");
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
if (path != "/")
QJsonIO::SetValue(stream, path, { "wsSettings", "path" });
const auto hasHost = query.hasQueryItem("host");
if (hasHost)
{
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" });
}
}
else if (type == "quic")
{
const auto hasQuicSecurity = query.hasQueryItem("quicSecurity");
if (hasQuicSecurity)
{
const auto quicSecurity = query.queryItemValue("quicSecurity");
QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" });
if (quicSecurity != "none")
{
const auto key = query.queryItemValue("key");
QJsonIO::SetValue(stream, key, { "quicSettings", "key" });
}
const auto hasHeaderType = query.hasQueryItem("headerType");
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
if (headerType != "none")
QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" });
}
}
else if (type == "grpc")
{
const auto hasServiceName = query.hasQueryItem("serviceName");
if (hasServiceName)
{
const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8());
QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" });
}
const auto hasMode = query.hasQueryItem("mode");
if (hasMode)
{
const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi";
QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" });
}
}
// tls-wise settings
const auto hasSecurity = query.hasQueryItem("security");
const auto security = hasSecurity ? query.queryItemValue("security") : "none";
const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" );
if (security != "none")
{
QJsonIO::SetValue(stream, security, "security");
}
// sni
const auto hasSNI = query.hasQueryItem("sni");
if (hasSNI)
{
const auto sni = query.queryItemValue("sni");
QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" });
}
// alpn
const auto hasALPN = query.hasQueryItem("alpn");
if (hasALPN)
{
const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8());
QStringList aplnElems = alpnRaw.split(",");
// h2 protocol is not supported by xray
aplnElems.removeAll("h2");
if (!aplnElems.isEmpty()) {
const auto alpnArray = QJsonArray::fromStringList(aplnElems);
QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" });
}
}
// xtls-specific
if (security == "xtls" || security == "reality")
{
const auto flow = query.queryItemValue("flow");
QJsonIO::SetValue(outbound, flow, { "settings", "vnext", 0, "users", 0, "flow" });
}
if (security == "reality")
{
if (query.hasQueryItem("fp"))
{
const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8());
QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" });
}
if (query.hasQueryItem("pbk"))
{
const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8());
QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" });
}
if (query.hasQueryItem("spiderX"))
{
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8());
QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" });
}
if (query.hasQueryItem("sid"))
{
const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8());
QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" });
}
}
// assembling config
QJsonObject root;
outbound["streamSettings"] = stream;
QJsonObject inbound = inbounds::GenerateInboundEntry();
root["outbounds"] = QJsonArray{ outbound };
root["inbounds"] = QJsonArray { inbound };
return root;
}
} // namespace amnezia::serialization::vless

View file

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

View file

@ -0,0 +1,172 @@
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
// This file is part of the Qv2ray VPN client.
//
// Qv2ray, A Qt frontend for V2Ray. Written in C++
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Copyright (c) 2024 AmneziaVPN
// This file has been modified for AmneziaVPN
//
// This file is based on the work of the Qv2ray VPN client.
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
//
// The modified version of this file is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this file. If not, see <https://www.gnu.org/licenses/>.
#include "3rd/QJsonStruct/QJsonIO.hpp"
#include "3rd/QJsonStruct/QJsonStruct.hpp"
#include "transfer.h"
#include "serialization.h"
#include <QUrlQuery>
#define OUTBOUND_TAG_PROXY "PROXY"
namespace amnezia::serialization::vmess_new
{
const static QStringList NetworkType{ "tcp", "http", "ws", "kcp", "quic", "grpc" };
const static QStringList QuicSecurityTypes{ "none", "aes-128-gcm", "chacha20-poly1305" };
const static QStringList QuicKcpHeaderTypes{ "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" };
const static QStringList FalseTypes{ "false", "False", "No", "Off", "0" };
QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage)
{
QUrl url{ vmessStr };
QUrlQuery query{ url };
//
#define default QJsonObject()
if (!url.isValid())
{
*errMessage = QObject::tr("vmess:// url is invalid");
return default;
}
// If previous alias is empty, just the PS is needed, else, append a "_"
const auto name = url.fragment(QUrl::FullyDecoded).trimmed();
*alias = alias->isEmpty() ? name : (*alias + "_" + name);
VMessServerObject server;
server.users << VMessServerObject::UserObject{};
StreamSettingsObject stream;
QString net;
bool tls = false;
// Check streamSettings
{
for (const auto &_protocol : url.userName().split("+"))
{
if (_protocol == "tls")
tls = true;
else
net = _protocol;
}
if (!NetworkType.contains(net))
{
*errMessage = QObject::tr("Invalid streamSettings protocol: ") + net;
return default;
}
stream.network = net;
stream.security = tls ? "tls" : "";
}
// Host Port UUID AlterID
{
const auto host = url.host();
int port = url.port();
QString uuid;
int aid;
{
const auto pswd = url.password();
const auto index = pswd.lastIndexOf("-");
uuid = pswd.mid(0, index);
aid = pswd.right(pswd.length() - index - 1).toInt();
}
server.address = host;
server.port = port;
server.users.first().id = uuid;
server.users.first().alterId = aid;
server.users.first().security = "auto";
}
const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) {
if (query.hasQueryItem(key))
return query.queryItemValue(key, QUrl::FullyDecoded);
else
return defaultValue;
};
//
// Begin transport settings parser
{
if (net == "tcp")
{
stream.tcpSettings.header.type = getQueryValue("type", "none");
}
else if (net == "http")
{
stream.httpSettings.host.append(getQueryValue("host", ""));
stream.httpSettings.path = getQueryValue("path", "/");
}
else if (net == "ws")
{
stream.wsSettings.headers["Host"] = getQueryValue("host", "");
stream.wsSettings.path = getQueryValue("path", "/");
}
else if (net == "kcp")
{
stream.kcpSettings.seed = getQueryValue("seed", "");
stream.kcpSettings.header.type = getQueryValue("type", "none");
}
else if (net == "quic")
{
stream.quicSettings.security = getQueryValue("security", "none");
stream.quicSettings.key = getQueryValue("key", "");
stream.quicSettings.header.type = getQueryValue("type", "none");
}
else if (net == "grpc")
{
stream.grpcSettings.serviceName = getQueryValue("serviceName", "");
}
else
{
*errMessage = QObject::tr("Unknown transport method: ") + net;
return default;
}
}
#undef default
if (tls)
{
stream.tlsSettings.allowInsecure = !FalseTypes.contains(getQueryValue("allowInsecure", "false"));
stream.tlsSettings.serverName = getQueryValue("tlsServerName", "");
}
QJsonObject root;
QJsonObject vConf;
QJsonArray vnextArray;
vnextArray.append(server.toJson());
vConf["vnext"] = vnextArray;
auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, stream.toJson());
QJsonObject inbound = inbounds::GenerateInboundEntry();
//
root["outbounds"] = QJsonArray{ outbound };
root["inbound"] = QJsonArray{ inbound };
return root;
}
} // namespace amnezia::serialization::vmess_new