Compare commits

...
25 changed files with 4332 additions and 271 deletions

No files matched your search

@@ -14,6 +14,7 @@
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocolConfig.h"
#include "core/models/protocols/xrayProtocolConfig.h"
#include "version.h"
using namespace amnezia;
@@ -317,7 +318,7 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
vlessServer.address = server.value(amnezia::protocols::xray::address).toString();
vlessServer.port = server.value(amnezia::protocols::xray::port).toInt();
vlessServer.id = user.value(amnezia::protocols::xray::id).toString();
vlessServer.flow = user.value(amnezia::protocols::xray::flow).toString("xtls-rprx-vision");
vlessServer.flow = user.value(amnezia::protocols::xray::flow).toString();
vlessServer.encryption = user.value(amnezia::protocols::xray::encryption).toString("none");
vlessServer.network = streamSettings.value(amnezia::protocols::xray::network).toString("tcp");
@@ -328,7 +329,8 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
vlessServer.serverName = realitySettings.value(amnezia::protocols::xray::serverName).toString();
vlessServer.publicKey = realitySettings.value(amnezia::protocols::xray::publicKey).toString();
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString(QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint));
vlessServer.fingerprint = amnezia::xrayEffective::realityFingerprint(
realitySettings.value(amnezia::protocols::xray::fingerprint).toString());
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
} else if (vlessServer.security == "tls") {
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
@@ -340,8 +342,20 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
for (const QJsonValue &v : alpnArr) {
alpnList << v.toString();
}
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
vlessServer.alpn = alpnList.join(",");
vlessServer.tlsPin = tlsSettings.value(amnezia::protocols::xray::pinnedPeerCertSha256).toString();
}
if (vlessServer.network == "xhttp") {
QJsonObject xhttpSettings = streamSettings.value(amnezia::protocols::xray::xhttpSettings).toObject();
vlessServer.xhttpHost = xhttpSettings.value(amnezia::protocols::xray::xhttpHost).toString();
vlessServer.xhttpPath = xhttpSettings.value(amnezia::protocols::xray::xhttpPath).toString();
vlessServer.xhttpMode = xhttpSettings.value(amnezia::protocols::xray::xhttpMode).toString();
xhttpSettings.remove(amnezia::protocols::xray::xhttpHost);
xhttpSettings.remove(amnezia::protocols::xray::xhttpPath);
xhttpSettings.remove(amnezia::protocols::xray::xhttpMode);
if (!xhttpSettings.isEmpty())
vlessServer.xhttpExtra = QString::fromUtf8(QJsonDocument(xhttpSettings).toJson(QJsonDocument::Compact));
}
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, APPLICATION_NAME);
@@ -202,6 +202,28 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
return c;
}
void XrayXhttpConfig::applyRangeDefaults()
{
if (scMaxEachPostBytesMin.isEmpty()) {
scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
}
if (scMaxEachPostBytesMax.isEmpty()) {
scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
}
if (scMinPostsIntervalMsMin.isEmpty()) {
scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
}
if (scMinPostsIntervalMsMax.isEmpty()) {
scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
}
if (scStreamUpServerSecsMin.isEmpty()) {
scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
}
if (scStreamUpServerSecsMax.isEmpty()) {
scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
}
}
QJsonObject XrayMkcpConfig::toJson() const
{
QJsonObject obj;
@@ -386,36 +408,20 @@ void XrayServerConfig::applyDefaults(bool fillFlowDefault)
if (xhttp.uplinkMethod.isEmpty()) {
xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
}
if (xhttp.sessionPlacement.isEmpty()) {
if (xhttp.sessionPlacement.isEmpty()
|| xhttp.sessionPlacement.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0) {
xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
}
if (xhttp.sessionKey.isEmpty()) {
xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey;
}
if (xhttp.seqPlacement.isEmpty()) {
if (xhttp.seqPlacement.isEmpty()
|| xhttp.seqPlacement.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0) {
xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
}
if (xhttp.uplinkDataPlacement.isEmpty()) {
xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
}
if (xhttp.scMaxEachPostBytesMin.isEmpty()) {
xhttp.scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
}
if (xhttp.scMaxEachPostBytesMax.isEmpty()) {
xhttp.scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
}
if (xhttp.scMinPostsIntervalMsMin.isEmpty()) {
xhttp.scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
}
if (xhttp.scMinPostsIntervalMsMax.isEmpty()) {
xhttp.scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
}
if (xhttp.scStreamUpServerSecsMin.isEmpty()) {
xhttp.scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
}
if (xhttp.scStreamUpServerSecsMax.isEmpty()) {
xhttp.scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
}
if (xhttp.xPadding.placement.isEmpty()) {
xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement;
@@ -509,6 +515,25 @@ namespace xrayEffective
return srv.security;
}
QString realityFingerprint(const QString &fingerprint)
{
static const QStringList mlkemCapable = { "chrome", "firefox", "safari",
"hellochrome_131", "hellochrome_133", "hellochrome_auto",
"hellofirefox_148", "hellofirefox_auto", "hellosafari_26_3",
"hellosafari_auto" };
const QString normalized = fingerprint.trimmed().toLower();
return mlkemCapable.contains(normalized) ? normalized
: QString::fromLatin1(protocols::xray::defaultRealityFingerprint);
}
QString realityServerName(const XrayServerConfig &srv)
{
const QString sniEff = srv.sni.trimmed();
if (!sniEff.isEmpty())
return sniEff;
return srv.site.isEmpty() ? QString::fromLatin1(protocols::xray::defaultSite) : srv.site;
}
QString clientFlow(const XrayServerConfig &srv)
{
namespace px = protocols::xray;
@@ -540,6 +565,33 @@ namespace xrayEffective
{
return xhttpMode(srv.xhttp.mode);
}
QStringList alpnList(const QString &alpn)
{
QStringList list;
for (const QString &part : alpn.split(QLatin1Char(','))) {
QString t = part.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())
list << t;
}
return list;
}
bool xhttpUsesHttp11(const XrayServerConfig &srv)
{
namespace px = protocols::xray;
const QString securityEff = security(srv);
if (securityEff.isEmpty() || securityEff == QLatin1String(px::securityNone))
return true;
if (securityEff != QLatin1String(px::securityTls))
return false;
const QStringList alpn = alpnList(srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn);
return alpn == QStringList { QStringLiteral("http/1.1") };
}
}
QJsonObject XrayServerConfig::serverStreamSettings() const
@@ -557,9 +609,8 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
namespace px = protocols::xray;
const bool clientSide = side == XrayStreamSide::Client;
const QString fingerprintEff = fingerprint.isEmpty()
? QString::fromLatin1(px::defaultFingerprint)
: fingerprint;
const QString fingerprintEff = fingerprint.trimmed().isEmpty() ? QString::fromLatin1(px::defaultFingerprint)
: fingerprint.trimmed().toLower();
QJsonObject streamSettings;
streamSettings[px::network] = xrayEffective::network(*this);
@@ -569,19 +620,10 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
if (securityEff == QLatin1String(px::securityTls)) {
QJsonObject tlsSettings;
tlsSettings[px::serverName] = sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : sni;
const QString alpnEff = alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : alpn;
QJsonArray alpnArray;
for (const QString &a : alpnEff.split(QLatin1Char(','))) {
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);
}
const QString sniEff = sni.trimmed();
tlsSettings[px::serverName] = sniEff.isEmpty() ? QString::fromLatin1(px::defaultSni) : sniEff;
const QJsonArray alpnArray = QJsonArray::fromStringList(
xrayEffective::alpnList(alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : alpn));
if (!alpnArray.isEmpty())
tlsSettings[px::alpn] = alpnArray;
if (clientSide && transport != QLatin1String(px::transportMkcp))
@@ -597,8 +639,9 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
if (clientSide && securityEff == QLatin1String(px::securityReality)) {
QJsonObject realitySettings;
realitySettings[px::fingerprint] = fingerprintEff;
realitySettings[px::serverName] = sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : sni;
realitySettings[px::fingerprint] =
isThirdPartyConfig ? fingerprintEff : xrayEffective::realityFingerprint(fingerprintEff);
realitySettings[px::serverName] = xrayEffective::realityServerName(*this);
streamSettings[px::realitySettings] = realitySettings;
}
@@ -656,33 +699,33 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
xo[px::uplinkChunkSize] = chunkSizeEff.toInt();
}
if (!xhttp.scMaxBufferedPosts.isEmpty())
if (!xhttp.scMaxBufferedPosts.isEmpty() && xhttp.scMaxBufferedPosts.toLongLong() != 0)
xo[px::scMaxBufferedPosts] = xhttp.scMaxBufferedPosts.toLongLong();
xrayEffective::putRangeIfAny(xo, px::scMaxEachPostBytes, xhttp.scMaxEachPostBytesMin,
xhttp.scMaxEachPostBytesMax, px::defaultXhttpScMaxEachPostBytesMin,
px::defaultXhttpScMaxEachPostBytesMax);
xhttp.scMaxEachPostBytesMax, px::coreDefaultXhttpScMaxEachPostBytesMin,
px::coreDefaultXhttpScMaxEachPostBytesMax);
if (clientSide) {
xrayEffective::putRangeIfAny(xo, px::scMinPostsIntervalMs, xhttp.scMinPostsIntervalMsMin,
xhttp.scMinPostsIntervalMsMax,
px::defaultXhttpScMinPostsIntervalMsMin,
px::defaultXhttpScMinPostsIntervalMsMax);
px::coreDefaultXhttpScMinPostsIntervalMsMin,
px::coreDefaultXhttpScMinPostsIntervalMsMax);
}
xrayEffective::putRangeIfAny(xo, px::scStreamUpServerSecs, xhttp.scStreamUpServerSecsMin,
xhttp.scStreamUpServerSecsMax, px::defaultXhttpScStreamUpServerSecsMin,
px::defaultXhttpScStreamUpServerSecsMax);
xhttp.scStreamUpServerSecsMax, px::coreDefaultXhttpScStreamUpServerSecsMin,
px::coreDefaultXhttpScStreamUpServerSecsMax);
const auto &pad = xhttp.xPadding;
xo[px::xPaddingObfsMode] = pad.obfsMode;
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
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;
xo[px::xPaddingBytes] = xrayEffective::range(QString::number(fromV), QString::number(toV));
}
if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
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;
xo[px::xPaddingBytes] = xrayEffective::range(QString::number(fromV), QString::number(toV));
}
xo[px::xPaddingKey] = pad.key.isEmpty() ? QString::fromLatin1(px::defaultXPaddingKey) : pad.key;
xo[px::xPaddingHeader] = pad.header.isEmpty() ? QString::fromLatin1(px::defaultXPaddingHeader) : pad.header;
xo[px::xPaddingPlacement] = xrayEffective::xPaddingPlacement(
@@ -713,7 +756,7 @@ QJsonObject XrayServerConfig::streamSettingsJson(XrayStreamSide side) const
addMuxRange(px::xmuxCMaxReuseTimes, xmux.cMaxReuseTimesMin, xmux.cMaxReuseTimesMax);
addMuxRange(px::xmuxHMaxRequestTimes, xmux.hMaxRequestTimesMin, xmux.hMaxRequestTimesMax);
addMuxRange(px::xmuxHMaxReusableSecs, xmux.hMaxReusableSecsMin, xmux.hMaxReusableSecsMax);
if (!xmux.hKeepAlivePeriod.isEmpty())
if (!xmux.hKeepAlivePeriod.isEmpty() && !xrayEffective::xhttpUsesHttp11(*this))
mux[px::xmuxHKeepAlivePeriod] = xmux.hKeepAlivePeriod.toLongLong();
if (!mux.isEmpty())
xo[px::xmux] = mux;
@@ -834,10 +877,14 @@ namespace
if (padding.contains(px::xPaddingObfsMode))
out.obfsMode = padding.value(px::xPaddingObfsMode).toBool(true);
out.key = padding.value(px::xPaddingKey).toString();
out.header = padding.value(px::xPaddingHeader).toString();
out.placement = padding.value(px::xPaddingPlacement).toString();
out.method = padding.value(px::xPaddingMethod).toString();
if (padding.contains(px::xPaddingKey))
out.key = padding.value(px::xPaddingKey).toString();
if (padding.contains(px::xPaddingHeader))
out.header = padding.value(px::xPaddingHeader).toString();
if (padding.contains(px::xPaddingPlacement))
out.placement = padding.value(px::xPaddingPlacement).toString();
if (padding.contains(px::xPaddingMethod))
out.method = padding.value(px::xPaddingMethod).toString();
QString bytesMin;
QString bytesMax;
@@ -992,17 +1039,10 @@ namespace
srv.fingerprint = normalizedFingerprint(tls.value(px::fingerprint).toString());
}
QStringList alpnList;
for (const QJsonValue &value : tls.value(px::alpn).toArray()) {
QString entry = value.toString().trimmed();
if (entry.compare(QLatin1String("HTTP/2"), Qt::CaseInsensitive) == 0)
entry = QStringLiteral("h2");
else if (entry.compare(QLatin1String("HTTP/1.1"), Qt::CaseInsensitive) == 0)
entry = QStringLiteral("http/1.1");
if (!entry.isEmpty())
alpnList << entry;
}
srv.alpn = alpnList.join(QLatin1Char(','));
QStringList alpnEntries;
for (const QJsonValue &value : tls.value(px::alpn).toArray())
alpnEntries << value.toString();
srv.alpn = xrayEffective::alpnList(alpnEntries.join(QLatin1Char(','))).join(QLatin1Char(','));
}
if (srv.transport == QLatin1String(px::transportXhttp)) {
@@ -1143,7 +1183,7 @@ QJsonObject XrayServerConfig::toServerInboundJson(const XrayServerInboundInputs
QJsonObject realitySettings;
realitySettings[px::dest] = siteEff + QStringLiteral(":443");
realitySettings[px::privateKey] = inputs.realityPrivateKey;
realitySettings[px::serverNames] = QJsonArray { sni.isEmpty() ? siteEff : sni };
realitySettings[px::serverNames] = QJsonArray { xrayEffective::realityServerName(*this) };
realitySettings[px::shortIds] = QJsonArray { inputs.realityShortId };
streamSettings[px::realitySettings] = realitySettings;
}
@@ -1326,7 +1366,7 @@ QJsonObject XrayServerConfig::serverView() const
const QString siteEff = site.isEmpty() ? QString::fromLatin1(px::defaultSite) : site;
QJsonObject reality;
reality[px::dest] = siteEff;
reality[px::serverName] = sni.isEmpty() ? siteEff : sni;
reality[px::serverName] = xrayEffective::realityServerName(*this);
view[QStringLiteral("reality")] = reality;
}
@@ -80,6 +80,8 @@ struct XrayXhttpConfig {
QJsonObject toJson() const;
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
static XrayXhttpConfig fromJson(const QJsonObject &json);
void applyRangeDefaults();
};
// ── mKCP transport ────────────────────────────────────────────────────────────
@@ -182,14 +184,18 @@ namespace xrayEffective
QString xPaddingPlacement(const QString &placement);
QString xPaddingMethod(const QString &method);
QString range(const QString &minV, const QString &maxV);
QStringList alpnList(const QString &alpn);
void putRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax);
QString security(const XrayServerConfig &srv);
QString realityFingerprint(const QString &fingerprint);
QString realityServerName(const XrayServerConfig &srv);
QString clientFlow(const XrayServerConfig &srv);
QString serverFlow(const XrayServerConfig &srv);
QString network(const XrayServerConfig &srv);
QString xhttpModeSent(const XrayServerConfig &srv);
bool xhttpUsesHttp11(const XrayServerConfig &srv);
}
QString sanitizeNativeConfig(const QString &nativeConfig);
@@ -50,7 +50,7 @@ namespace amnezia
{
constexpr char dataDir[] = "/opt/amnezia/xray";
constexpr char serverConfigPath[] = "/opt/amnezia/xray/server.json";
constexpr char expectedServerXrayRelease[] = "v26.7.28";
constexpr char expectedServerXrayRelease[] = "v26.10.31";
constexpr char uuidPath[] = "/opt/amnezia/xray/xray_uuid.key";
constexpr char PublicKeyPath[] = "/opt/amnezia/xray/xray_public.key";
constexpr char PrivateKeyPath[] = "/opt/amnezia/xray/xray_private.key";
@@ -70,6 +70,7 @@ namespace amnezia
constexpr char defaultFlow[] = "xtls-rprx-vision";
constexpr char defaultTransport[] = "raw";
constexpr char defaultFingerprint[] = "hellochrome_120";
constexpr char defaultRealityFingerprint[] = "chrome";
constexpr char defaultSni[] = "www.googletagmanager.com";
constexpr char defaultAlpn[] = "h2";
@@ -90,6 +91,13 @@ namespace amnezia
constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1";
constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100";
constexpr char coreDefaultXhttpScMaxEachPostBytesMin[] = "1000000";
constexpr char coreDefaultXhttpScMaxEachPostBytesMax[] = "1000000";
constexpr char coreDefaultXhttpScMinPostsIntervalMsMin[] = "30";
constexpr char coreDefaultXhttpScMinPostsIntervalMsMax[] = "30";
constexpr char coreDefaultXhttpScStreamUpServerSecsMin[] = "20";
constexpr char coreDefaultXhttpScStreamUpServerSecsMax[] = "80";
constexpr char defaultXPaddingPlacement[] = "Cookie";
constexpr char defaultXPaddingMethod[] = "Repeat-x";
constexpr char defaultXPaddingKey[] = "x_padding";
+8 -1
View File
@@ -56,8 +56,15 @@ struct VlessServerObject
QString shortId;
QString fingerprint = "chrome";
QString spiderX = "";
QString alpn;
QString xhttpHost;
QString xhttpPath;
QString xhttpMode;
QString xhttpExtra;
QString tlsPin;
JSONSTRUCT_COMPARE(VlessServerObject, address, id, port, flow, encryption)
JSONSTRUCT_REGISTER(VlessServerObject, F(address, id, port, flow, encryption, network, security, serverName, publicKey, shortId, fingerprint, spiderX))
JSONSTRUCT_REGISTER(VlessServerObject, F(address, id, port, flow, encryption, network, security, serverName, publicKey, shortId, fingerprint, spiderX, alpn, xhttpHost, xhttpPath, xhttpMode),
F(xhttpExtra, tlsPin))
};
+83 -62
View File
@@ -110,31 +110,47 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
QJsonIO::SetValue(outbound, encryption, { "settings", "vnext", 0, "users", 0, "encryption" });
// type-wise settings
if (type == "http" || type == "h2" || type == "h3" || type == "quic")
{
*errMessage = QObject::tr("transport \"%1\" was removed from Xray-core, use XHTTP").arg(type);
return QJsonObject();
}
if (query.queryItemValue("security") == "xtls")
{
*errMessage = QObject::tr("security \"xtls\" was removed from Xray-core, use TLS or REALITY with xtls-rprx-vision");
return QJsonObject();
}
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 headerType = query.hasQueryItem("headerType") ? query.queryItemValue("headerType") : "none";
if (query.hasQueryItem("seed") || headerType != "none")
{
const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(","));
QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" });
*errMessage = QObject::tr("mKCP seed and header were removed from Xray-core");
return QJsonObject();
}
}
else if (type == "xhttp" || type == "splithttp")
{
if (query.hasQueryItem("host"))
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "xhttpSettings", "host" });
if (query.hasQueryItem("path"))
QJsonIO::SetValue(stream, QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()), { "xhttpSettings", "path" });
if (query.hasQueryItem("mode"))
QJsonIO::SetValue(stream, query.queryItemValue("mode"), { "xhttpSettings", "mode" });
if (query.hasQueryItem("extra"))
{
const auto extra = QJsonDocument::fromJson(QUrl::fromPercentEncoding(query.queryItemValue("extra").toUtf8()).toUtf8());
if (extra.isObject())
QJsonIO::SetValue(stream, extra.object(), { "xhttpSettings", "extra" });
}
}
else if (type == "httpupgrade")
{
if (query.hasQueryItem("host"))
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "httpupgradeSettings", "host" });
if (query.hasQueryItem("path"))
QJsonIO::SetValue(stream, QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()), { "httpupgradeSettings", "path" });
}
else if (type == "ws")
{
const auto hasPath = query.hasQueryItem("path");
@@ -145,27 +161,7 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
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" });
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "host" });
}
}
else if (type == "grpc")
@@ -188,7 +184,7 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
// 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" );
const auto tlsKey = security == "tls" ? "tlsSettings" : "realitySettings";
if (security != "none")
{
QJsonIO::SetValue(stream, security, "security");
@@ -206,35 +202,37 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
{
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")
if (security == "tls" || security == "reality")
{
const auto flow = query.queryItemValue("flow");
QJsonIO::SetValue(outbound, flow, { "settings", "vnext", 0, "users", 0, "flow" });
if (query.hasQueryItem("fp"))
{
const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8());
QJsonIO::SetValue(stream, fp, { tlsKey, "fingerprint" });
}
}
if (security == "tls" && query.hasQueryItem("pcs"))
{
QJsonIO::SetValue(stream, query.queryItemValue("pcs"), { "tlsSettings", "pinnedPeerCertSha256" });
}
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 spiderXKey = query.hasQueryItem("spx") ? "spx" : "spiderX";
if (query.hasQueryItem(spiderXKey))
{
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8());
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue(spiderXKey).toUtf8());
QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" });
}
if (query.hasQueryItem("sid"))
@@ -277,20 +275,43 @@ const QString Serialize(const VlessServerObject &server, const QString &alias)
if (!server.security.isEmpty() && server.security != "none") {
query.addQueryItem("security", server.security);
}
if (!server.flow.isEmpty() && (server.security == "xtls" || server.security == "reality")) {
if (!server.flow.isEmpty() && (server.security == "reality" || server.security == "tls")) {
query.addQueryItem("flow", server.flow);
}
if (!server.serverName.isEmpty()) {
query.addQueryItem("sni", server.serverName);
}
if (server.security == "reality") {
if (!server.fingerprint.isEmpty()) {
query.addQueryItem("fp", server.fingerprint);
if (!server.fingerprint.isEmpty() && (server.security == "reality" || server.security == "tls")) {
query.addQueryItem("fp", server.fingerprint);
}
if (!server.alpn.isEmpty() && server.security == "tls") {
query.addQueryItem("alpn", server.alpn);
}
if (server.network == "xhttp") {
if (!server.xhttpHost.isEmpty()) {
query.addQueryItem("host", server.xhttpHost);
}
if (!server.xhttpPath.isEmpty()) {
query.addQueryItem("path", server.xhttpPath);
}
if (!server.xhttpMode.isEmpty()) {
query.addQueryItem("mode", server.xhttpMode);
}
if (!server.xhttpExtra.isEmpty()) {
query.addQueryItem("extra", server.xhttpExtra);
}
}
if (!server.tlsPin.isEmpty() && server.security == "tls") {
query.addQueryItem("pcs", server.tlsPin);
}
if (server.security == "reality") {
if (!server.publicKey.isEmpty()) {
query.addQueryItem("pbk", server.publicKey);
}
@@ -300,7 +321,7 @@ const QString Serialize(const VlessServerObject &server, const QString &alias)
}
if (!server.spiderX.isEmpty()) {
query.addQueryItem("spiderX", server.spiderX);
query.addQueryItem("spx", server.spiderX);
}
}
+10
View File
@@ -197,6 +197,15 @@ if(NOT ANDROID)
test_common
)
add_executable(test_xray_runtime
testXrayRuntime.cpp
)
target_link_libraries(test_xray_runtime PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_language_model_and_controller
testUiLanguageModelAndController.cpp
)
@@ -482,6 +491,7 @@ if(NOT ANDROID)
add_test(NAME XrayGeneratorMatrixTest COMMAND test_xray_generator_matrix)
add_test(NAME XrayConfigModelTest COMMAND test_xray_config_model)
add_test(NAME XrayCoreAcceptsTest COMMAND test_xray_core_accepts)
add_test(NAME XrayRuntimeTest COMMAND test_xray_runtime)
add_test(NAME ConfigFormatVersionTest COMMAND test_config_format_version)
add_test(NAME UiLanguageModelAndControllerTest COMMAND test_ui_language_model_and_controller)
add_test(NAME UiIpModelAndControllerTest COMMAND test_ui_ip_model_and_controller)
File diff suppressed because it is too large. Load diff
+182 -2
View File
@@ -4,6 +4,7 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayContractCases.h"
#include "utils/xrayMatrix.h"
using namespace amnezia;
@@ -28,6 +29,19 @@ namespace
return withNative(srv, compact(clientJson(srv)));
}
XrayServerConfig xhttpWithAppRanges(const QString &mode)
{
XrayServerConfig srv = baseConfig("xhttp", "reality", "");
srv.xhttp.mode = mode;
srv.xhttp.applyRangeDefaults();
return srv;
}
XrayProtocolConfig stored(const XrayProtocolConfig &saved)
{
return issued(XrayServerConfig::fromJson(saved.serverConfig.toJson()));
}
bool hasError(const QStringList &errors, const QString &needle)
{
for (const QString &e : errors) {
@@ -54,21 +68,38 @@ private slots:
void serverEditTouchesServer();
void portChangeRequiresReinstall();
void switchingToXhttpFillsDefaultsButKeepsParsedMode();
void switchingToMkcpReplacesReality();
void realityShowsTheFingerprintItSends();
void unchangedSaveKeepsClientConfig();
void serverEditDropsClientConfig();
void deviceOnlyEditRebuildsClientConfig();
void deviceOnlyEditWithBrokenNativeDropsClientConfig();
void unsavedChangesFollowEdits();
void clearedServerRangeReachesServerAndStaysEmpty();
void clearedClientRangeStaysEmpty();
void startingXhttpFillsAppRangeDefaults();
void xmuxOnWithoutValuesEqualsOff();
void contractTable();
void contractAppVerdict_data();
void contractAppVerdict();
};
void TestXrayConfigModel::options()
{
QCOMPARE(XrayConfigModel::securityOptions(), QStringList({ "tls", "reality" }));
QCOMPARE(XrayConfigModel::realityFingerprintOptions(), QStringList({ "chrome", "firefox", "safari" }));
QCOMPARE(XrayConfigModel::xhttpModeOptions(),
QStringList({ "Auto", "Packet-up", "Stream-up", "Stream-one" }));
QCOMPARE(XrayConfigModel::xhttpUplinkDataPlacementOptions(),
QStringList({ "Body", "Auto", "Header", "Cookie" }));
QCOMPARE(XrayConfigModel::xPaddingBytesMinDefault(), QStringLiteral("100"));
QCOMPARE(XrayConfigModel::xPaddingBytesMaxDefault(), QStringLiteral("1000"));
QCOMPARE(XrayConfigModel::scStreamUpServerSecsMinDefault(), QStringLiteral("20"));
QCOMPARE(XrayConfigModel::scStreamUpServerSecsMaxDefault(), QStringLiteral("80"));
QCOMPARE(XrayConfigModel::scMinPostsIntervalMsMinDefault(), QStringLiteral("30"));
QCOMPARE(XrayConfigModel::scMinPostsIntervalMsMaxDefault(), QStringLiteral("30"));
QCOMPARE(XrayConfigModel::scMaxEachPostBytesMinDefault(), QStringLiteral("1000000"));
QCOMPARE(XrayConfigModel::scMaxEachPostBytesMaxDefault(), QStringLiteral("1000000"));
}
void TestXrayConfigModel::isValidSni_data()
@@ -104,7 +135,9 @@ void TestXrayConfigModel::validationErrors_data()
QTest::newRow("port 70000") << "raw" << "reality" << "port=70000" << "Port must be";
QTest::newRow("tls sni *") << "raw" << "tls" << "sni=*" << "SNI";
QTest::newRow("reality wildcard sni") << "xhttp" << "reality" << "sni=*.asd.com" << "SNI";
QTest::newRow("none ignores sni") << "raw" << "none" << "sni=*" << "";
QTest::newRow("none is rejected") << "raw" << "none" << "" << "None is not supported";
QTest::newRow("mkcp + reality is rejected") << "mkcp" << "reality" << "" << "mKCP does not work with REALITY";
QTest::newRow("native none is accepted") << "raw" << "none" << "native" << "";
QTest::newRow("packet-up + reality") << "xhttp" << "reality" << "mode=Packet-up" << "";
QTest::newRow("packet-up + tls") << "xhttp" << "tls" << "mode=Packet-up" << "";
QTest::newRow("auto + reality") << "xhttp" << "reality" << "mode=Auto" << "";
@@ -121,8 +154,10 @@ void TestXrayConfigModel::validationErrors()
QFETCH(QString, edit);
QFETCH(QString, expected);
XrayServerConfig srv = baseConfig(transport, security, "");
srv.isThirdPartyConfig = edit == QLatin1String("native");
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(baseConfig(transport, security, "")));
model.updateModel(DockerContainer::Xray, issued(srv));
const QString value = edit.section(QLatin1Char('='), 1);
if (edit.startsWith("port="))
@@ -252,6 +287,37 @@ void TestXrayConfigModel::switchingToXhttpFillsDefaultsButKeepsParsedMode()
QCOMPARE(parsed.xhttpMode(), QStringLiteral("Packet-up"));
}
void TestXrayConfigModel::switchingToMkcpReplacesReality()
{
XrayConfigModel reality;
reality.updateModel(DockerContainer::Xray, issued(baseConfig("raw", "reality", "")));
reality.setTransport("mkcp");
QCOMPARE(reality.security(), QStringLiteral("tls"));
QVERIFY2(reality.validationErrors().isEmpty(), qPrintable(reality.validationErrors().join("; ")));
XrayConfigModel tls;
tls.updateModel(DockerContainer::Xray, issued(baseConfig("raw", "tls", "")));
tls.setTransport("mkcp");
QCOMPARE(tls.security(), QStringLiteral("tls"));
}
void TestXrayConfigModel::realityShowsTheFingerprintItSends()
{
XrayServerConfig selfHosted = baseConfig("raw", "reality", "");
selfHosted.fingerprint = QStringLiteral("hellochrome_120");
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(selfHosted));
QCOMPARE(model.realityFingerprint(), QStringLiteral("chrome"));
model.setFingerprint(QStringLiteral("firefox"));
QCOMPARE(model.realityFingerprint(), QStringLiteral("firefox"));
XrayServerConfig native = selfHosted;
native.isThirdPartyConfig = true;
XrayConfigModel nativeModel;
nativeModel.updateModel(DockerContainer::Xray, issued(native));
QCOMPARE(nativeModel.realityFingerprint(), QStringLiteral("hellochrome_120"));
}
void TestXrayConfigModel::unchangedSaveKeepsClientConfig()
{
XrayConfigModel model;
@@ -313,6 +379,120 @@ void TestXrayConfigModel::unsavedChangesFollowEdits()
QVERIFY(!model.hasUnsavedChanges());
}
void TestXrayConfigModel::clearedServerRangeReachesServerAndStaysEmpty()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(xhttpWithAppRanges("Stream-up")));
QCOMPARE(model.xhttpScStreamUpServerSecsMin(), QStringLiteral("1"));
QCOMPARE(model.xhttpScStreamUpServerSecsMax(), QStringLiteral("100"));
model.setXhttpScStreamUpServerSecsMin(QString());
model.setXhttpScStreamUpServerSecsMax(QString());
QVERIFY(model.validationErrors().isEmpty());
QVERIFY(model.pendingChangeTouchesServer(QString()));
const XrayProtocolConfig saved = model.getProtocolConfig();
const QString server = compact(serverJson(saved.serverConfig));
QVERIFY2(!server.contains(QStringLiteral("scStreamUpServerSecs")), qPrintable(server));
XrayConfigModel reopened;
reopened.updateModel(DockerContainer::Xray, stored(saved));
QVERIFY(reopened.xhttpScStreamUpServerSecsMin().isEmpty());
QVERIFY(reopened.xhttpScStreamUpServerSecsMax().isEmpty());
QVERIFY(!reopened.hasUnsavedChanges());
}
void TestXrayConfigModel::clearedClientRangeStaysEmpty()
{
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(xhttpWithAppRanges("Packet-up")));
QCOMPARE(model.xhttpScMinPostsIntervalMsMin(), QStringLiteral("100"));
model.setXhttpScMinPostsIntervalMsMin(QString());
model.setXhttpScMinPostsIntervalMsMax(QString());
QVERIFY(model.validationErrors().isEmpty());
QVERIFY(!model.pendingChangeTouchesServer(QString()));
const XrayProtocolConfig saved = model.getProtocolConfig();
const QString client = compact(clientJson(saved.serverConfig));
QVERIFY2(!client.contains(QStringLiteral("scMinPostsIntervalMs")), qPrintable(client));
XrayConfigModel reopened;
reopened.updateModel(DockerContainer::Xray, stored(saved));
QVERIFY(reopened.xhttpScMinPostsIntervalMsMin().isEmpty());
QVERIFY(reopened.xhttpScMinPostsIntervalMsMax().isEmpty());
}
void TestXrayConfigModel::startingXhttpFillsAppRangeDefaults()
{
XrayServerConfig raw = baseConfig("raw", "reality", "");
raw.xhttp = XrayXhttpConfig {};
XrayConfigModel model;
model.updateModel(DockerContainer::Xray, issued(raw));
model.setTransport("xhttp");
QCOMPARE(model.xhttpScStreamUpServerSecsMin(), QStringLiteral("1"));
QCOMPARE(model.xhttpScStreamUpServerSecsMax(), QStringLiteral("100"));
QCOMPARE(model.xhttpScMinPostsIntervalMsMin(), QStringLiteral("100"));
QCOMPARE(model.xhttpScMinPostsIntervalMsMax(), QStringLiteral("800"));
QCOMPARE(model.xhttpScMaxEachPostBytesMin(), QStringLiteral("1000000"));
QCOMPARE(model.xhttpScMaxEachPostBytesMax(), QStringLiteral("1000000"));
model.setXhttpScStreamUpServerSecsMin(QString());
model.setXhttpScStreamUpServerSecsMax(QString());
model.resetToDefaults();
QCOMPARE(model.xhttpScStreamUpServerSecsMin(), QStringLiteral("1"));
QCOMPARE(model.xhttpScStreamUpServerSecsMax(), QStringLiteral("100"));
}
void TestXrayConfigModel::xmuxOnWithoutValuesEqualsOff()
{
XrayServerConfig on = baseConfig("xhttp", "reality", "");
on.xhttp.xmux = XrayXmuxConfig::fromJson(QJsonObject {});
on.xhttp.xmux.enabled = true;
XrayServerConfig off = on;
off.xhttp.xmux.enabled = false;
const QString onClient = compact(clientJson(on));
QVERIFY2(!onClient.contains(QStringLiteral("\"xmux\"")), qPrintable(onClient));
QCOMPARE(onClient, compact(clientJson(off)));
}
void TestXrayConfigModel::contractTable()
{
QStringList names;
for (const ContractCase &c : contractCases()) {
QVERIFY2(!names.contains(c.name), qPrintable(QStringLiteral("duplicate row ") + c.name));
names << c.name;
if (c.app == AppVerdict::Blocked && c.core == CoreVerdict::Accept)
QVERIFY2(!c.stricterWhy.isEmpty(), qPrintable(c.name + QStringLiteral(": app is stricter than the core, say why")));
if (c.app == AppVerdict::Ok)
QVERIFY2(c.core != CoreVerdict::Reject,
qPrintable(c.name + QStringLiteral(": a row may not expect the app to pass what the core rejects")));
}
qInfo().noquote() << "contract rows:" << names.size();
}
void TestXrayConfigModel::contractAppVerdict_data()
{
QTest::addColumn<int>("index");
for (int i = 0; i < contractCases().size(); ++i)
QTest::newRow(qPrintable(contractCases().at(i).name)) << i;
}
void TestXrayConfigModel::contractAppVerdict()
{
QFETCH(int, index);
const ContractCase &c = contractCases().at(index);
const QStringList errors = contractErrors(c);
const AppVerdict actual = errors.isEmpty() ? AppVerdict::Ok : AppVerdict::Blocked;
if (!c.knownIssue.isEmpty())
QEXPECT_FAIL("", qPrintable(c.knownIssue), Abort);
QVERIFY2(actual == c.app, qPrintable(QStringLiteral("app %1, expected %2; errors: %3")
.arg(verdictName(actual), verdictName(c.app), errors.join(QStringLiteral("; ")))));
}
QTEST_GUILESS_MAIN(TestXrayConfigModel)
#include "testXrayConfigModel.moc"
+323 -8
View File
@@ -7,8 +7,11 @@
#include <QTemporaryDir>
#include <QTest>
#include <functional>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayContractCases.h"
#include "utils/xrayMatrix.h"
using namespace amnezia;
@@ -16,6 +19,213 @@ using namespace xraytest;
namespace
{
using JsonEdit = std::function<void(QJsonObject &)>;
struct Probe
{
QString name;
QString ruleId;
bool server = false;
QString transport;
QString security;
QString flow;
QString mode;
JsonEdit edit;
QString rejectWith;
};
JsonEdit onStream(const QString &key, const JsonEdit &fn)
{
return [key, fn](QJsonObject &bound) {
QJsonObject stream = bound.value("streamSettings").toObject();
if (key.isEmpty()) {
fn(stream);
} else {
QJsonObject sub = stream.value(key).toObject();
fn(sub);
stream[key] = sub;
}
bound["streamSettings"] = stream;
};
}
JsonEdit setIn(const QString &key, const QString &field, const QJsonValue &value)
{
return onStream(key, [field, value](QJsonObject &obj) { obj[field] = value; });
}
JsonEdit removeIn(const QString &key, const QString &field)
{
return onStream(key, [field](QJsonObject &obj) { obj.remove(field); });
}
JsonEdit onSettings(const JsonEdit &fn)
{
return [fn](QJsonObject &bound) {
QJsonObject settings = bound.value("settings").toObject();
fn(settings);
bound["settings"] = settings;
};
}
JsonEdit onUser(const JsonEdit &fn)
{
return onSettings([fn](QJsonObject &settings) {
if (settings.contains("vnext")) {
QJsonArray vnext = settings.value("vnext").toArray();
QJsonObject entry = vnext.first().toObject();
QJsonArray users = entry.value("users").toArray();
QJsonObject user = users.first().toObject();
fn(user);
users[0] = user;
entry["users"] = users;
vnext[0] = entry;
settings["vnext"] = vnext;
} else {
QJsonArray clients = settings.value("clients").toArray();
QJsonObject user = clients.first().toObject();
fn(user);
clients[0] = user;
settings["clients"] = clients;
}
});
}
void editVless(QJsonObject &config, const QString &listKey, const JsonEdit &edit)
{
QJsonArray list = config.value(listKey).toArray();
for (int i = 0; i < list.size(); ++i) {
QJsonObject bound = list.at(i).toObject();
if (bound.value("protocol").toString() != QLatin1String("vless"))
continue;
edit(bound);
list[i] = bound;
}
config[listKey] = list;
}
QList<Probe> probes()
{
const QString vision = QStringLiteral("xtls-rprx-vision");
const QString one = QStringLiteral("Stream-one");
const QString packet = QStringLiteral("Packet-up");
const QString rs = QStringLiteral("realitySettings");
const QString tls = QStringLiteral("tlsSettings");
const QString xs = QStringLiteral("xhttpSettings");
const QString kcp = QStringLiteral("kcpSettings");
const QString longShortId = QStringLiteral("0123456789abcdef0");
return {
{ "reality on kcp", "reality-network", false, "raw", "reality", "", "", setIn({}, "network", "kcp"),
"REALITY only supports RAW, XHTTP and gRPC" },
{ "reality without realitySettings", "reality-settings-required", false, "raw", "reality", "", "",
removeIn({}, rs), "Empty \"realitySettings\"" },
{ "server flow vision-udp443", "server-flow-no-udp443", true, "raw", "reality", vision, "",
onUser([](QJsonObject &u) { u["flow"] = "xtls-rprx-vision-udp443"; }), "doesn't support" },
{ "client without encryption", "encryption-required-client", false, "raw", "reality", vision, "",
onUser([](QJsonObject &u) { u.remove("encryption"); }), "\"encryption\":\"none\"" },
{ "server without decryption", "decryption-required-server", true, "raw", "reality", vision, "",
onSettings([](QJsonObject &s) { s.remove("decryption"); }), "\"decryption\":\"none\"" },
{ "server client with encryption", "encryption-not-in-inbound", true, "raw", "reality", vision, "",
onUser([](QJsonObject &u) { u["encryption"] = "none"; }), "should not be in inbound" },
{ "tls fingerprint foo", "tls-fingerprint-known", false, "raw", "tls", "", "", setIn(tls, "fingerprint", "foo"),
"unknown \"fingerprint\"" },
{ "tls allowInsecure", "tls-allow-insecure-removed", false, "raw", "tls", "", "", setIn(tls, "allowInsecure", true),
"allowInsecure" },
{ "tls pin 4 hex", "tls-pin-length", false, "raw", "tls", "", "", setIn(tls, "pinnedPeerCertSha256", "abcd"),
"incorrect pinnedPeerCertSha256 length" },
{ "tls pin 64 hex", "tls-pin-length", false, "raw", "tls", "", "",
setIn(tls, "pinnedPeerCertSha256", QString(64, QLatin1Char('b'))), "" },
{ "reality fingerprint unsafe", "reality-fingerprint-unsafe", false, "raw", "reality", "", "",
setIn(rs, "fingerprint", "unsafe"), "invalid \"fingerprint\"" },
{ "reality without publicKey", "reality-client-password", false, "raw", "reality", "", "", removeIn(rs, "publicKey"),
"empty \"password\"" },
{ "reality publicKey abc", "reality-client-password", false, "raw", "reality", "", "", setIn(rs, "publicKey", "abc"),
"invalid \"password\"" },
{ "reality client serverNames", "reality-client-no-servernames", false, "raw", "reality", "", "",
setIn(rs, "serverNames", QJsonArray { "www.example.com" }), "non-empty \"serverNames\"" },
{ "reality shortId 17 hex", "reality-shortid", false, "raw", "reality", "", "", setIn(rs, "shortId", longShortId),
"too long \"shortId\"" },
{ "reality shortId zz", "reality-shortid", false, "raw", "reality", "", "", setIn(rs, "shortId", "zz"),
"invalid \"shortId\"" },
{ "reality shortIds 17 hex", "reality-shortid", true, "raw", "reality", "", "",
setIn(rs, "shortIds", QJsonArray { longShortId }), "too long \"shortIds[" },
{ "reality shortIds zz", "reality-shortid", true, "raw", "reality", "", "", setIn(rs, "shortIds", QJsonArray { "zz" }),
"invalid \"shortIds[" },
{ "reality spiderX abc", "reality-spiderx", false, "raw", "reality", "", "", setIn(rs, "spiderX", "abc"),
"invalid \"spiderX\"" },
{ "reality spiderX /abc", "reality-spiderx", false, "raw", "reality", "", "", setIn(rs, "spiderX", "/abc"), "" },
{ "reality without privateKey", "reality-server-required", true, "raw", "reality", "", "", removeIn(rs, "privateKey"),
"empty \"privateKey\"" },
{ "reality empty serverNames", "reality-server-required", true, "raw", "reality", "", "",
setIn(rs, "serverNames", QJsonArray()), "empty \"serverNames\"" },
{ "reality empty shortIds", "reality-server-required", true, "raw", "reality", "", "", setIn(rs, "shortIds", QJsonArray()),
"empty \"shortIds\"" },
{ "reality empty target", "reality-server-required", true, "raw", "reality", "", "", setIn(rs, "dest", ""),
"valid value for \"target\"" },
{ "xhttp mode foo", "xhttp-mode-enum", false, "xhttp", "tls", "", one, setIn(xs, "mode", "foo"), "unsupported mode" },
{ "server xhttp mode foo", "xhttp-mode-enum", true, "xhttp", "tls", "", one, setIn(xs, "mode", "foo"),
"unsupported mode" },
{ "xhttp headers Host", "xhttp-headers-host", false, "xhttp", "tls", "", one,
setIn(xs, "headers", QJsonObject { { "Host", "a.example.com" } }), "can't contain \"host\"" },
{ "cookie data in stream-one", "xhttp-uplink-data-placement-packet-up", false, "xhttp", "tls", "", one,
setIn(xs, "uplinkDataPlacement", "cookie"), "only in packet-up mode" },
{ "cookie data in packet-up", "xhttp-uplink-data-placement-packet-up", false, "xhttp", "tls", "", packet,
setIn(xs, "uplinkDataPlacement", "cookie"), "" },
{ "GET in stream-one", "xhttp-get-packet-up", false, "xhttp", "tls", "", one, setIn(xs, "uplinkHTTPMethod", "GET"),
"GET only in packet-up" },
{ "GET in packet-up", "xhttp-get-packet-up", false, "xhttp", "tls", "", packet, setIn(xs, "uplinkHTTPMethod", "GET"),
"" },
{ "downloadSettings in stream-one", "xhttp-download-settings-stream-one", false, "xhttp", "tls", "", one,
setIn(xs, "downloadSettings", QJsonObject()), "\"downloadSettings\" in \"stream-one\"" },
{ "xPaddingBytes 0-100", "xhttp-padding-bytes", false, "xhttp", "tls", "", one, setIn(xs, "xPaddingBytes", "0-100"),
"cannot be disabled" },
{ "xPaddingBytes -1-5", "xhttp-padding-bytes", false, "xhttp", "tls", "", one, setIn(xs, "xPaddingBytes", "-1-5"),
"cannot be disabled" },
{ "xPaddingBytes 0-0", "xhttp-padding-bytes", false, "xhttp", "tls", "", one, setIn(xs, "xPaddingBytes", "0-0"), "" },
{ "xPaddingBytes 1-1", "xhttp-padding-bytes", false, "xhttp", "tls", "", one, setIn(xs, "xPaddingBytes", "1-1"), "" },
{ "xPaddingPlacement foo", "xhttp-padding-placement", false, "xhttp", "tls", "", one,
setIn(xs, "xPaddingPlacement", "foo"), "unsupported padding placement" },
{ "xPaddingMethod foo", "xhttp-padding-method", false, "xhttp", "tls", "", one, setIn(xs, "xPaddingMethod", "foo"),
"unsupported padding method" },
{ "sessionIDPlacement foo", "xhttp-placement-enum", false, "xhttp", "tls", "", packet,
setIn(xs, "sessionIDPlacement", "foo"), "unsupported session placement" },
{ "seqPlacement foo", "xhttp-placement-enum", false, "xhttp", "tls", "", packet, setIn(xs, "seqPlacement", "foo"),
"unsupported seq placement" },
{ "sessionIDTable ab length 1", "xhttp-session-table", false, "xhttp", "tls", "", packet,
onStream(xs, [](QJsonObject &x) {
x["sessionIDTable"] = "ab";
x["sessionIDLength"] = 1;
}),
"too small" },
{ "xmux concurrency and connections", "xmux-exclusive", false, "xhttp", "tls", "", one,
setIn(xs, "xmux", QJsonObject { { "maxConcurrency", 1 }, { "maxConnections", 1 } }),
"maxConnections cannot be specified together" },
{ "kcp header", "kcp-header-removed", false, "mkcp", "tls", "", "", setIn(kcp, "header", QJsonObject { { "type", "none" } }),
"" },
{ "kcp mtu 20", "kcp-mtu", false, "mkcp", "tls", "", "", setIn(kcp, "mtu", 20), "MTU must be at least 21" },
{ "kcp mtu 21", "kcp-mtu", false, "mkcp", "tls", "", "", setIn(kcp, "mtu", 21), "" },
{ "kcp tti 9", "kcp-tti", false, "mkcp", "tls", "", "", setIn(kcp, "tti", 9), "TTI must be between 10 and 1000" },
{ "kcp tti 1000", "kcp-tti", false, "mkcp", "tls", "", "", setIn(kcp, "tti", 1000), "" },
{ "kcp tti 1001", "kcp-tti", false, "mkcp", "tls", "", "", setIn(kcp, "tti", 1001), "TTI must be between 10 and 1000" },
{ "kcp cwndMultiplier 0", "kcp-cwnd", false, "mkcp", "tls", "", "", setIn(kcp, "cwndMultiplier", 0),
"CwndMultiplier must be at least 1" },
{ "kcp maxSendingWindow 100", "kcp-sending-window", false, "mkcp", "tls", "", "", setIn(kcp, "maxSendingWindow", 100),
"MaxSendingWindow must be at least" },
{ "kcp uplinkCapacity -1", "kcp-capacity-overflow", false, "mkcp", "tls", "", "", setIn(kcp, "uplinkCapacity", -1),
"uint32" },
{ "range abc-5", "", false, "xhttp", "tls", "", packet, setIn(xs, "scMaxEachPostBytes", "abc-5"),
"Invalid integer range" },
{ "range -1-5", "", false, "xhttp", "tls", "", packet, setIn(xs, "scMaxEachPostBytes", "-1-5"), "" },
{ "security foo", "", false, "raw", "tls", "", "", setIn({}, "security", "foo"), "Unknown security" },
};
}
QString lastLine(const QString &output)
{
const QStringList lines = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
return lines.isEmpty() ? QString() : lines.last().trimmed().left(240);
}
struct CoreCase
{
QString name;
@@ -27,7 +237,10 @@ namespace
{
QList<CoreCase> out;
for (const QString &security : { QStringLiteral("tls"), QStringLiteral("reality") }) {
for (const QString &fp : XrayConfigModel::fingerprintOptions()) {
const QStringList fingerprints = security == QLatin1String("reality")
? XrayConfigModel::realityFingerprintOptions()
: XrayConfigModel::fingerprintOptions();
for (const QString &fp : fingerprints) {
XrayServerConfig srv = baseConfig("raw", security, "xtls-rprx-vision");
srv.fingerprint = fp;
out.append({ QStringLiteral("option/raw/%1/fingerprint=%2").arg(security, fp), srv });
@@ -39,13 +252,13 @@ namespace
out.append({ QStringLiteral("option/xhttp/tls/alpn=%1").arg(alpn), srv });
}
for (const QString &method : XrayConfigModel::xhttpUplinkMethodOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "none", "");
XrayServerConfig srv = baseConfig("xhttp", "tls", "");
srv.xhttp.mode = QStringLiteral("Packet-up");
srv.xhttp.uplinkMethod = method;
out.append({ QStringLiteral("option/xhttp/packet-up/uplinkMethod=%1").arg(method), srv });
}
for (const QString &pl : XrayConfigModel::xhttpSessionPlacementOptions()) {
XrayServerConfig srv = baseConfig("xhttp", "none", "");
XrayServerConfig srv = baseConfig("xhttp", "tls", "");
srv.xhttp.mode = QStringLiteral("Packet-up");
srv.xhttp.sessionPlacement = pl;
srv.xhttp.seqPlacement = pl;
@@ -106,6 +319,10 @@ private slots:
void generatedConfigs();
void coreRejectsWhatWeRelyOn_data();
void coreRejectsWhatWeRelyOn();
void contract_data();
void contract();
void coreBoundaries_data();
void coreBoundaries();
private:
struct Run
@@ -118,6 +335,7 @@ private:
void useLocalCertificate(QJsonObject &serverConfig) const;
QString m_xray;
bool m_coreBypassesNoTlsRule = false;
QTemporaryDir m_dir;
QString m_certFile;
QString m_keyFile;
@@ -127,7 +345,7 @@ void TestXrayCoreAccepts::initTestCase()
{
m_xray = qEnvironmentVariable("XRAY_BIN");
if (m_xray.isEmpty() || !QFileInfo(m_xray).isExecutable())
QSKIP("Set XRAY_BIN to an xray v26.7.28 binary to run the core checks");
QSKIP("Set XRAY_BIN to an xray v26.10.31 or v26.7.28 binary to run the core checks");
QVERIFY(m_dir.isValid());
QProcess version;
@@ -135,8 +353,9 @@ void TestXrayCoreAccepts::initTestCase()
QVERIFY(version.waitForFinished(30000));
const QString firstLine = QString::fromUtf8(version.readAllStandardOutput()).section(QLatin1Char('\n'), 0, 0);
qInfo().noquote() << "xray under test:" << firstLine;
if (!firstLine.contains(QLatin1String("26.7.28")))
qWarning().noquote() << "XRAY_BIN is not v26.7.28, results may differ from the VPS";
m_coreBypassesNoTlsRule = firstLine.contains(QLatin1String("26.7.28"));
if (!m_coreBypassesNoTlsRule && !firstLine.contains(QLatin1String("26.10.31")))
qWarning().noquote() << "XRAY_BIN is neither v26.10.31 nor v26.7.28, results may differ from the VPS";
const QString prefix = m_dir.filePath(QStringLiteral("tls"));
QProcess cert;
@@ -211,8 +430,11 @@ void TestXrayCoreAccepts::generatedConfigs()
qPrintable(clientRun.output));
return;
}
if (!c.allowedByUi)
QSKIP("Blocked by validationErrors()");
if (!c.allowedByUi) {
QVERIFY2(serverRun.exitCode != 0 || clientRun.exitCode != 0,
qPrintable(QStringLiteral("validationErrors() blocks a config the core accepts\n") + compact(client)));
return;
}
QVERIFY2(serverRun.exitCode == 0, qPrintable(QStringLiteral("server.json rejected: ") + serverRun.output
+ QStringLiteral("\n") + compact(server)));
@@ -228,6 +450,7 @@ void TestXrayCoreAccepts::coreRejectsWhatWeRelyOn_data()
QTest::newRow("cookie uplinkDataPlacement outside packet-up") << "cookie" << "only in packet-up";
QTest::newRow("GET outside packet-up") << "get" << "GET only in packet-up";
QTest::newRow("allowInsecure") << "insecure" << "allowInsecure";
QTest::newRow("vless without TLS to a public address") << "none" << "vless without TLS";
}
void TestXrayCoreAccepts::coreRejectsWhatWeRelyOn()
@@ -241,6 +464,10 @@ void TestXrayCoreAccepts::coreRejectsWhatWeRelyOn()
XrayServerInboundInputs in = inboundInputs(srv);
in.clients = QJsonArray { XrayServerConfig::makeClientEntry(clientId(), "xtls-rprx-vision-udp443") };
config = srv.toServerInboundJson(in);
} else if (what == QLatin1String("none")) {
if (m_coreBypassesNoTlsRule)
QSKIP("xray v26.7.28 lets VLESS outbounds in the vnext style skip this check");
config = clientJson(baseConfig("raw", "none", ""));
} else {
XrayServerConfig srv = baseConfig("xhttp", "tls", "");
srv.xhttp.mode = QStringLiteral("Stream-one");
@@ -274,6 +501,94 @@ void TestXrayCoreAccepts::coreRejectsWhatWeRelyOn()
QVERIFY2(run.output.contains(expected), qPrintable(run.output));
}
void TestXrayCoreAccepts::contract_data()
{
QTest::addColumn<int>("index");
for (int i = 0; i < contractCases().size(); ++i)
QTest::newRow(qPrintable(contractCases().at(i).name)) << i;
}
void TestXrayCoreAccepts::contract()
{
QFETCH(int, index);
const ContractCase &c = contractCases().at(index);
XrayServerConfig srv;
const QStringList errors = contractErrors(c, &srv);
const AppVerdict app = errors.isEmpty() ? AppVerdict::Ok : AppVerdict::Blocked;
QJsonObject server = serverJson(srv);
useLocalCertificate(server);
const QJsonObject client = clientJson(srv);
const QString base = QStringLiteral("contract_%1").arg(index);
const Run serverRun = xrayTest(server, base + QStringLiteral("_server"));
const Run clientRun = xrayTest(client, base + QStringLiteral("_client"));
const CoreVerdict core = serverRun.exitCode == 0 && clientRun.exitCode == 0 ? CoreVerdict::Accept : CoreVerdict::Reject;
QString coreOutput;
if (serverRun.exitCode != 0)
coreOutput += QStringLiteral("server: ") + serverRun.output + QLatin1Char('\n');
if (clientRun.exitCode != 0)
coreOutput += QStringLiteral("client: ") + clientRun.output + QLatin1Char('\n');
qInfo().noquote() << QStringLiteral("contract app=%1 core=%2 rule=%3 input=%4 | %5%6")
.arg(verdictName(app), verdictName(core), c.ruleId.isEmpty() ? QStringLiteral("-") : c.ruleId,
c.viaUi ? QStringLiteral("ui") : QStringLiteral("import"), c.name,
core == CoreVerdict::Reject ? QStringLiteral(" | ") + lastLine(coreOutput) : QString());
const QString configs = QStringLiteral("\nclient: ") + compact(client) + QStringLiteral("\nserver: ") + compact(server);
if (c.core == CoreVerdict::Explore) {
if (c.knownIssue.isEmpty())
QVERIFY2(!(app == AppVerdict::Ok && core == CoreVerdict::Reject),
qPrintable(QStringLiteral("the app passes a config the core rejects\n") + coreOutput + configs));
QSKIP(qPrintable(QStringLiteral("EXPLORE app=%1 core=%2").arg(verdictName(app), verdictName(core))));
}
QVERIFY2(core == c.core, qPrintable(QStringLiteral("core %1, expected %2\n%3%4")
.arg(verdictName(core), verdictName(c.core), coreOutput, configs)));
if (c.core == CoreVerdict::Reject && !c.coreMessage.isEmpty())
QVERIFY2(coreOutput.contains(c.coreMessage), qPrintable(coreOutput));
if (!c.knownIssue.isEmpty())
QEXPECT_FAIL("", qPrintable(c.knownIssue), Abort);
QVERIFY2(app == c.app, qPrintable(QStringLiteral("app %1, expected %2; errors: %3")
.arg(verdictName(app), verdictName(c.app), errors.join(QStringLiteral("; ")))));
}
void TestXrayCoreAccepts::coreBoundaries_data()
{
QTest::addColumn<int>("index");
const QList<Probe> all = probes();
for (int i = 0; i < all.size(); ++i)
QTest::newRow(qPrintable((all.at(i).server ? QStringLiteral("server/") : QStringLiteral("client/")) + all.at(i).name)) << i;
}
void TestXrayCoreAccepts::coreBoundaries()
{
QFETCH(int, index);
const Probe p = probes().at(index);
XrayServerConfig srv = baseConfig(p.transport, p.security, p.flow);
if (!p.mode.isEmpty())
srv.xhttp.mode = p.mode;
QJsonObject config = p.server ? serverJson(srv) : clientJson(srv);
if (p.server)
useLocalCertificate(config);
editVless(config, p.server ? QStringLiteral("inbounds") : QStringLiteral("outbounds"), p.edit);
const Run run = xrayTest(config, QStringLiteral("probe_%1").arg(index));
qInfo().noquote() << QStringLiteral("probe core=%1 rule=%2 | %3%4")
.arg(run.exitCode == 0 ? QStringLiteral("accept") : QStringLiteral("reject"),
p.ruleId.isEmpty() ? QStringLiteral("-") : p.ruleId, p.name,
run.exitCode == 0 ? QString() : QStringLiteral(" | ") + lastLine(run.output));
if (p.rejectWith.isEmpty()) {
QVERIFY2(run.exitCode == 0, qPrintable(run.output + QStringLiteral("\n") + compact(config)));
return;
}
QVERIFY2(run.exitCode != 0, qPrintable(QStringLiteral("xray accepted it: ") + compact(config)));
QVERIFY2(run.output.contains(p.rejectWith), qPrintable(run.output));
}
QTEST_GUILESS_MAIN(TestXrayCoreAccepts)
#include "testXrayCoreAccepts.moc"
+38 -2
View File
@@ -48,6 +48,8 @@ private slots:
void deviceOnlyChangesKeepServerView();
void serverChangesChangeServerView_data();
void serverChangesChangeServerView();
void realityFingerprintCarriesMlkem_data();
void realityFingerprintCarriesMlkem();
};
void TestXrayGeneratorMatrix::invariants_data()
@@ -113,7 +115,7 @@ void TestXrayGeneratorMatrix::invariants()
QCOMPARE(str(rOut, "serverName"), srv.sni);
QCOMPARE(str(rOut, "publicKey"), key32('\x02'));
QCOMPARE(str(rOut, "shortId"), shortId());
QCOMPARE(str(rOut, "fingerprint"), srv.fingerprint);
QCOMPARE(str(rOut, "fingerprint"), xrayEffective::realityFingerprint(srv.fingerprint));
QVERIFY(!sIn.contains("tlsSettings") && !sOut.contains("tlsSettings"));
} else if (security == QLatin1String("tls")) {
const QJsonObject tIn = sIn.value("tlsSettings").toObject();
@@ -152,9 +154,12 @@ void TestXrayGeneratorMatrix::invariants()
QVERIFY(x.contains("xPaddingObfsMode"));
QCOMPARE(x.value("xPaddingObfsMode").toBool(), srv.xhttp.xPadding.obfsMode);
if (!srv.xhttp.xPadding.obfsMode) {
for (const char *key : { "xPaddingBytes", "xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod" })
for (const char *key : { "xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod" })
QVERIFY2(!x.contains(QString::fromLatin1(key)), qPrintable(compact(x)));
}
if (!srv.xhttp.xPadding.bytesMin.isEmpty() && !srv.xhttp.xPadding.bytesMax.isEmpty())
QCOMPARE(str(x, "xPaddingBytes"),
xrayEffective::range(srv.xhttp.xPadding.bytesMin, srv.xhttp.xPadding.bytesMax));
}
const XrayXmuxConfig &xmux = srv.xhttp.xmux;
@@ -244,6 +249,37 @@ void TestXrayGeneratorMatrix::serverChangesChangeServerView()
QVERIFY2(!before.hasEqualServerSettings(after), qPrintable(field + " must count as a server change"));
}
void TestXrayGeneratorMatrix::realityFingerprintCarriesMlkem_data()
{
QTest::addColumn<QString>("security");
QTest::addColumn<QString>("fingerprint");
QTest::addColumn<bool>("thirdParty");
QTest::addColumn<QString>("expected");
QTest::newRow("reality hellochrome_120") << "reality" << "hellochrome_120" << false << "chrome";
QTest::newRow("reality ios") << "reality" << "ios" << false << "chrome";
QTest::newRow("reality random") << "reality" << "random" << false << "chrome";
QTest::newRow("reality empty") << "reality" << "" << false << "chrome";
QTest::newRow("reality firefox") << "reality" << "firefox" << false << "firefox";
QTest::newRow("reality safari") << "reality" << "safari" << false << "safari";
QTest::newRow("native reality hellochrome_120") << "reality" << "hellochrome_120" << true << "hellochrome_120";
QTest::newRow("tls hellochrome_120") << "tls" << "hellochrome_120" << false << "hellochrome_120";
}
void TestXrayGeneratorMatrix::realityFingerprintCarriesMlkem()
{
QFETCH(QString, security);
QFETCH(QString, fingerprint);
QFETCH(bool, thirdParty);
QFETCH(QString, expected);
XrayServerConfig srv = baseConfig("raw", security, "");
srv.fingerprint = fingerprint;
srv.isThirdPartyConfig = thirdParty;
const QJsonObject stream = streamOf(vlessOutbound(clientJson(srv)));
const char *settings = security == QLatin1String("reality") ? "realitySettings" : "tlsSettings";
QCOMPARE(str(stream.value(QString::fromLatin1(settings)).toObject(), "fingerprint"), expected);
}
QTEST_APPLESS_MAIN(TestXrayGeneratorMatrix)
#include "testXrayGeneratorMatrix.moc"
+566
View File
@@ -0,0 +1,566 @@
#include <QCryptographicHash>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QSslCertificate>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTemporaryDir>
#include <QTest>
#include <QUuid>
#include <cstdio>
#include <functional>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayMatrix.h"
#include "utils/xrayRuntime.h"
using namespace amnezia;
using namespace xraytest;
namespace
{
using JsonEdit = std::function<void(QJsonObject &server, QJsonObject &client)>;
struct RuntimeCase
{
QString name;
XrayServerConfig srv;
JsonEdit edit;
bool traffic = true;
QString sinceServer;
};
const QString kSite = QStringLiteral("www.googletagmanager.com");
const QHostAddress kTarget(QStringLiteral("203.0.113.80"));
void setClientUserFlow(QJsonObject &client, const QString &flow)
{
QJsonArray outbounds = client.value("outbounds").toArray();
for (int i = 0; i < outbounds.size(); ++i) {
QJsonObject outbound = outbounds.at(i).toObject();
if (outbound.value("protocol").toString() != QLatin1String("vless"))
continue;
QJsonObject settings = outbound.value("settings").toObject();
QJsonArray vnext = settings.value("vnext").toArray();
QJsonObject server = vnext.first().toObject();
QJsonArray users = server.value("users").toArray();
QJsonObject user = users.first().toObject();
user["flow"] = flow;
users[0] = user;
server["users"] = users;
vnext[0] = server;
settings["vnext"] = vnext;
outbound["settings"] = settings;
outbounds[i] = outbound;
}
client["outbounds"] = outbounds;
}
void setServerClientFlow(QJsonObject &server, const QString &flow)
{
QJsonArray inbounds = server.value("inbounds").toArray();
QJsonObject inbound = inbounds.first().toObject();
QJsonObject settings = inbound.value("settings").toObject();
QJsonArray clients = settings.value("clients").toArray();
QJsonObject entry = clients.first().toObject();
entry["flow"] = flow;
clients[0] = entry;
settings["clients"] = clients;
inbound["settings"] = settings;
inbounds[0] = inbound;
server["inbounds"] = inbounds;
}
XrayServerConfig runtimeConfig(const QString &transport, const QString &security, const QString &flow,
const QString &mode = QString())
{
XrayServerConfig srv = baseConfig(transport, security, flow);
if (!mode.isEmpty())
srv.xhttp.mode = mode;
return srv;
}
QList<RuntimeCase> optionCases()
{
QList<RuntimeCase> out;
for (const QString &fp : XrayConfigModel::realityFingerprintOptions()) {
XrayServerConfig srv = runtimeConfig("raw", "reality", "xtls-rprx-vision");
srv.fingerprint = fp;
out.append({ QStringLiteral("option/raw/reality/fingerprint=%1").arg(fp), srv });
}
for (const QString &fp : XrayConfigModel::fingerprintOptions()) {
XrayServerConfig srv = runtimeConfig("raw", "tls", "");
srv.fingerprint = fp;
out.append({ QStringLiteral("option/raw/tls/fingerprint=%1").arg(fp), srv });
}
for (const QString &alpn : XrayConfigModel::alpnOptions()) {
XrayServerConfig srv = runtimeConfig("xhttp", "tls", "", "Stream-one");
srv.alpn = alpn;
out.append({ QStringLiteral("option/xhttp/tls/alpn=%1").arg(alpn), srv });
}
for (const QString &method : XrayConfigModel::xhttpUplinkMethodOptions()) {
XrayServerConfig srv = runtimeConfig("xhttp", "tls", "", "Packet-up");
srv.xhttp.uplinkMethod = method;
out.append({ QStringLiteral("option/xhttp/tls/packet-up/uplinkMethod=%1").arg(method), srv });
}
for (const QString &mtu : { QStringLiteral("576"), QStringLiteral("1460") }) {
XrayServerConfig srv = runtimeConfig("mkcp", "tls", "");
srv.mkcp.mtu = mtu;
out.append({ QStringLiteral("option/mkcp/tls/mtu=%1").arg(mtu), srv });
}
for (const QString &tti : { QStringLiteral("10"), QStringLiteral("100") }) {
XrayServerConfig srv = runtimeConfig("mkcp", "tls", "");
srv.mkcp.tti = tti;
out.append({ QStringLiteral("option/mkcp/tls/tti=%1").arg(tti), srv });
}
return out;
}
QList<RuntimeCase> ruleCases()
{
const QString vision = QStringLiteral("xtls-rprx-vision");
QList<RuntimeCase> out;
out.append({ "rule/vision-needs-raw", runtimeConfig("xhttp", "reality", "", "Stream-one"),
[vision](QJsonObject &server, QJsonObject &client) {
setServerClientFlow(server, vision);
setClientUserFlow(client, vision);
},
false });
out.append({ "rule/vision-needs-tls13", runtimeConfig("raw", "tls", vision),
[](QJsonObject &server, QJsonObject &) {
editSection(server, "inbounds", "tlsSettings",
[](QJsonObject &tls) { tls["maxVersion"] = QStringLiteral("1.2"); });
},
false });
out.append({ "rule/flow-server-vision-client-empty", runtimeConfig("raw", "reality", vision),
[](QJsonObject &, QJsonObject &client) { setClientUserFlow(client, QString()); }, false });
out.append({ "rule/flow-client-vision-server-empty", runtimeConfig("raw", "reality", ""),
[vision](QJsonObject &, QJsonObject &client) { setClientUserFlow(client, vision); }, false });
out.append({ "rule/vision-mux-tcp", runtimeConfig("raw", "reality", vision),
[](QJsonObject &, QJsonObject &client) {
QJsonArray outbounds = client.value("outbounds").toArray();
QJsonObject outbound = outbounds.first().toObject();
outbound["mux"] = QJsonObject { { "enabled", true } };
outbounds[0] = outbound;
client["outbounds"] = outbounds;
},
false });
out.append({ "rule/reality-client-hello-mlkem", runtimeConfig("raw", "reality", ""),
[](QJsonObject &, QJsonObject &client) {
editSection(client, "outbounds", "realitySettings",
[](QJsonObject &r) { r["fingerprint"] = QStringLiteral("hellochrome_120"); });
},
false, "26.10.17" });
out.append({ "rule/reality-fingerprint-tls12", runtimeConfig("raw", "reality", ""),
[](QJsonObject &, QJsonObject &client) {
editSection(client, "outbounds", "realitySettings",
[](QJsonObject &r) { r["fingerprint"] = QStringLiteral("android"); });
},
false });
out.append({ "rule/vision-fingerprint-tls12", runtimeConfig("raw", "tls", vision),
[](QJsonObject &, QJsonObject &client) {
editSection(client, "outbounds", "tlsSettings",
[](QJsonObject &tls) { tls["fingerprint"] = QStringLiteral("android"); });
},
false });
out.append({ "rule/reality-sni-ip", runtimeConfig("raw", "reality", ""),
[](QJsonObject &, QJsonObject &client) {
editSection(client, "outbounds", "realitySettings",
[](QJsonObject &r) { r["serverName"] = QStringLiteral("1.1.1.1"); });
},
false });
const QList<QStringList> modeClashes = {
{ "rule/xhttp-server-mode-stream-one", "tls", "Packet-up", "stream-one" },
{ "rule/xhttp-server-mode-stream-up", "tls", "Packet-up", "stream-up" },
{ "rule/xhttp-server-mode-packet-up", "tls", "Stream-one", "packet-up" },
{ "rule/xhttp-auto-vs-server-stream-one", "tls", "Auto", "stream-one" },
{ "rule/xhttp-auto-vs-server-packet-up", "reality", "Auto", "packet-up" },
};
for (const QStringList &clash : modeClashes) {
const QString serverMode = clash.at(3);
out.append({ clash.at(0), runtimeConfig("xhttp", clash.at(1), "", clash.at(2)),
[serverMode](QJsonObject &server, QJsonObject &) {
editSection(server, "inbounds", "xhttpSettings",
[serverMode](QJsonObject &x) { x["mode"] = serverMode; });
},
false });
}
XrayServerConfig noH2 = runtimeConfig("xhttp", "tls", "", "Stream-one");
noH2.alpn = QStringLiteral("h2");
out.append({ "rule/xhttp-fingerprint-no-h2", noH2,
[](QJsonObject &, QJsonObject &client) {
editSection(client, "outbounds", "tlsSettings",
[](QJsonObject &tls) { tls["fingerprint"] = QStringLiteral("android"); });
},
false });
const QList<QStringList> brokenPostBytes = {
{ "rule/xhttp-sc-max-each-post-bytes", "0-100" },
{ "rule/xhttp-range-reversed-zero", "1000000-0" },
};
for (const QStringList &broken : brokenPostBytes) {
const QString range = broken.at(1);
out.append({ broken.at(0), runtimeConfig("xhttp", "tls", "", "Packet-up"),
[range](QJsonObject &, QJsonObject &client) {
editSection(client, "outbounds", "xhttpSettings",
[range](QJsonObject &x) { x["scMaxEachPostBytes"] = range; });
},
false });
}
out.append({ "rule/xhttp-auto-resolve-reality", runtimeConfig("xhttp", "reality", "", "Auto") });
out.append({ "rule/xhttp-auto-resolve-other", runtimeConfig("xhttp", "tls", "", "Auto") });
XrayServerConfig unlimited = runtimeConfig("xhttp", "tls", "", "Stream-one");
unlimited.xhttp.xmux.enabled = true;
unlimited.xhttp.xmux.cMaxReuseTimesMin = QStringLiteral("0");
unlimited.xhttp.xmux.cMaxReuseTimesMax = QStringLiteral("0");
out.append({ "rule/xmux-zero-unlimited", unlimited });
XrayServerConfig capacity = runtimeConfig("mkcp", "tls", "");
capacity.mkcp.uplinkCapacity = QStringLiteral("5000");
capacity.mkcp.downlinkCapacity = QStringLiteral("5000");
out.append({ "rule/kcp-capacity-overflow", capacity });
return out;
}
QList<RuntimeCase> fingerprintCases()
{
const QList<QPair<QString, XrayServerConfig>> realityTransports = {
{ "raw-vision", runtimeConfig("raw", "reality", "xtls-rprx-vision") },
{ "xhttp-stream-one", runtimeConfig("xhttp", "reality", "", "Stream-one") },
{ "xhttp-packet-up", runtimeConfig("xhttp", "reality", "", "Packet-up") },
};
QList<RuntimeCase> out;
for (const RealityFingerprintCase &fp : realityFingerprintCases()) {
if (fp.hello == RealityHello::Unreliable)
continue;
const QString name = fp.name;
const QString since =
fp.hello == RealityHello::RealCertificate ? QStringLiteral("26.10.17") : QString();
for (const auto &transport : realityTransports) {
out.append({ QStringLiteral("fingerprint/core/%1/%2").arg(transport.first, name), transport.second,
[name](QJsonObject &, QJsonObject &client) { setClientRealityFingerprint(client, name); },
fp.hello == RealityHello::Connects, since });
}
}
for (const QString &stored : legacyRealityFingerprints()) {
for (const auto &transport : realityTransports) {
XrayServerConfig srv = transport.second;
srv.fingerprint = stored;
out.append({ QStringLiteral("fingerprint/app/%1/'%2'").arg(transport.first, stored), srv });
}
}
XrayServerConfig xhttpTls = runtimeConfig("xhttp", "tls", "", "Stream-one");
xhttpTls.alpn = QStringLiteral("h2");
const QList<QPair<QString, XrayServerConfig>> tlsTransports = {
{ "raw-vision", runtimeConfig("raw", "tls", "xtls-rprx-vision") },
{ "xhttp-stream-one-h2", xhttpTls },
};
for (const QString &fp : XrayConfigModel::fingerprintOptions()) {
const bool tls12Only = fp == QLatin1String("android") || fp == QLatin1String("360");
for (const auto &transport : tlsTransports) {
XrayServerConfig srv = transport.second;
srv.fingerprint = fp;
out.append({ QStringLiteral("fingerprint/tls/%1/%2").arg(transport.first, fp), srv, {}, !tls12Only });
}
}
return out;
}
const QList<RuntimeCase> &cases()
{
static const QList<RuntimeCase> all = [] {
QList<RuntimeCase> out;
for (const MatrixCase &m : matrix()) {
if (m.allowedByUi)
out.append({ m.name, m.srv });
}
out << optionCases() << ruleCases() << fingerprintCases();
return out;
}();
return all;
}
}
class TestXrayRuntime : public QObject
{
Q_OBJECT
private:
QString m_serverBin;
QString m_clientBin;
QString m_serverVersion;
QTemporaryDir m_dir;
QString m_certFile;
QString m_keyFile;
QString m_certPin;
QString m_realityPrivateKey;
QString m_realityPublicKey;
QTcpServer m_http;
QString m_lastReason;
QStringList m_failures;
int m_casesRun = 0;
bool m_ready = false;
QString writeConfig(const QJsonObject &config, const QString &name)
{
const QString path = m_dir.filePath(name + QStringLiteral(".json"));
QFile file(path);
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate))
file.write(QJsonDocument(config).toJson());
return path;
}
void answerHttp()
{
while (QTcpSocket *socket = m_http.nextPendingConnection()) {
connect(socket, &QTcpSocket::readyRead, socket, [socket] {
const QByteArray request = socket->peek(socket->bytesAvailable());
if (!request.contains("\r\n\r\n"))
return;
const QByteArray path = request.split(' ').value(1);
const QByteArray body = path.mid(1);
socket->write("HTTP/1.1 200 OK\r\nContent-Length: " + QByteArray::number(body.size())
+ "\r\nConnection: close\r\n\r\n" + body);
socket->disconnectFromHost();
});
connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater);
}
}
QJsonObject serverConfig(const XrayServerConfig &srv) const
{
XrayServerInboundInputs in;
in.clients = QJsonArray { XrayServerConfig::makeClientEntry(clientId(), xrayEffective::serverFlow(srv)) };
in.realityPrivateKey = m_realityPrivateKey;
in.realityShortId = shortId();
QJsonObject server = srv.toServerInboundJson(in);
QJsonArray inbounds = server.value("inbounds").toArray();
QJsonObject inbound = inbounds.first().toObject();
inbound["listen"] = QStringLiteral("127.0.0.1");
inbounds[0] = inbound;
server["inbounds"] = inbounds;
editSection(server, "inbounds", "tlsSettings", [this](QJsonObject &tls) {
tls["certificates"] =
QJsonArray { QJsonObject { { "certificateFile", m_certFile }, { "keyFile", m_keyFile } } };
});
server["log"] = QJsonObject { { "loglevel", "warning" } };
server["outbounds"] = QJsonArray { QJsonObject {
{ "protocol", "freedom" },
{ "settings",
QJsonObject { { "redirect", QStringLiteral("127.0.0.1:%1").arg(m_http.serverPort()) },
{ "finalRules",
QJsonArray { QJsonObject { { "action", "allow" },
{ "ip", QJsonArray { "127.0.0.1/32" } } } } } } } } };
return server;
}
QJsonObject clientConfig(const XrayServerConfig &srv, int socksPort) const
{
XrayClientOutboundInputs in;
in.serverAddress = QStringLiteral("127.0.0.1");
in.clientId = clientId();
in.realityPublicKey = m_realityPublicKey;
in.realityShortId = shortId();
in.tlsPinnedPeerCertSha256 = m_certPin;
XrayProtocolConfig cfg;
cfg.serverConfig = srv;
QJsonObject client = cfg.toClientOutboundJson(in);
QJsonArray inbounds = client.value("inbounds").toArray();
QJsonObject socks = inbounds.first().toObject();
socks["listen"] = QStringLiteral("127.0.0.1");
socks["port"] = socksPort;
inbounds[0] = socks;
client["inbounds"] = inbounds;
client["log"] = QJsonObject { { "loglevel", "warning" } };
return client;
}
bool runCase(const RuntimeCase &c, QString &why)
{
XrayServerConfig srv = c.srv;
srv.port = QString::number(xrayruntime::freeLocalPort());
srv.sni = kSite;
srv.site = kSite;
QJsonObject server = serverConfig(srv);
const int socksPort = xrayruntime::freeLocalPort();
QJsonObject client = clientConfig(srv, socksPort);
if (c.edit)
c.edit(server, client);
const QString id = QUuid::createUuid().toString(QUuid::Id128);
xrayruntime::XrayRun serverRun;
xrayruntime::XrayRun clientRun;
auto logs = [&] {
const QString serverLog = xrayruntime::collapsedLog(serverRun.stop());
const QString clientLog = xrayruntime::collapsedLog(clientRun.stop());
QStringList parts;
if (!serverLog.isEmpty())
parts << QStringLiteral("server: ") + serverLog;
if (!clientLog.isEmpty())
parts << QStringLiteral("client: ") + clientLog;
return parts.join(QStringLiteral(" | "));
};
if (!serverRun.start(m_serverBin, writeConfig(server, id + QStringLiteral("_server")))) {
why = QStringLiteral("server did not start | ") + logs();
return false;
}
if (!clientRun.start(m_clientBin, writeConfig(client, id + QStringLiteral("_client")))) {
why = QStringLiteral("client did not start | ") + logs();
return false;
}
const QByteArray path = "/" + id.toLatin1();
QString error;
const int attempts = c.traffic ? 2 : 1;
bool flowed = false;
for (int attempt = 0; attempt < attempts && !flowed; ++attempt) {
error.clear();
xrayruntime::httpGetThroughSocks(socksPort, kTarget, 80, path, 8000, error);
flowed = error.isEmpty();
}
const QString runLog = logs();
why = flowed ? QStringLiteral("traffic flowed") : error;
if (!runLog.isEmpty())
why += QStringLiteral(" | ") + runLog;
return flowed;
}
private slots:
void initTestCase()
{
const QString bin = qEnvironmentVariable("XRAY_BIN");
m_serverBin = qEnvironmentVariable("XRAY_SERVER_BIN", bin);
m_clientBin = qEnvironmentVariable("XRAY_CLIENT_BIN", bin);
if (!QFileInfo(m_serverBin).isExecutable() || !QFileInfo(m_clientBin).isExecutable())
QSKIP("Set XRAY_BIN, or XRAY_SERVER_BIN and XRAY_CLIENT_BIN, to xray binaries");
QVERIFY(m_dir.isValid());
m_serverVersion = xrayruntime::version(m_serverBin);
qInfo().noquote() << "server xray" << m_serverVersion << "client xray" << xrayruntime::version(m_clientBin);
const QString prefix = m_dir.filePath(QStringLiteral("tls"));
QProcess cert;
cert.start(m_serverBin, { "tls", "cert", "-domain", "amnezia-xray", "-file", prefix });
QVERIFY(cert.waitForFinished(30000));
m_certFile = prefix + QStringLiteral(".crt");
m_keyFile = prefix + QStringLiteral(".key");
const QList<QSslCertificate> certs = QSslCertificate::fromPath(m_certFile);
QVERIFY2(!certs.isEmpty(), "xray tls cert did not produce a certificate");
m_certPin = QString::fromLatin1(certs.first().digest(QCryptographicHash::Sha256).toHex());
QProcess keys;
keys.start(m_serverBin, { "x25519" });
QVERIFY(keys.waitForFinished(30000));
for (const QString &line : QString::fromUtf8(keys.readAllStandardOutput()).split(QLatin1Char('\n'))) {
const QString value = line.section(QLatin1Char(':'), 1).trimmed();
if (line.startsWith(QLatin1String("PrivateKey")))
m_realityPrivateKey = value;
else if (line.contains(QLatin1String("PublicKey")))
m_realityPublicKey = value;
}
QVERIFY2(!m_realityPrivateKey.isEmpty() && !m_realityPublicKey.isEmpty(), "xray x25519 output not recognised");
connect(&m_http, &QTcpServer::newConnection, this, &TestXrayRuntime::answerHttp);
QVERIFY(m_http.listen(QHostAddress::LocalHost, 0));
m_ready = true;
}
void cleanup()
{
++m_casesRun;
if (QTest::currentTestFailed())
m_failures << QStringLiteral("%1: %2").arg(QString::fromLatin1(QTest::currentDataTag()), m_lastReason);
m_lastReason.clear();
}
void cleanupTestCase()
{
if (m_ready)
std::fprintf(stderr, "%s\n",
qPrintable(xrayruntime::failureSummary(
QStringLiteral("xray runtime (server %1, client %2)")
.arg(m_serverVersion, xrayruntime::version(m_clientBin)),
m_failures, m_casesRun)));
}
void connects_data()
{
QTest::addColumn<int>("index");
for (int i = 0; i < cases().size(); ++i)
QTest::newRow(qPrintable(cases().at(i).name)) << i;
}
void connects()
{
QFETCH(int, index);
const RuntimeCase &c = cases().at(index);
const bool ruleApplies = c.sinceServer.isEmpty() || xrayruntime::versionAtLeast(m_serverVersion, c.sinceServer);
const bool expectTraffic = ruleApplies ? c.traffic : !c.traffic;
QString why;
const bool flowed = runCase(c, why);
m_lastReason = expectTraffic ? why : QStringLiteral("expected no traffic, but it flowed");
QVERIFY2(flowed == expectTraffic, qPrintable(m_lastReason));
}
void appMapsRealityFingerprint_data()
{
QTest::addColumn<QString>("stored");
QTest::addColumn<QString>("sent");
for (const QString &stored : legacyRealityFingerprints())
QTest::newRow(qPrintable(QStringLiteral("'%1'").arg(stored))) << stored << QStringLiteral("chrome");
for (const RealityFingerprintCase &fp : realityFingerprintCases()) {
if (fp.hello == RealityHello::Connects)
QTest::newRow(qPrintable(fp.name)) << fp.name << fp.name;
}
}
void appMapsRealityFingerprint()
{
QFETCH(QString, stored);
QFETCH(QString, sent);
XrayServerConfig srv = runtimeConfig("raw", "reality", "xtls-rprx-vision");
srv.fingerprint = stored;
const QJsonObject reality = streamOf(vlessOutbound(clientJson(srv))).value("realitySettings").toObject();
QCOMPARE(reality.value("fingerprint").toString(), sent);
}
void realityRandomFingerprint()
{
const RuntimeCase random { QStringLiteral("random"), runtimeConfig("raw", "reality", "xtls-rprx-vision"),
[](QJsonObject &, QJsonObject &client) {
setClientRealityFingerprint(client, QStringLiteral("random"));
} };
const int runs = 10;
int flowed = 0;
for (int i = 0; i < runs; ++i) {
QString why;
flowed += runCase(random, why) ? 1 : 0;
}
qInfo().noquote() << QStringLiteral("REALITY fingerprint random: traffic in %1 of %2 runs on server %3")
.arg(flowed)
.arg(runs)
.arg(m_serverVersion);
}
};
QTEST_GUILESS_MAIN(TestXrayRuntime)
#include "testXrayRuntime.moc"
+53
View File
@@ -124,6 +124,59 @@ private slots:
QCOMPARE(importResult.config, config);
}
void testVlessLinkRoundTrip()
{
const QJsonObject extra {
{ "xPaddingObfsMode", true },
{ "xPaddingKey", "x_pad" },
{ "xPaddingPlacement", "cookie" },
{ "xPaddingBytes", "200-400" },
{ "xmux", QJsonObject { { "maxConnections", 4 } } },
};
const QString pin = QStringLiteral("0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9");
serialization::VlessServerObject server;
server.address = QStringLiteral("203.0.113.10");
server.port = 443;
server.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
server.flow.clear();
server.network = QStringLiteral("xhttp");
server.security = QStringLiteral("tls");
server.serverName = QStringLiteral("www.example.com");
server.fingerprint = QStringLiteral("chrome");
server.alpn = QStringLiteral("h2,http/1.1");
server.xhttpHost = QStringLiteral("www.example.com");
server.xhttpPath = QStringLiteral("/p");
server.xhttpMode = QStringLiteral("packet-up");
server.xhttpExtra = QString::fromUtf8(QJsonDocument(extra).toJson(QJsonDocument::Compact));
server.tlsPin = pin;
const QString link = serialization::vless::Serialize(server, QStringLiteral("rt"));
QVERIFY2(link.startsWith(QStringLiteral("vless://")), qPrintable(link));
QString alias;
QString error;
const QJsonObject root = serialization::vless::Deserialize(link, &alias, &error);
QVERIFY2(!root.isEmpty(), qPrintable(error + QStringLiteral(" ") + link));
const QJsonObject stream =
root.value("outbounds").toArray().first().toObject().value("streamSettings").toObject();
QCOMPARE(stream.value("network").toString(), QStringLiteral("xhttp"));
QCOMPARE(stream.value("security").toString(), QStringLiteral("tls"));
const QJsonObject xhttp = stream.value("xhttpSettings").toObject();
QCOMPARE(xhttp.value("host").toString(), server.xhttpHost);
QCOMPARE(xhttp.value("path").toString(), server.xhttpPath);
QCOMPARE(xhttp.value("mode").toString(), server.xhttpMode);
QCOMPARE(xhttp.value("extra").toObject(), extra);
const QJsonObject tls = stream.value("tlsSettings").toObject();
QCOMPARE(tls.value("pinnedPeerCertSha256").toString(), pin);
QCOMPARE(tls.value("serverName").toString(), server.serverName);
QCOMPARE(tls.value("fingerprint").toString(), server.fingerprint);
QCOMPARE(tls.value("alpn").toArray(), QJsonArray({ "h2", "http/1.1" }));
}
void testVmessNew()
{
const QString configData = getEnvValue("THIRD_PARTY_VMESS_NEW_IMPORT_DATA");
+34
View File
@@ -84,6 +84,8 @@ private slots:
void udp443FlowStaysOnTheDevice();
void mkcpTlsOutboundHasNoFingerprint();
void xhttpPathGetsLeadingSlash();
void emptyRangeStaysCoreDefault();
void halfEmptyRangeUsesCoreDefault();
};
void TestXrayServerInbound::serverInboundCarriesNoDeviceSettings_data()
@@ -243,6 +245,38 @@ void TestXrayServerInbound::xhttpPathGetsLeadingSlash()
QVERIFY2(inbound.contains(QStringLiteral("\"path\":\"/text\"")), qPrintable(inbound));
}
void TestXrayServerInbound::emptyRangeStaysCoreDefault()
{
XrayServerConfig c = fullyConfigured();
c.xhttp.scStreamUpServerSecsMin.clear();
c.xhttp.scStreamUpServerSecsMax.clear();
c.xhttp.scMaxEachPostBytesMin.clear();
c.xhttp.scMaxEachPostBytesMax.clear();
const QJsonObject inbound = c.toServerInboundJson(inboundInputs());
const QString text = compact(inbound);
QVERIFY2(!text.contains(QStringLiteral("scStreamUpServerSecs")), qPrintable(text));
QVERIFY2(!text.contains(QStringLiteral("scMaxEachPostBytes")), qPrintable(text));
XrayServerConfig rebuilt;
QCOMPARE(XrayServerConfig::fromServerInboundJson(inbound, rebuilt), XrayServerJsonStatus::Ok);
rebuilt.applyDefaults(false);
QVERIFY(rebuilt.xhttp.scStreamUpServerSecsMin.isEmpty());
QVERIFY(rebuilt.xhttp.scStreamUpServerSecsMax.isEmpty());
QVERIFY(rebuilt.xhttp.scMaxEachPostBytesMin.isEmpty());
QVERIFY(rebuilt.xhttp.scMaxEachPostBytesMax.isEmpty());
}
void TestXrayServerInbound::halfEmptyRangeUsesCoreDefault()
{
XrayServerConfig c = fullyConfigured();
c.xhttp.scStreamUpServerSecsMin.clear();
c.xhttp.scStreamUpServerSecsMax = "50";
const QString inbound = compact(c.toServerInboundJson(inboundInputs()));
QVERIFY2(inbound.contains(QStringLiteral("\"scStreamUpServerSecs\":\"20-50\"")), qPrintable(inbound));
}
QTEST_APPLESS_MAIN(TestXrayServerInbound)
#include "testXrayServerInbound.moc"
+695
View File
@@ -0,0 +1,695 @@
#ifndef XRAY_CONTRACT_CASES_H
#define XRAY_CONTRACT_CASES_H
#include <functional>
#include <tuple>
#include <QList>
#include <QString>
#include <QStringList>
#include "core/models/protocols/xrayProtocolConfig.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayMatrix.h"
namespace xraytest
{
enum class AppVerdict {
Ok,
Blocked,
};
enum class CoreVerdict {
Accept,
Reject,
Explore,
};
inline QString verdictName(AppVerdict v)
{
return v == AppVerdict::Ok ? QStringLiteral("ok") : QStringLiteral("blocked");
}
inline QString verdictName(CoreVerdict v)
{
switch (v) {
case CoreVerdict::Accept: return QStringLiteral("accept");
case CoreVerdict::Reject: return QStringLiteral("reject");
case CoreVerdict::Explore: return QStringLiteral("explore");
}
return {};
}
using ModelEdit = std::function<void(XrayConfigModel &)>;
using Setter = void (XrayConfigModel::*)(const QString &);
struct ContractCase
{
QString name;
QString ruleId;
QString transport;
QString security;
QString flow;
ModelEdit edit;
AppVerdict app = AppVerdict::Ok;
CoreVerdict core = CoreVerdict::Accept;
QString coreMessage;
QString stricterWhy;
QString knownIssue;
bool viaUi = true;
ContractCase &rule(const QString &id) { ruleId = id; return *this; }
ContractCase &blocked() { app = AppVerdict::Blocked; return *this; }
ContractCase &reject(const QString &message = QString()) { core = CoreVerdict::Reject; coreMessage = message; return *this; }
ContractCase &explore() { core = CoreVerdict::Explore; return *this; }
ContractCase &stricter(const QString &why) { stricterWhy = why; return *this; }
ContractCase &known(const QString &why) { knownIssue = why; return *this; }
ContractCase &rawInput() { viaUi = false; return *this; }
};
struct ContractCtx
{
QString label;
QString transport;
QString security;
QString flow;
ModelEdit prep;
};
inline ContractCtx rawCtx(const QString &security, const QString &flow = QStringLiteral("xtls-rprx-vision"))
{
return { QStringLiteral("raw/%1/%2").arg(security, flow.isEmpty() ? QStringLiteral("noflow") : flow),
QStringLiteral("raw"), security, flow, {} };
}
inline ContractCtx xhttpCtx(const QString &security, const QString &mode)
{
return { QStringLiteral("xhttp/%1/%2").arg(security, mode.toLower()), QStringLiteral("xhttp"), security,
QString(), [mode](XrayConfigModel &m) { m.setXhttpMode(mode); } };
}
inline ContractCtx xhttpHeaderKeysCtx()
{
return { QStringLiteral("xhttp/tls/packet-up/header-placements"), QStringLiteral("xhttp"), QStringLiteral("tls"),
QString(), [](XrayConfigModel &m) {
m.setXhttpMode(QStringLiteral("Packet-up"));
m.setXhttpSessionPlacement(QStringLiteral("Header"));
m.setXhttpSeqPlacement(QStringLiteral("Header"));
m.setXhttpUplinkDataPlacement(QStringLiteral("Header"));
m.setXPaddingObfsMode(true);
m.setXPaddingPlacement(QStringLiteral("Header"));
} };
}
inline ContractCtx kcpCtx(const QString &security)
{
return { QStringLiteral("mkcp/%1").arg(security), QStringLiteral("mkcp"), security, QString(), {} };
}
inline ContractCase kase(const ContractCtx &ctx, const QString &what, const ModelEdit &edit)
{
ContractCase c;
c.name = ctx.label + QLatin1Char('/') + what;
c.transport = ctx.transport;
c.security = ctx.security;
c.flow = ctx.flow;
if (ctx.security == QLatin1String("none"))
c.blocked().reject(QStringLiteral("vless without TLS"));
const ModelEdit prep = ctx.prep;
c.edit = [prep, edit](XrayConfigModel &m) {
if (prep)
prep(m);
if (edit)
edit(m);
};
return c;
}
inline ModelEdit set(Setter setter, const QString &value)
{
return [setter, value](XrayConfigModel &m) { (m.*setter)(value); };
}
inline QString shown(const QString &value)
{
if (value.isEmpty())
return QStringLiteral("empty");
if (value.size() > 24)
return QStringLiteral("%1chars").arg(value.size());
return value;
}
inline QStringList contractErrors(const ContractCase &c, amnezia::XrayServerConfig *outServer = nullptr)
{
amnezia::XrayProtocolConfig stored;
stored.serverConfig = baseConfig(c.transport, c.security, c.flow);
XrayConfigModel model;
model.updateModel(amnezia::DockerContainer::Xray, stored);
if (c.edit)
c.edit(model);
if (outServer)
*outServer = model.getProtocolConfig().serverConfig;
return model.validationErrors();
}
struct RangeExpect
{
AppVerdict app;
CoreVerdict core;
const char *message;
const char *stricter;
const char *known;
};
struct RangeInput
{
const char *min;
const char *max;
bool viaUi;
};
inline const RangeInput (&rangeInputs())[10]
{
static const RangeInput inputs[10] = {
{ "", "", true }, { "0", "0", true }, { "0", "100", true },
{ "100", "0", true }, { "1", "1", true }, { "400", "200", false },
{ "-1", "5", false }, { "abc", "5", false }, { "1", "2147483648", false },
{ "1", "9223372036854775808", false },
};
return inputs;
}
inline void addRange(QList<ContractCase> &out, const ContractCtx &ctx, const QString &field, const QString &ruleId,
Setter setMin, Setter setMax, const RangeExpect (&expect)[10])
{
for (int i = 0; i < 10; ++i) {
const RangeInput &in = rangeInputs()[i];
const QString min = QString::fromLatin1(in.min);
const QString max = QString::fromLatin1(in.max);
const QString what = min.isEmpty() && max.isEmpty() ? QStringLiteral("%1=empty").arg(field)
: QStringLiteral("%1=%2..%3").arg(field, min, max);
ContractCase c = kase(ctx, what, [setMin, setMax, min, max](XrayConfigModel &m) {
(m.*setMin)(min);
(m.*setMax)(max);
});
c.ruleId = ruleId;
c.app = expect[i].app;
c.core = expect[i].core;
c.coreMessage = QString::fromLatin1(expect[i].message);
c.stricterWhy = QString::fromLatin1(expect[i].stricter);
c.knownIssue = QString::fromLatin1(expect[i].known);
c.viaUi = in.viaUi;
out.append(c);
}
}
inline QList<ContractCase> buildContractCases()
{
using A = AppVerdict;
using C = CoreVerdict;
QList<ContractCase> out;
const ContractCtx rawReality = rawCtx(QStringLiteral("reality"));
const ContractCtx rawTls = rawCtx(QStringLiteral("tls"));
const ContractCtx rawTlsNoFlow = rawCtx(QStringLiteral("tls"), QString());
const ContractCtx rawNone = rawCtx(QStringLiteral("none"), QString());
const ContractCtx xTlsOne = xhttpCtx(QStringLiteral("tls"), QStringLiteral("Stream-one"));
const ContractCtx xTlsPacket = xhttpCtx(QStringLiteral("tls"), QStringLiteral("Packet-up"));
const ContractCtx xTlsUp = xhttpCtx(QStringLiteral("tls"), QStringLiteral("Stream-up"));
const ContractCtx xRealityOne = xhttpCtx(QStringLiteral("reality"), QStringLiteral("Stream-one"));
const ContractCtx kcpTls = kcpCtx(QStringLiteral("tls"));
for (const ContractCtx &ctx : { rawReality, rawTls, rawTlsNoFlow, rawNone, xTlsOne, xTlsPacket, xTlsUp,
xRealityOne, kcpTls, xhttpHeaderKeysCtx() })
out.append(kase(ctx, QStringLiteral("baseline"), {}));
for (const QString port : { QStringLiteral("1"), QStringLiteral("65535") })
out.append(kase(rawReality, QStringLiteral("port=") + port, set(&XrayConfigModel::setPort, port)));
for (const QString port : { QStringLiteral("0"), QStringLiteral("65536"), QString() })
out.append(kase(rawReality, QStringLiteral("port=") + shown(port), set(&XrayConfigModel::setPort, port))
.blocked().explore());
for (const QString port : { QStringLiteral("-1"), QStringLiteral("abc") })
out.append(kase(rawReality, QStringLiteral("port=") + port, set(&XrayConfigModel::setPort, port))
.blocked().explore().rawInput());
const QString longName = QString(250, QLatin1Char('a')) + QStringLiteral(".com");
struct SniRow
{
QString value;
bool viaUi;
};
const QList<SniRow> badSni = {
{ QStringLiteral("*"), false }, { QStringLiteral("*.example.com"), false },
{ QStringLiteral("a b"), false }, { QStringLiteral("пример.рф"), false },
{ QStringLiteral("example.com."), true }, { QStringLiteral("12345"), true },
{ longName, true },
};
for (const ContractCtx &ctx : { rawTls, rawReality }) {
const bool reality = ctx.security == QLatin1String("reality");
for (const QString sni : { QStringLiteral("www.example.com"), QStringLiteral("xn--e1afmkfd.com"), QString() })
out.append(kase(ctx, QStringLiteral("sni=") + shown(sni), set(&XrayConfigModel::setSni, sni)));
out.append(kase(ctx, QStringLiteral("sni=xn--e1afmkfd.xn--p1ai"),
set(&XrayConfigModel::setSni, QStringLiteral("xn--e1afmkfd.xn--p1ai"))));
for (const SniRow &row : badSni) {
ContractCase c = kase(ctx, QStringLiteral("sni=") + shown(row.value), set(&XrayConfigModel::setSni, row.value))
.blocked()
.stricter(QStringLiteral("the core takes any serverName; the TLS handshake fails later"));
c.viaUi = row.viaUi;
out.append(c);
}
ContractCase ipv4 = kase(ctx, QStringLiteral("sni=203.0.113.10"),
set(&XrayConfigModel::setSni, QStringLiteral("203.0.113.10")));
ContractCase ipv6 = kase(ctx, QStringLiteral("sni=2001:db8::1"),
set(&XrayConfigModel::setSni, QStringLiteral("2001:db8::1")));
if (reality) {
ipv4.rule(QStringLiteral("reality-sni-ip")).blocked()
.stricter(QStringLiteral("REALITY with an IP SNI builds but carries no traffic"));
ipv6.rule(QStringLiteral("reality-server-required")).blocked().reject(QStringLiteral("valid value for \"target\""));
}
out.append(ipv4);
out.append(ipv6);
}
const QStringList tls13Fingerprints = XrayConfigModel::realityFingerprintOptions();
const QStringList tls12Fingerprints = { QStringLiteral("android"), QStringLiteral("360") };
struct FpCtx
{
ContractCtx ctx;
QString tls12Rule;
QString tls12Why;
};
const QList<FpCtx> fpContexts = {
{ rawTls, QStringLiteral("vision-fingerprint-tls12"), QStringLiteral("vision needs TLS 1.3; the core fails at runtime") },
{ rawReality, QStringLiteral("reality-fingerprint-tls12"), QStringLiteral("REALITY needs TLS 1.3; the core fails at runtime") },
{ xTlsOne, QStringLiteral("xhttp-fingerprint-no-h2"), QStringLiteral("the fingerprint offers no h2 while ALPN is h2") },
};
for (const FpCtx &f : fpContexts) {
for (const QString &fp : tls13Fingerprints)
out.append(kase(f.ctx, QStringLiteral("fingerprint=") + fp, set(&XrayConfigModel::setFingerprint, fp)));
for (const QString &fp : tls12Fingerprints)
out.append(kase(f.ctx, QStringLiteral("fingerprint=") + fp, set(&XrayConfigModel::setFingerprint, fp))
.rule(f.tls12Rule).blocked().stricter(f.tls12Why));
ContractCase foo = kase(f.ctx, QStringLiteral("fingerprint=foo"),
set(&XrayConfigModel::setFingerprint, QStringLiteral("foo")))
.rule(QStringLiteral("tls-fingerprint-known")).blocked().rawInput();
if (f.ctx.security == QLatin1String("reality"))
foo.stricter(QStringLiteral("the generator sends chrome instead of an unknown REALITY fingerprint"));
else
foo.reject(QStringLiteral("unknown \"fingerprint\""));
out.append(foo);
out.append(kase(f.ctx, QStringLiteral("fingerprint=Chrome"), set(&XrayConfigModel::setFingerprint, QStringLiteral("Chrome")))
.rawInput());
}
out.append(kase(rawTls, QStringLiteral("fingerprint=unsafe"), set(&XrayConfigModel::setFingerprint, QStringLiteral("unsafe")))
.blocked().stricter(QStringLiteral("TLS accepts \"unsafe\", which turns uTLS off")).rawInput());
out.append(kase(rawReality, QStringLiteral("fingerprint=unsafe"), set(&XrayConfigModel::setFingerprint, QStringLiteral("unsafe")))
.rule(QStringLiteral("reality-fingerprint-unsafe")).blocked()
.stricter(QStringLiteral("the generator sends chrome instead of \"unsafe\" for REALITY")).rawInput());
for (const QString &fp : tls12Fingerprints) {
out.append(kase(rawTlsNoFlow, QStringLiteral("fingerprint=") + fp, set(&XrayConfigModel::setFingerprint, fp)));
out.append(kase(xTlsOne, QStringLiteral("alpn=http/1.1+fingerprint=") + fp, [fp](XrayConfigModel &m) {
m.setAlpn(QStringLiteral("http/1.1"));
m.setFingerprint(fp);
}).rule(QStringLiteral("xhttp-fingerprint-no-h2")));
out.append(kase(kcpTls, QStringLiteral("fingerprint=") + fp, set(&XrayConfigModel::setFingerprint, fp))
.rule(QStringLiteral("tls-fingerprint-kcp-ignored")));
}
out.append(kase(kcpTls, QStringLiteral("fingerprint=foo"), set(&XrayConfigModel::setFingerprint, QStringLiteral("foo")))
.rule(QStringLiteral("tls-fingerprint-kcp-ignored")).blocked()
.stricter(QStringLiteral("mKCP does not write the fingerprint")).rawInput());
for (const ContractCtx &ctx : { rawTls, xTlsOne }) {
for (const QString alpn : { QStringLiteral("h2"), QStringLiteral("http/1.1"), QStringLiteral("h2,http/1.1"),
QStringLiteral("HTTP/2"), QString() }) {
ContractCase c = kase(ctx, QStringLiteral("alpn=") + shown(alpn), set(&XrayConfigModel::setAlpn, alpn));
if (ctx.transport == QLatin1String("xhttp"))
c.rule(QStringLiteral("xhttp-http-version"));
out.append(c);
}
out.append(kase(ctx, QStringLiteral("alpn=foo"), set(&XrayConfigModel::setAlpn, QStringLiteral("foo")))
.blocked().stricter(QStringLiteral("the core takes any ALPN token; the handshake fails later"))
.rawInput());
}
out.append(kase(rawReality, QStringLiteral("alpn=foo"), set(&XrayConfigModel::setAlpn, QStringLiteral("foo"))).rawInput());
for (const QString host : { QString(), QStringLiteral("cdn.example.com"), QStringLiteral("203.0.113.10") })
out.append(kase(xTlsOne, QStringLiteral("host=") + shown(host), set(&XrayConfigModel::setXhttpHost, host)));
for (const QString host : { QStringLiteral("bad host"), QStringLiteral("a:80"),
QStringLiteral("пример.рф") })
out.append(kase(xTlsOne, QStringLiteral("host=") + host, set(&XrayConfigModel::setXhttpHost, host))
.blocked().stricter(QStringLiteral("the core takes any host string")).rawInput());
out.append(kase(xTlsOne, QStringLiteral("host=::1"), set(&XrayConfigModel::setXhttpHost, QStringLiteral("::1")))
.blocked().stricter(QStringLiteral("an IPv6 host without brackets breaks the request URL")));
for (const QString path : { QStringLiteral("/"), QString(), QStringLiteral("text"), QStringLiteral("/?ed=1"),
QStringLiteral("/%"), QStringLiteral("/") + QString(2000, QLatin1Char('p')) })
out.append(kase(xTlsOne, QStringLiteral("path=") + shown(path), set(&XrayConfigModel::setXhttpPath, path)));
out.append(kase(xTlsOne, QStringLiteral("path=/a b"), set(&XrayConfigModel::setXhttpPath, QStringLiteral("/a b"))).rawInput());
for (const QString security : { QStringLiteral("none"), QStringLiteral("tls"), QStringLiteral("reality") }) {
for (const QString &mode : XrayConfigModel::xhttpModeOptions()) {
QString ruleId = QStringLiteral("xhttp-mode-enum");
if (mode == QLatin1String("Auto"))
ruleId = security == QLatin1String("reality") ? QStringLiteral("xhttp-auto-resolve-reality")
: QStringLiteral("xhttp-auto-resolve-other");
out.append(kase(xhttpCtx(security, mode), QStringLiteral("mode"), {}).rule(ruleId));
}
}
out.append(kase(xTlsOne, QStringLiteral("mode=foo"), set(&XrayConfigModel::setXhttpMode, QStringLiteral("foo")))
.rule(QStringLiteral("xhttp-mode-enum")).blocked().reject(QStringLiteral("unsupported mode")).rawInput());
out.append(kase(xTlsOne, QStringLiteral("mode=stream-one"), set(&XrayConfigModel::setXhttpMode, QStringLiteral("stream-one")))
.rawInput());
out.append(kase(xTlsOne, QStringLiteral("mode=empty"), set(&XrayConfigModel::setXhttpMode, QString())).rawInput());
for (const QString &mode : XrayConfigModel::xhttpModeOptions()) {
const ContractCtx ctx = xhttpCtx(QStringLiteral("tls"), mode);
for (const QString &method : XrayConfigModel::xhttpUplinkMethodOptions())
out.append(kase(ctx, QStringLiteral("method=") + method, set(&XrayConfigModel::setXhttpUplinkMethod, method)));
ContractCase get = kase(ctx, QStringLiteral("method=GET"), set(&XrayConfigModel::setXhttpUplinkMethod, QStringLiteral("GET")))
.rule(QStringLiteral("xhttp-get-packet-up"));
if (mode != QLatin1String("Packet-up"))
get.blocked().stricter(QStringLiteral("the generator sends POST instead of GET outside packet-up"));
out.append(get);
}
out.append(kase(xTlsPacket, QStringLiteral("method=get"), set(&XrayConfigModel::setXhttpUplinkMethod, QStringLiteral("get")))
.rawInput());
out.append(kase(xTlsPacket, QStringLiteral("method=foo"), set(&XrayConfigModel::setXhttpUplinkMethod, QStringLiteral("foo")))
.blocked().stricter(QStringLiteral("the core takes any HTTP method token")).rawInput());
for (const QString &mode : XrayConfigModel::xhttpModeOptions()) {
const ContractCtx ctx = xhttpCtx(QStringLiteral("tls"), mode);
for (const QString &pl : XrayConfigModel::xhttpSessionPlacementOptions()) {
out.append(kase(ctx, QStringLiteral("sessionPlacement=") + pl, set(&XrayConfigModel::setXhttpSessionPlacement, pl))
.rule(QStringLiteral("xhttp-placement-enum")));
out.append(kase(ctx, QStringLiteral("seqPlacement=") + pl, set(&XrayConfigModel::setXhttpSeqPlacement, pl))
.rule(QStringLiteral("xhttp-placement-enum")));
}
for (const QString &pl : XrayConfigModel::xhttpUplinkDataPlacementOptions()) {
ContractCase c = kase(ctx, QStringLiteral("uplinkDataPlacement=") + pl,
set(&XrayConfigModel::setXhttpUplinkDataPlacement, pl));
if ((pl == QLatin1String("Header") || pl == QLatin1String("Cookie")) && mode != QLatin1String("Packet-up"))
c.rule(QStringLiteral("xhttp-uplink-data-placement-packet-up"));
out.append(c);
}
}
out.append(kase(xTlsPacket, QStringLiteral("sessionPlacement=foo"),
set(&XrayConfigModel::setXhttpSessionPlacement, QStringLiteral("foo")))
.rule(QStringLiteral("xhttp-placement-enum")).blocked()
.reject(QStringLiteral("unsupported session placement")).rawInput());
out.append(kase(xTlsPacket, QStringLiteral("seqPlacement=foo"), set(&XrayConfigModel::setXhttpSeqPlacement, QStringLiteral("foo")))
.rule(QStringLiteral("xhttp-placement-enum")).blocked()
.reject(QStringLiteral("unsupported seq placement")).rawInput());
out.append(kase(xTlsPacket, QStringLiteral("sessionPlacement=None"),
set(&XrayConfigModel::setXhttpSessionPlacement, QStringLiteral("None")))
.rule(QStringLiteral("xhttp-placement-none-is-path")).rawInput());
out.append(kase(xTlsPacket, QStringLiteral("uplinkDataPlacement=foo"),
set(&XrayConfigModel::setXhttpUplinkDataPlacement, QStringLiteral("foo")))
.blocked().reject(QStringLiteral("unsupported uplink data placement")).rawInput());
const ContractCtx keysCtx = xhttpHeaderKeysCtx();
const QList<QPair<QString, Setter>> keyFields = {
{ QStringLiteral("sessionKey"), &XrayConfigModel::setXhttpSessionKey },
{ QStringLiteral("seqKey"), &XrayConfigModel::setXhttpSeqKey },
{ QStringLiteral("uplinkDataKey"), &XrayConfigModel::setXhttpUplinkDataKey },
{ QStringLiteral("xPaddingKey"), &XrayConfigModel::setXPaddingKey },
{ QStringLiteral("xPaddingHeader"), &XrayConfigModel::setXPaddingHeader },
};
for (const auto &field : keyFields) {
for (const QString key : { QString(), QStringLiteral("x_ok"), QStringLiteral("X-Header"), QString(300, QLatin1Char('k')) })
out.append(kase(keysCtx, field.first + QLatin1Char('=') + shown(key), set(field.second, key)));
for (const QString key : { QStringLiteral("a b"), QStringLiteral("ключ"), QStringLiteral("a:b"),
QStringLiteral("a;b") })
out.append(kase(keysCtx, field.first + QLatin1Char('=') + key, set(field.second, key))
.blocked().stricter(QStringLiteral("the core takes the name, but the request carries no traffic"))
.rawInput());
}
const char *noRange = "Invalid integer range";
const char *disabled = "cannot be disabled";
const RangeExpect xPaddingBytes[10] = {
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Reject, disabled, "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Reject, disabled, "", "" },
{ A::Blocked, C::Reject, disabled, "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
};
addRange(out, xTlsOne, QStringLiteral("xPaddingBytes"), QStringLiteral("xhttp-padding-bytes"),
&XrayConfigModel::setXPaddingBytesMin, &XrayConfigModel::setXPaddingBytesMax, xPaddingBytes);
const char *zeroPost = "0 in the range breaks the packet-up uplink at runtime";
const char *smallPost = "below 1000 bytes per post the packet-up uplink stalls";
const char *negativePost = "the client panics: scMaxEachPostBytes should be bigger than 0";
const RangeExpect scMaxEachPostBytes[10] = {
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Accept, "", zeroPost, "" },
{ A::Blocked, C::Accept, "", zeroPost, "" },
{ A::Blocked, C::Accept, "", smallPost, "" },
{ A::Blocked, C::Accept, "", smallPost, "" },
{ A::Blocked, C::Accept, "", negativePost, "" },
{ A::Blocked, C::Reject, noRange, "", "" },
{ A::Blocked, C::Accept, "", smallPost, "" },
{ A::Blocked, C::Reject, noRange, "", "" },
};
addRange(out, xTlsPacket, QStringLiteral("scMaxEachPostBytes"), QStringLiteral("xhttp-sc-max-each-post-bytes"),
&XrayConfigModel::setXhttpScMaxEachPostBytesMin, &XrayConfigModel::setXhttpScMaxEachPostBytesMax,
scMaxEachPostBytes);
const char *unchecked = "validationErrors does not check this range and the generator writes it as is";
const RangeExpect unvalidated[10] = {
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Reject, noRange, "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Reject, noRange, "", unchecked },
};
addRange(out, xTlsPacket, QStringLiteral("scMinPostsIntervalMs"), QStringLiteral("xhttp-min-posts-interval-zero"),
&XrayConfigModel::setXhttpScMinPostsIntervalMsMin, &XrayConfigModel::setXhttpScMinPostsIntervalMsMax,
unvalidated);
const char *zeroSecs = "0 in the range is only a core warning";
const RangeExpect scStreamUpServerSecs[10] = {
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Accept, "", zeroSecs, "" },
{ A::Blocked, C::Accept, "", zeroSecs, "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Reject, noRange, "", "" },
{ A::Ok, C::Accept, "", "", "" },
{ A::Blocked, C::Reject, noRange, "", "" },
};
addRange(out, xTlsUp, QStringLiteral("scStreamUpServerSecs"), QStringLiteral("xhttp-stream-up-server-secs-zero"),
&XrayConfigModel::setXhttpScStreamUpServerSecsMin, &XrayConfigModel::setXhttpScStreamUpServerSecsMax,
scStreamUpServerSecs);
const QList<std::tuple<QString, Setter, Setter>> xmuxRanges = {
{ QStringLiteral("xmux.maxConcurrency"), &XrayConfigModel::setXmuxMaxConcurrencyMin, &XrayConfigModel::setXmuxMaxConcurrencyMax },
{ QStringLiteral("xmux.maxConnections"), &XrayConfigModel::setXmuxMaxConnectionsMin, &XrayConfigModel::setXmuxMaxConnectionsMax },
{ QStringLiteral("xmux.cMaxReuseTimes"), &XrayConfigModel::setXmuxCMaxReuseTimesMin, &XrayConfigModel::setXmuxCMaxReuseTimesMax },
{ QStringLiteral("xmux.hMaxRequestTimes"), &XrayConfigModel::setXmuxHMaxRequestTimesMin, &XrayConfigModel::setXmuxHMaxRequestTimesMax },
{ QStringLiteral("xmux.hMaxReusableSecs"), &XrayConfigModel::setXmuxHMaxReusableSecsMin, &XrayConfigModel::setXmuxHMaxReusableSecsMax },
};
for (const auto &r : xmuxRanges)
addRange(out, xTlsOne, std::get<0>(r), QStringLiteral("xmux-zero-unlimited"), std::get<1>(r), std::get<2>(r), unvalidated);
out.append(kase(xTlsOne, QStringLiteral("xmux=concurrency+connections"), [](XrayConfigModel &m) {
m.setXmuxEnabled(true);
m.setXmuxMaxConcurrencyMin(QStringLiteral("1"));
m.setXmuxMaxConcurrencyMax(QStringLiteral("1"));
m.setXmuxMaxConnectionsMin(QStringLiteral("1"));
m.setXmuxMaxConnectionsMax(QStringLiteral("1"));
}).rule(QStringLiteral("xmux-exclusive")).blocked().reject(QStringLiteral("maxConnections cannot be specified together")));
out.append(kase(xTlsOne, QStringLiteral("xmux=off+concurrency+connections"), [](XrayConfigModel &m) {
m.setXmuxMaxConcurrencyMin(QStringLiteral("1"));
m.setXmuxMaxConcurrencyMax(QStringLiteral("1"));
m.setXmuxMaxConnectionsMin(QStringLiteral("1"));
m.setXmuxMaxConnectionsMax(QStringLiteral("1"));
m.setXmuxEnabled(false);
}).rule(QStringLiteral("xmux-empty-defaults")));
for (const QString v : { QString(), QStringLiteral("0"), QStringLiteral("1"), QStringLiteral("65536") })
out.append(kase(xTlsPacket, QStringLiteral("uplinkChunkSize=") + shown(v), set(&XrayConfigModel::setXhttpUplinkChunkSize, v)));
for (const QString v : { QStringLiteral("-1"), QStringLiteral("2147483648") })
out.append(kase(xTlsPacket, QStringLiteral("uplinkChunkSize=") + v, set(&XrayConfigModel::setXhttpUplinkChunkSize, v))
.rawInput());
out.append(kase(xTlsPacket, QStringLiteral("uplinkChunkSize=abc"),
set(&XrayConfigModel::setXhttpUplinkChunkSize, QStringLiteral("abc")))
.blocked().stricter(QStringLiteral("the generator writes 0 for a non-number")).rawInput());
for (const QString v : { QStringLiteral("0"), QStringLiteral("1"), QStringLiteral("100000") })
out.append(kase(xTlsPacket, QStringLiteral("scMaxBufferedPosts=") + v, set(&XrayConfigModel::setXhttpScMaxBufferedPosts, v))
.rule(QStringLiteral("xhttp-max-buffered-posts-prealloc")));
out.append(kase(xTlsPacket, QStringLiteral("scMaxBufferedPosts=100001"),
set(&XrayConfigModel::setXhttpScMaxBufferedPosts, QStringLiteral("100001")))
.rule(QStringLiteral("xhttp-max-buffered-posts-prealloc")).blocked()
.stricter(QStringLiteral("the server preallocates a buffer per post")));
out.append(kase(xTlsPacket, QStringLiteral("scMaxBufferedPosts=-1"),
set(&XrayConfigModel::setXhttpScMaxBufferedPosts, QStringLiteral("-1")))
.rule(QStringLiteral("xhttp-max-buffered-posts-prealloc")).blocked()
.stricter(QStringLiteral("a negative value carries no traffic")).rawInput());
out.append(kase(xTlsPacket, QStringLiteral("scMaxBufferedPosts=abc"),
set(&XrayConfigModel::setXhttpScMaxBufferedPosts, QStringLiteral("abc")))
.rule(QStringLiteral("xhttp-max-buffered-posts-prealloc")).blocked()
.stricter(QStringLiteral("the generator drops a non-number")).rawInput());
out.append(kase(xTlsPacket, QStringLiteral("scMaxBufferedPosts=9223372036854775808"),
set(&XrayConfigModel::setXhttpScMaxBufferedPosts, QStringLiteral("9223372036854775808")))
.rule(QStringLiteral("xhttp-max-buffered-posts-prealloc")).rawInput());
for (const QString v : { QStringLiteral("0"), QStringLiteral("30"), QStringLiteral("-1") })
out.append(kase(xTlsOne, QStringLiteral("hKeepAlivePeriod=") + v, set(&XrayConfigModel::setXmuxHKeepAlivePeriod, v)));
out.append(kase(xTlsOne, QStringLiteral("hKeepAlivePeriod=abc"),
set(&XrayConfigModel::setXmuxHKeepAlivePeriod, QStringLiteral("abc")))
.blocked().stricter(QStringLiteral("the generator writes 0 for a non-number")).rawInput());
out.append(kase(xTlsOne, QStringLiteral("hKeepAlivePeriod=9223372036854775808"),
set(&XrayConfigModel::setXmuxHKeepAlivePeriod, QStringLiteral("9223372036854775808")))
.rawInput());
out.append(kase(xTlsOne, QStringLiteral("alpn=http/1.1+hKeepAlivePeriod=30"), [](XrayConfigModel &m) {
m.setAlpn(QStringLiteral("http/1.1"));
m.setXmuxHKeepAlivePeriod(QStringLiteral("30"));
}).rule(QStringLiteral("use-keepalive-h1-alpn")));
out.append(kase(xhttpCtx(QStringLiteral("none"), QStringLiteral("Stream-one")), QStringLiteral("hKeepAlivePeriod=30"),
set(&XrayConfigModel::setXmuxHKeepAlivePeriod, QStringLiteral("30")))
.rule(QStringLiteral("use-keepalive-h1-none")));
struct KcpRow
{
QString field;
Setter setter;
QString ruleId;
QStringList ok;
QStringList clamped;
QStringList rawClamped;
QString why;
};
const QList<KcpRow> kcpRows = {
{ QStringLiteral("mtu"), &XrayConfigModel::setMkcpMtu, QStringLiteral("kcp-mtu"),
{ QString(), QStringLiteral("576"), QStringLiteral("1460") },
{ QStringLiteral("20"), QStringLiteral("21"), QStringLiteral("575"), QStringLiteral("1461") },
{ QStringLiteral("abc"), QStringLiteral("-1") },
QStringLiteral("UI range 576-1460 is narrower than the core (>= 21); the generator clamps") },
{ QStringLiteral("tti"), &XrayConfigModel::setMkcpTti, QStringLiteral("kcp-tti"),
{ QStringLiteral("10"), QStringLiteral("100") },
{ QStringLiteral("9"), QStringLiteral("101"), QStringLiteral("1000"), QStringLiteral("1001") },
{},
QStringLiteral("UI range 10-100 is narrower than the core (10-1000); the generator clamps") },
{ QStringLiteral("cwndMultiplier"), &XrayConfigModel::setMkcpCwndMultiplier, QStringLiteral("kcp-cwnd"),
{ QStringLiteral("1"), QStringLiteral("5000") },
{ QStringLiteral("0") },
{ QStringLiteral("-1") },
QStringLiteral("the generator raises cwndMultiplier to 1") },
{ QStringLiteral("maxSendingWindow"), &XrayConfigModel::setMkcpMaxSendingWindow, QStringLiteral("kcp-sending-window"),
{ QString(), QStringLiteral("1350"), QStringLiteral("2147483647") },
{ QStringLiteral("1349") },
{ QStringLiteral("abc"), QStringLiteral("-1") },
QStringLiteral("the generator omits a window below MTU") },
};
for (const KcpRow &row : kcpRows) {
for (const QString &v : row.ok)
out.append(kase(kcpTls, row.field + QLatin1Char('=') + shown(v), set(row.setter, v)).rule(row.ruleId));
for (const QString &v : row.clamped)
out.append(kase(kcpTls, row.field + QLatin1Char('=') + v, set(row.setter, v)).rule(row.ruleId).blocked()
.stricter(row.why));
for (const QString &v : row.rawClamped)
out.append(kase(kcpTls, row.field + QLatin1Char('=') + v, set(row.setter, v)).rule(row.ruleId).blocked()
.stricter(row.why).rawInput());
}
out.append(kase(kcpTls, QStringLiteral("cwndMultiplier=5001"), set(&XrayConfigModel::setMkcpCwndMultiplier, QStringLiteral("5001")))
.rule(QStringLiteral("kcp-cwnd-overflow")).blocked()
.stricter(QStringLiteral("large cwndMultiplier overflows the window at runtime")));
for (const QPair<QString, Setter> &cap : QList<QPair<QString, Setter>> {
{ QStringLiteral("uplinkCapacity"), &XrayConfigModel::setMkcpUplinkCapacity },
{ QStringLiteral("downlinkCapacity"), &XrayConfigModel::setMkcpDownlinkCapacity } }) {
for (const QString v : { QStringLiteral("0"), QStringLiteral("4095") })
out.append(kase(kcpTls, cap.first + QLatin1Char('=') + v, set(cap.second, v)).rule(QStringLiteral("kcp-capacity-overflow")));
out.append(kase(kcpTls, cap.first + QStringLiteral("=4096"), set(cap.second, QStringLiteral("4096")))
.rule(QStringLiteral("kcp-capacity-overflow")).blocked()
.stricter(QStringLiteral("capacity above 4095 overflows at runtime")));
out.append(kase(kcpTls, cap.first + QStringLiteral("=-1"), set(cap.second, QStringLiteral("-1")))
.rule(QStringLiteral("kcp-capacity-overflow")).blocked().reject().rawInput());
out.append(kase(kcpTls, cap.first + QStringLiteral("=abc"), set(cap.second, QStringLiteral("abc")))
.rule(QStringLiteral("kcp-capacity-overflow")).blocked()
.stricter(QStringLiteral("the generator writes 0 for a non-number")).rawInput());
}
for (const QString &pl : XrayConfigModel::xPaddingPlacementOptions()) {
for (const QString &method : XrayConfigModel::xPaddingMethodOptions())
out.append(kase(xTlsOne, QStringLiteral("xPadding=%1/%2").arg(pl, method), [pl, method](XrayConfigModel &m) {
m.setXPaddingObfsMode(true);
m.setXPaddingPlacement(pl);
m.setXPaddingMethod(method);
}).rule(QStringLiteral("xhttp-padding-placement")));
}
out.append(kase(xTlsOne, QStringLiteral("xPaddingPlacement=foo"), [](XrayConfigModel &m) {
m.setXPaddingObfsMode(true);
m.setXPaddingPlacement(QStringLiteral("foo"));
}).rule(QStringLiteral("xhttp-padding-placement")).blocked().reject(QStringLiteral("unsupported padding placement"))
.rawInput());
out.append(kase(xTlsOne, QStringLiteral("xPaddingMethod=foo"), [](XrayConfigModel &m) {
m.setXPaddingObfsMode(true);
m.setXPaddingMethod(QStringLiteral("foo"));
}).rule(QStringLiteral("xhttp-padding-method")).blocked().reject(QStringLiteral("unsupported padding method"))
.rawInput());
out.append(kase(xTlsOne, QStringLiteral("obfs off+xPaddingPlacement=foo"), [](XrayConfigModel &m) {
m.setXPaddingObfsMode(false);
m.setXPaddingPlacement(QStringLiteral("foo"));
}).rule(QStringLiteral("use-padding-obfs-fields")).rawInput());
out.append(kase(xTlsOne, QStringLiteral("obfs off+xPaddingBytes=200..400"), [](XrayConfigModel &m) {
m.setXPaddingObfsMode(false);
m.setXPaddingBytesMin(QStringLiteral("200"));
m.setXPaddingBytesMax(QStringLiteral("400"));
}).rule(QStringLiteral("use-padding-bytes-without-obfs")));
out.append(kase(xTlsOne, QStringLiteral("xPaddingMethod=Random"), set(&XrayConfigModel::setXPaddingMethod, QStringLiteral("Random")))
.rawInput());
const QString vision = QStringLiteral("xtls-rprx-vision");
for (const ContractCtx &ctx : { rawTlsNoFlow, rawCtx(QStringLiteral("reality"), QString()) }) {
for (const QString &flow : XrayConfigModel::flowOptions()) {
ContractCase c = kase(ctx, QStringLiteral("flow=") + shown(flow), set(&XrayConfigModel::setFlow, flow));
if (flow.endsWith(QLatin1String("-udp443")))
c.rule(ctx.security == QLatin1String("tls") ? QStringLiteral("vision-udp443")
: QStringLiteral("vision-udp-via-xudp"));
out.append(c);
}
out.append(kase(ctx, QStringLiteral("flow=foo"), set(&XrayConfigModel::setFlow, QStringLiteral("foo")))
.blocked().reject(QStringLiteral("doesn't support")).rawInput());
}
out.append(kase(rawNone, QStringLiteral("flow=vision"), set(&XrayConfigModel::setFlow, vision))
.rule(QStringLiteral("vision-needs-security")));
out.append(kase(xTlsOne, QStringLiteral("flow=vision"), set(&XrayConfigModel::setFlow, vision))
.rule(QStringLiteral("vision-needs-raw")));
out.append(kase(kcpTls, QStringLiteral("flow=vision"), set(&XrayConfigModel::setFlow, vision))
.rule(QStringLiteral("vision-needs-raw")));
out.append(kase(rawNone, QStringLiteral("flow=foo"), set(&XrayConfigModel::setFlow, QStringLiteral("foo")))
.blocked().stricter(QStringLiteral("the generator drops flow without TLS or REALITY")).rawInput());
out.append(kase(xTlsOne, QStringLiteral("flow=foo"), set(&XrayConfigModel::setFlow, QStringLiteral("foo")))
.blocked().stricter(QStringLiteral("the generator drops flow outside raw")).rawInput());
out.append(kase(rawTls, QStringLiteral("security=foo"), set(&XrayConfigModel::setSecurity, QStringLiteral("foo")))
.blocked().reject(QStringLiteral("Unknown security")).rawInput());
out.append(kase(rawReality, QStringLiteral("transport=foo"), set(&XrayConfigModel::setTransport, QStringLiteral("foo")))
.blocked().stricter(QStringLiteral("the generator falls back to raw")).rawInput());
out.append(kase(kcpTls, QStringLiteral("security=reality"), set(&XrayConfigModel::setSecurity, QStringLiteral("reality")))
.rule(QStringLiteral("reality-network")).blocked().reject(QStringLiteral("vless without TLS")));
return out;
}
inline const QList<ContractCase> &contractCases()
{
static const QList<ContractCase> all = buildContractCases();
return all;
}
}
#endif
+449
View File
@@ -0,0 +1,449 @@
#ifndef XRAY_EXPLORE_CASES_H
#define XRAY_EXPLORE_CASES_H
#include <QJsonObject>
#include <QList>
#include <QPair>
#include <QString>
#include <QStringList>
#include <functional>
#include "ui/models/protocols/xrayConfigModel.h"
#include "utils/xrayMatrix.h"
namespace xrayexplore
{
using Edit = std::function<void(XrayConfigModel &)>;
using ClientEdit = std::function<void(QJsonObject &)>;
using TextSetter = void (XrayConfigModel::*)(const QString &);
using Range = QPair<QString, QString>;
struct ExploreCase
{
QString group;
QString name;
Edit edit;
ClientEdit clientEdit;
};
struct Carrier
{
QString name;
QString transport;
QString security;
QString flow;
QString mode;
};
inline void apply(XrayConfigModel &m, const Carrier &c)
{
m.setTransport(c.transport);
m.setSecurity(c.security);
m.setFlow(c.flow);
if (!c.mode.isEmpty())
m.setXhttpMode(c.mode);
}
inline Carrier raw(const QString &security, const QString &flow)
{
const QString flowName = flow.isEmpty() ? QStringLiteral("noflow") : flow;
return { QStringLiteral("raw/%1/%2").arg(security, flowName), QStringLiteral("raw"), security, flow, {} };
}
inline Carrier xhttp(const QString &security, const QString &mode)
{
return { QStringLiteral("xhttp/%1/%2").arg(security, mode), QStringLiteral("xhttp"), security, {}, mode };
}
inline Carrier mkcp()
{
return { QStringLiteral("mkcp/tls"), QStringLiteral("mkcp"), QStringLiteral("tls"), {}, {} };
}
inline QList<Carrier> carriers(const QString &security)
{
QList<Carrier> out;
for (const QString &flow : XrayConfigModel::flowOptions())
out << raw(security, flow);
for (const QString &mode : XrayConfigModel::xhttpModeOptions())
out << xhttp(security, mode);
return out;
}
inline QString shown(const QString &value)
{
return QStringLiteral("'%1'").arg(value);
}
inline QString shown(const Range &range)
{
return QStringLiteral("'%1'..'%2'").arg(range.first, range.second);
}
class Builder
{
public:
QList<ExploreCase> cases;
void add(const QString &group, const Carrier &carrier, const QString &what, const Edit &edit,
const ClientEdit &clientEdit = {})
{
const QString name = what.isEmpty() ? QStringLiteral("%1/%2").arg(group, carrier.name)
: QStringLiteral("%1/%2 %3").arg(group, carrier.name, what);
cases.append({ group, name,
[carrier, edit](XrayConfigModel &m) {
apply(m, carrier);
if (edit)
edit(m);
},
clientEdit });
}
void text(const QString &group, const QList<Carrier> &on, const QString &field, TextSetter setter,
const QStringList &values, const Edit &prepare = {})
{
for (const Carrier &carrier : on) {
for (const QString &value : values) {
add(group, carrier, field + QLatin1Char('=') + shown(value), [prepare, setter, value](XrayConfigModel &m) {
if (prepare)
prepare(m);
(m.*setter)(value);
});
}
}
}
void range(const QString &group, const QList<Carrier> &on, const QString &field, TextSetter setMin,
TextSetter setMax, const QList<Range> &values, const Edit &prepare = {})
{
for (const Carrier &carrier : on) {
for (const Range &value : values) {
add(group, carrier, field + QLatin1Char('=') + shown(value),
[prepare, setMin, setMax, value](XrayConfigModel &m) {
if (prepare)
prepare(m);
(m.*setMin)(value.first);
(m.*setMax)(value.second);
});
}
}
}
};
inline QString longDomain(int length)
{
QStringList labels;
int left = length - 4;
char letter = 'a';
while (left > 0) {
const int size = qMin(63, left);
labels << QString(size, QLatin1Char(letter++));
left -= size + 1;
}
return labels.join(QLatin1Char('.')) + QStringLiteral(".com");
}
inline void baseGroup(Builder &b)
{
const QString g = QStringLiteral("base");
for (const QString &security : XrayConfigModel::securityOptions()) {
for (const Carrier &carrier : carriers(security))
b.add(g, carrier, {}, {});
}
b.add(g, mkcp(), {}, {});
b.add(g, mkcp(), QStringLiteral("then security=reality"),
[](XrayConfigModel &m) { m.setSecurity(QStringLiteral("reality")); });
b.add(g, raw(QStringLiteral("reality"), QStringLiteral("xtls-rprx-vision")),
QStringLiteral("then transport=mkcp"), [](XrayConfigModel &m) { m.setTransport(QStringLiteral("mkcp")); });
b.text(g, { raw(QStringLiteral("tls"), {}) }, QStringLiteral("security"), &XrayConfigModel::setSecurity,
{ "none", "", "xtls", "TLS", "garbage" });
}
inline void tlsFingerprintGrid(Builder &b, const QString &g, const QList<Carrier> &bases)
{
QStringList fingerprints = XrayConfigModel::fingerprintOptions();
fingerprints << QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint);
fingerprints.removeDuplicates();
for (const Carrier &carrier : bases) {
for (const QString &fp : fingerprints) {
for (const QString &alpn : XrayConfigModel::alpnOptions()) {
b.add(g, carrier, QStringLiteral("fingerprint=%1 alpn=%2").arg(fp, alpn), [fp, alpn](XrayConfigModel &m) {
m.setFingerprint(fp);
m.setAlpn(alpn);
});
}
}
}
}
inline void tlsGroup(Builder &b)
{
const QString g = QStringLiteral("tls");
tlsFingerprintGrid(b, g,
{ raw("tls", ""), raw("tls", "xtls-rprx-vision"), raw("tls", "xtls-rprx-vision-udp443"),
xhttp("tls", "Stream-one") });
b.text(g, { mkcp() }, QStringLiteral("alpn"), &XrayConfigModel::setAlpn, XrayConfigModel::alpnOptions());
b.text(g, { raw("tls", "") }, QStringLiteral("alpn"), &XrayConfigModel::setAlpn,
{ "HTTP/2", "h2, http/1.1", "http/1.1,h2", "h3", "foo", "" });
b.text(g, { raw("tls", "") }, QStringLiteral("fingerprint"), &XrayConfigModel::setFingerprint,
{ "", "foo", " Chrome " });
}
inline void tlsXhttpGroup(Builder &b)
{
tlsFingerprintGrid(b, QStringLiteral("tlsxhttp"),
{ xhttp("tls", "Auto"), xhttp("tls", "Packet-up"), xhttp("tls", "Stream-up") });
}
inline void realityGroup(Builder &b)
{
const QString g = QStringLiteral("reality");
QStringList fingerprints = XrayConfigModel::realityFingerprintOptions();
for (const xraytest::RealityFingerprintCase &fp : xraytest::realityFingerprintCases()) {
if (fp.hello == xraytest::RealityHello::Connects)
fingerprints << fp.name;
}
fingerprints.removeDuplicates();
b.text(g, carriers(QStringLiteral("reality")), QStringLiteral("fingerprint"), &XrayConfigModel::setFingerprint,
fingerprints);
b.text(g, { raw("reality", "xtls-rprx-vision") }, QStringLiteral("fingerprint"),
&XrayConfigModel::setFingerprint, xraytest::legacyRealityFingerprints());
}
inline void oldClientGroup(Builder &b)
{
const QString g = QStringLiteral("oldclient");
for (const Carrier &carrier : { raw("reality", "xtls-rprx-vision"), xhttp("reality", "Auto") }) {
for (const xraytest::RealityFingerprintCase &fp : xraytest::realityFingerprintCases()) {
const QString name = fp.name;
b.add(g, carrier, QStringLiteral("client fingerprint=%1").arg(name), {},
[name](QJsonObject &client) { xraytest::setClientRealityFingerprint(client, name); });
}
}
}
inline void sniGroup(Builder &b)
{
const QString g = QStringLiteral("sni");
const QList<Carrier> bases = { raw("tls", ""), xhttp("tls", "Stream-one"), mkcp(),
raw("reality", "xtls-rprx-vision"), xhttp("reality", "Auto") };
const QStringList values = { "www.googletagmanager.com",
"www.microsoft.com",
"www.apple.com",
"dl.google.com",
"www.cloudflare.com",
"github.com",
"www.bing.com",
"yandex.ru",
"",
"1.1.1.1",
"a_b.example.com",
longDomain(253),
"amnezia-explore-nonexistent.com",
"neverssl.com",
"*.example.com",
"localhost",
"1234",
longDomain(254),
"xn--e1afmkfd.xn--p1ai",
"example.com.",
QString(63, QLatin1Char('a')) + QStringLiteral(".example.com"),
"2606:4700::1111",
" example.com " };
b.text(g, bases, QStringLiteral("sni"), &XrayConfigModel::setSni, values);
const Carrier reality = raw("reality", "xtls-rprx-vision");
b.add(g, reality, QStringLiteral("sni='www.microsoft.com' site='www.googletagmanager.com'"),
[](XrayConfigModel &m) {
m.setSni(QStringLiteral("www.microsoft.com"));
m.setSite(QStringLiteral("www.googletagmanager.com"));
});
b.add(g, reality, QStringLiteral("sni='' site='www.microsoft.com'"), [](XrayConfigModel &m) {
m.setSni(QString());
m.setSite(QStringLiteral("www.microsoft.com"));
});
}
inline void portGroup(Builder &b)
{
const QString g = QStringLiteral("port");
b.text(g, { raw("reality", "xtls-rprx-vision"), mkcp() }, QStringLiteral("port"), &XrayConfigModel::setPort,
{ "2053", "65535", "1" });
b.text(g, { raw("reality", "xtls-rprx-vision") }, QStringLiteral("port"), &XrayConfigModel::setPort,
{ "0", "65536", "-1", "", "abc" });
}
inline void xhttpGroup(Builder &b)
{
const QString g = QStringLiteral("xhttp");
QList<Carrier> everyMode;
for (const QString &security : XrayConfigModel::securityOptions()) {
for (const QString &mode : XrayConfigModel::xhttpModeOptions())
everyMode << xhttp(security, mode);
}
auto inMode = [](const QString &mode) {
QList<Carrier> out;
for (const QString &security : XrayConfigModel::securityOptions())
out << xhttp(security, mode);
return out;
};
const QList<Carrier> streamOne = inMode(QStringLiteral("Stream-one"));
const QList<Carrier> packetUp = inMode(QStringLiteral("Packet-up"));
const QList<Carrier> streamUp = inMode(QStringLiteral("Stream-up"));
const QList<Range> xmuxRanges = { { "0", "0" }, { "1", "1" }, { "16", "32" }, { "0", "4" },
{ "4", "0" }, { "32", "16" }, { "abc", "5" } };
auto placement = [](TextSetter setter, const QString &value) {
return [setter, value](XrayConfigModel &m) { (m.*setter)(value); };
};
b.text(g, streamOne, QStringLiteral("mode"), &XrayConfigModel::setXhttpMode, { "", "packet-up", "foo" });
b.text(g, streamOne, QStringLiteral("host"), &XrayConfigModel::setXhttpHost,
{ "", "www.googletagmanager.com", "example.com", "1.2.3.4", "::1", "a,b", "123", "bad host" });
b.text(g, streamOne, QStringLiteral("path"), &XrayConfigModel::setXhttpPath,
{ "", "/", "/abc", "abc", " /abc ", "/a/b", "/x?y=1", "/%41", "/a b" });
QStringList methods = XrayConfigModel::xhttpUplinkMethodOptions();
methods << "post" << "GET" << "OPTIONS" << "FOO";
b.text(g, everyMode, QStringLiteral("uplinkMethod"), &XrayConfigModel::setXhttpUplinkMethod, methods);
for (const Carrier &carrier : everyMode) {
for (const bool grpc : { true, false }) {
for (const bool sse : { true, false }) {
const QString yes = QStringLiteral("true");
const QString no = QStringLiteral("false");
b.add(g, carrier, QStringLiteral("disableGrpc=%1 disableSse=%2").arg(grpc ? yes : no, sse ? yes : no),
[grpc, sse](XrayConfigModel &m) {
m.setXhttpDisableGrpc(grpc);
m.setXhttpDisableSse(sse);
});
}
}
}
QStringList sessionPlacements = XrayConfigModel::xhttpSessionPlacementOptions();
sessionPlacements << "" << "Body" << "foo";
b.text(g, everyMode, QStringLiteral("sessionPlacement"), &XrayConfigModel::setXhttpSessionPlacement,
sessionPlacements);
QStringList seqPlacements = XrayConfigModel::xhttpSeqPlacementOptions();
seqPlacements << "" << "Body" << "foo";
b.text(g, everyMode, QStringLiteral("seqPlacement"), &XrayConfigModel::setXhttpSeqPlacement, seqPlacements);
const QStringList keys = { "", "sid", "X-Sid", "a-b_1", "a b", "a:b" };
b.text(g, packetUp, QStringLiteral("sessionPlacement=Header sessionKey"), &XrayConfigModel::setXhttpSessionKey,
keys, placement(&XrayConfigModel::setXhttpSessionPlacement, QStringLiteral("Header")));
b.text(g, packetUp, QStringLiteral("seqPlacement=Header seqKey"), &XrayConfigModel::setXhttpSeqKey, keys,
placement(&XrayConfigModel::setXhttpSeqPlacement, QStringLiteral("Header")));
QStringList dataPlacements = XrayConfigModel::xhttpUplinkDataPlacementOptions();
dataPlacements << "" << "foo";
b.text(g, everyMode, QStringLiteral("uplinkDataPlacement"), &XrayConfigModel::setXhttpUplinkDataPlacement,
dataPlacements);
const Edit headerData = placement(&XrayConfigModel::setXhttpUplinkDataPlacement, QStringLiteral("Header"));
b.text(g, packetUp, QStringLiteral("uplinkDataPlacement=Header uplinkDataKey"),
&XrayConfigModel::setXhttpUplinkDataKey, { "", "xd", "X-Data", "a:b" }, headerData);
b.text(g, packetUp, QStringLiteral("uplinkDataPlacement=Header uplinkChunkSize"),
&XrayConfigModel::setXhttpUplinkChunkSize, { "", "0", "1", "63", "64", "4096", "65536", "abc" },
headerData);
b.text(g, packetUp, QStringLiteral("scMaxBufferedPosts"), &XrayConfigModel::setXhttpScMaxBufferedPosts,
{ "", "0", "1", "30", "1000", "-1", "abc" });
b.range(g, packetUp, QStringLiteral("scMaxEachPostBytes"), &XrayConfigModel::setXhttpScMaxEachPostBytesMin,
&XrayConfigModel::setXhttpScMaxEachPostBytesMax,
{ { "1000000", "1000000" }, { "100", "100" }, { "1", "1" }, { "0", "0" }, { "0", "1000" },
{ "1000000", "0" }, { "2000", "1000" }, { "", "5000" }, { "abc", "5" } });
b.range(g, packetUp, QStringLiteral("scMinPostsIntervalMs"), &XrayConfigModel::setXhttpScMinPostsIntervalMsMin,
&XrayConfigModel::setXhttpScMinPostsIntervalMsMax,
{ { "100", "800" }, { "0", "0" }, { "0", "500" }, { "10", "10" }, { "800", "100" }, { "abc", "5" } });
b.range(g, streamUp, QStringLiteral("scStreamUpServerSecs"), &XrayConfigModel::setXhttpScStreamUpServerSecsMin,
&XrayConfigModel::setXhttpScStreamUpServerSecsMax,
{ { "1", "100" }, { "0", "0" }, { "0", "5" }, { "1", "0" }, { "5", "5" }, { "100", "1" } });
for (const Carrier &carrier : streamOne) {
b.add(g, carrier, QStringLiteral("xPaddingObfsMode=false"),
[](XrayConfigModel &m) { m.setXPaddingObfsMode(false); });
}
b.range(g, streamOne, QStringLiteral("xPaddingBytes"), &XrayConfigModel::setXPaddingBytesMin,
&XrayConfigModel::setXPaddingBytesMax,
{ { "", "" }, { "100", "1000" }, { "0", "0" }, { "0", "1" }, { "0", "1000" }, { "1", "1" },
{ "500", "100" }, { "100", "" }, { "-1", "5" } });
b.text(g, streamOne, QStringLiteral("xPaddingKey"), &XrayConfigModel::setXPaddingKey, { "", "pad", "X-Pad", "a b" });
b.text(g, streamOne, QStringLiteral("xPaddingPlacement=Header xPaddingHeader"), &XrayConfigModel::setXPaddingHeader,
{ "", "pad", "X-Pad", "a b" }, placement(&XrayConfigModel::setXPaddingPlacement, QStringLiteral("Header")));
for (const QString &paddingPlacement : XrayConfigModel::xPaddingPlacementOptions()) {
b.text(g, streamOne, QStringLiteral("xPaddingPlacement=%1 xPaddingMethod").arg(paddingPlacement),
&XrayConfigModel::setXPaddingMethod, XrayConfigModel::xPaddingMethodOptions(),
placement(&XrayConfigModel::setXPaddingPlacement, paddingPlacement));
}
b.text(g, streamOne, QStringLiteral("xPaddingPlacement"), &XrayConfigModel::setXPaddingPlacement,
{ "queryInHeader", "Body", "foo" });
b.text(g, streamOne, QStringLiteral("xPaddingMethod"), &XrayConfigModel::setXPaddingMethod, { "Random", "foo" });
for (const Carrier &carrier : streamOne) {
b.add(g, carrier, QStringLiteral("xmuxEnabled=false"), [](XrayConfigModel &m) { m.setXmuxEnabled(false); });
b.add(g, carrier, QStringLiteral("xmux maxConcurrency='1'..'1' maxConnections='1'..'1'"), [](XrayConfigModel &m) {
m.setXmuxEnabled(true);
m.setXmuxMaxConcurrencyMin(QStringLiteral("1"));
m.setXmuxMaxConcurrencyMax(QStringLiteral("1"));
m.setXmuxMaxConnectionsMin(QStringLiteral("1"));
m.setXmuxMaxConnectionsMax(QStringLiteral("1"));
});
}
const Edit xmuxOn = [](XrayConfigModel &m) { m.setXmuxEnabled(true); };
b.range(g, streamOne, QStringLiteral("xmux maxConcurrency"), &XrayConfigModel::setXmuxMaxConcurrencyMin,
&XrayConfigModel::setXmuxMaxConcurrencyMax, xmuxRanges, xmuxOn);
b.range(g, streamOne, QStringLiteral("xmux maxConnections"), &XrayConfigModel::setXmuxMaxConnectionsMin,
&XrayConfigModel::setXmuxMaxConnectionsMax, xmuxRanges, xmuxOn);
b.range(g, streamOne, QStringLiteral("xmux cMaxReuseTimes"), &XrayConfigModel::setXmuxCMaxReuseTimesMin,
&XrayConfigModel::setXmuxCMaxReuseTimesMax, xmuxRanges, xmuxOn);
b.range(g, streamOne, QStringLiteral("xmux hMaxRequestTimes"), &XrayConfigModel::setXmuxHMaxRequestTimesMin,
&XrayConfigModel::setXmuxHMaxRequestTimesMax, xmuxRanges, xmuxOn);
b.range(g, streamOne, QStringLiteral("xmux hMaxReusableSecs"), &XrayConfigModel::setXmuxHMaxReusableSecsMin,
&XrayConfigModel::setXmuxHMaxReusableSecsMax, xmuxRanges, xmuxOn);
b.text(g, streamOne, QStringLiteral("xmux hKeepAlivePeriod"), &XrayConfigModel::setXmuxHKeepAlivePeriod,
{ "", "0", "30", "-1", "abc" }, xmuxOn);
}
inline void mkcpGroup(Builder &b)
{
const QString g = QStringLiteral("mkcp");
const QList<Carrier> on = { mkcp() };
b.text(g, on, QStringLiteral("tti"), &XrayConfigModel::setMkcpTti,
{ "", "50", "20", "10", "100", "9", "101", "0", "1000", "abc", "-1" });
b.text(g, on, QStringLiteral("mtu"), &XrayConfigModel::setMkcpMtu,
{ "", "1350", "576", "1460", "575", "1461", "0", "21", "abc" });
const QStringList capacities = { "", "5", "0", "1", "4095", "4096", "5000", "abc", "9999999999", "-1" };
b.text(g, on, QStringLiteral("uplinkCapacity"), &XrayConfigModel::setMkcpUplinkCapacity, capacities);
b.text(g, on, QStringLiteral("downlinkCapacity"), &XrayConfigModel::setMkcpDownlinkCapacity, capacities);
b.text(g, on, QStringLiteral("cwndMultiplier"), &XrayConfigModel::setMkcpCwndMultiplier,
{ "", "1", "2", "5000", "5001", "6000", "0", "-5", "abc" });
b.text(g, on, QStringLiteral("maxSendingWindow"), &XrayConfigModel::setMkcpMaxSendingWindow,
{ "", "1350", "1349", "0", "2097152", "abc", "-1" });
b.add(g, mkcp(), QStringLiteral("mtu='1460' maxSendingWindow='1400'"), [](XrayConfigModel &m) {
m.setMkcpMtu(QStringLiteral("1460"));
m.setMkcpMaxSendingWindow(QStringLiteral("1400"));
});
}
inline QStringList groupNames()
{
return { "base", "tls", "tlsxhttp", "reality", "oldclient", "sni", "xhttp", "mkcp", "port" };
}
inline QList<ExploreCase> cases(const QStringList &groups)
{
Builder b;
const QList<QPair<QString, std::function<void(Builder &)>>> all = {
{ "base", baseGroup }, { "tls", tlsGroup },
{ "tlsxhttp", tlsXhttpGroup }, { "reality", realityGroup },
{ "oldclient", oldClientGroup }, { "sni", sniGroup },
{ "xhttp", xhttpGroup }, { "mkcp", mkcpGroup },
{ "port", portGroup },
};
for (const auto &group : all) {
if (groups.contains(group.first))
group.second(b);
}
return b.cases;
}
}
#endif
+95 -4
View File
@@ -9,6 +9,8 @@
#include <QString>
#include <QStringList>
#include <functional>
#include "core/models/protocols/xrayProtocolConfig.h"
namespace xraytest
@@ -20,6 +22,89 @@ namespace xraytest
bool allowedByUi = true;
};
enum class RealityHello {
Connects,
RealCertificate,
NoTls13,
ConfigRejected,
ClientPanics,
Unreliable,
};
struct RealityFingerprintCase
{
QString name;
RealityHello hello;
};
inline QList<RealityFingerprintCase> realityFingerprintCases()
{
QList<RealityFingerprintCase> cases;
auto add = [&cases](RealityHello hello, const QStringList &names) {
for (const QString &name : names)
cases.append({ name, hello });
};
add(RealityHello::Connects,
{ "chrome", "firefox", "safari", "hellochrome_131", "hellochrome_133", "hellochrome_auto",
"hellofirefox_148", "hellofirefox_auto", "hellosafari_26_3", "hellosafari_auto" });
add(RealityHello::RealCertificate,
{ "ios", "edge", "qq", "hellofirefox_120", "hellochrome_120",
"helloios_13", "helloios_14", "helloedge_106", "hello360_11_0", "helloqq_11_1",
"hellofirefox_63", "hellofirefox_65", "hellofirefox_99", "hellofirefox_102", "hellofirefox_105",
"hellochrome_70", "hellochrome_72", "hellochrome_83", "hellochrome_87", "hellochrome_96",
"hellochrome_100", "hellochrome_102", "hellochrome_106_shuffle", "helloios_auto", "helloedge_85",
"helloedge_auto", "hellosafari_16_0", "helloqq_auto", "hellochrome_115_pq", "hellochrome_120_pq" });
add(RealityHello::NoTls13,
{ "android", "360", "hellofirefox_55", "hellofirefox_56", "hellochrome_58", "hellochrome_62",
"helloios_11_1", "helloios_12_1", "helloandroid_11_okhttp", "hello360_auto", "hello360_7_5" });
add(RealityHello::ConfigRejected, { "unsafe", "hellogolang" });
add(RealityHello::ClientPanics,
{ "hellochrome_100_psk", "hellochrome_112_psk_shuf", "hellochrome_114_padding_psk_shuf",
"hellochrome_115_pq_psk" });
add(RealityHello::Unreliable,
{ "random", "randomized", "randomizednoalpn", "hellorandomized", "hellorandomizedalpn",
"hellorandomizednoalpn" });
return cases;
}
inline QStringList legacyRealityFingerprints()
{
return { "hellochrome_120", "ios", "android", "edge", "360", "qq",
"random", "", " Chrome ", "hellogolang", "unsafe", "garbage",
"hellochrome_100_psk" };
}
inline void editStream(QJsonObject &config, const char *bounds, const std::function<void(QJsonObject &)> &edit)
{
QJsonArray list = config.value(QString::fromLatin1(bounds)).toArray();
for (int i = 0; i < list.size(); ++i) {
QJsonObject bound = list.at(i).toObject();
if (bound.value("protocol").toString() != QLatin1String("vless"))
continue;
QJsonObject stream = bound.value("streamSettings").toObject();
edit(stream);
bound["streamSettings"] = stream;
list[i] = bound;
}
config[QString::fromLatin1(bounds)] = list;
}
inline void editSection(QJsonObject &config, const char *bounds, const char *section,
const std::function<void(QJsonObject &)> &edit)
{
editStream(config, bounds, [&](QJsonObject &stream) {
QJsonObject settings = stream.value(QString::fromLatin1(section)).toObject();
edit(settings);
stream[QString::fromLatin1(section)] = settings;
});
}
inline void setClientRealityFingerprint(QJsonObject &client, const QString &fingerprint)
{
editSection(client, "outbounds", "realitySettings",
[&fingerprint](QJsonObject &reality) { reality["fingerprint"] = fingerprint; });
}
inline QString clientId()
{
return QStringLiteral("11111111-2222-4333-8444-555555555555");
@@ -59,7 +144,7 @@ namespace xraytest
inline QList<MatrixCase> matrix()
{
const QStringList transports = { "raw", "xhttp", "mkcp" };
const QStringList securities = { "none", "tls", "reality" };
const QStringList securities = { "tls", "reality" };
const QStringList flows = { "", "xtls-rprx-vision", "xtls-rprx-vision-udp443" };
const QStringList modes = { "Auto", "Packet-up", "Stream-up", "Stream-one" };
const QStringList placements = { "Body", "Auto", "Header", "Cookie" };
@@ -71,7 +156,8 @@ namespace xraytest
const QString flowName = flow.isEmpty() ? QStringLiteral("noflow") : flow;
if (transport != QLatin1String("xhttp")) {
cases.append({ QStringLiteral("%1/%2/%3").arg(transport, security, flowName),
baseConfig(transport, security, flow), true });
baseConfig(transport, security, flow),
transport != QLatin1String("mkcp") || security != QLatin1String("reality") });
continue;
}
for (const QString &mode : modes) {
@@ -161,15 +247,20 @@ namespace xraytest
return config.value(QStringLiteral("inbounds")).toArray().first().toObject();
}
inline QJsonObject vlessOutbound(const QJsonObject &config)
inline QJsonObject vlessEntry(const QJsonObject &config, const QString &list)
{
for (const QJsonValue &v : config.value(QStringLiteral("outbounds")).toArray()) {
for (const QJsonValue &v : config.value(list).toArray()) {
if (v.toObject().value(QStringLiteral("protocol")).toString() == QLatin1String("vless"))
return v.toObject();
}
return {};
}
inline QJsonObject vlessOutbound(const QJsonObject &config)
{
return vlessEntry(config, QStringLiteral("outbounds"));
}
inline QJsonObject streamOf(const QJsonObject &bound)
{
return bound.value(QStringLiteral("streamSettings")).toObject();
+182
View File
@@ -0,0 +1,182 @@
#ifndef XRAY_TEST_RUNTIME_H
#define XRAY_TEST_RUNTIME_H
#include <QByteArray>
#include <QElapsedTimer>
#include <QHash>
#include <QHostAddress>
#include <QProcess>
#include <QRegularExpression>
#include <QString>
#include <QStringList>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTest>
#include <QtEndian>
#include <functional>
namespace xrayruntime
{
inline int freeLocalPort()
{
QTcpServer server;
server.listen(QHostAddress::LocalHost, 0);
const int port = server.serverPort();
server.close();
return port;
}
inline QString collapsedLog(const QString &output)
{
static const QRegularExpression entry(QStringLiteral(R"(\[(?:Error|Warning)\]\s+(?:\[\d+\]\s+)?(.+)$)"));
static const QRegularExpression address(QStringLiteral(R"(\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?)"));
QStringList messages;
QHash<QString, int> counts;
for (const QString &line : output.split(QLatin1Char('\n'))) {
const QRegularExpressionMatch match = entry.match(line.trimmed());
if (!match.hasMatch())
continue;
QString message = match.captured(1);
message.replace(address, QStringLiteral("<ip>"));
if (message.startsWith(QLatin1String("core: Xray ")) && message.endsWith(QLatin1String(" started")))
continue;
if (!counts.contains(message))
messages << message;
++counts[message];
}
QStringList collapsed;
for (const QString &message : messages)
collapsed << (counts.value(message) > 1
? QStringLiteral("%1 x%2").arg(message, QString::number(counts.value(message)))
: message);
return collapsed.join(QStringLiteral(" | "));
}
inline QString failureSummary(const QString &title, const QStringList &failures, int casesRun)
{
if (failures.isEmpty())
return QStringLiteral("%1: all %2 cases passed").arg(title, QString::number(casesRun));
return QStringLiteral("%1: %2 of %3 cases failed\n %4")
.arg(title, QString::number(failures.size()), QString::number(casesRun),
failures.join(QStringLiteral("\n ")));
}
inline QString version(const QString &bin)
{
QProcess process;
process.start(bin, { QStringLiteral("version") });
process.waitForFinished(30000);
static const QRegularExpression number(QStringLiteral(R"(Xray\s+v?(\d+\.\d+\.\d+))"));
return number.match(QString::fromUtf8(process.readAllStandardOutput())).captured(1);
}
inline bool versionAtLeast(const QString &version, const QString &minimum)
{
const QStringList have = version.split(QLatin1Char('.'));
const QStringList need = minimum.split(QLatin1Char('.'));
for (int i = 0; i < need.size(); ++i) {
const int h = i < have.size() ? have.at(i).toInt() : 0;
const int n = need.at(i).toInt();
if (h != n)
return h > n;
}
return true;
}
inline bool waitUntil(const std::function<bool()> &done, int timeoutMs)
{
QElapsedTimer timer;
timer.start();
while (!done()) {
if (timer.elapsed() > timeoutMs)
return false;
QTest::qWait(10);
}
return true;
}
struct XrayRun
{
QProcess process;
QByteArray output;
bool start(const QString &bin, const QString &configPath, int timeoutMs = 10000)
{
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(bin, { QStringLiteral("run"), QStringLiteral("-c"), configPath });
if (!process.waitForStarted(timeoutMs))
return false;
return waitUntil(
[this] {
output += process.readAll();
return output.contains(" started") || process.state() != QProcess::Running;
},
timeoutMs)
&& process.state() == QProcess::Running;
}
QString stop()
{
process.kill();
process.waitForFinished(3000);
output += process.readAll();
return QString::fromUtf8(output);
}
};
inline QByteArray httpGetThroughSocks(quint16 socksPort, const QHostAddress &target, quint16 targetPort,
const QByteArray &path, int timeoutMs, QString &error)
{
QTcpSocket socket;
QByteArray received;
auto readAtLeast = [&](int size) {
return waitUntil(
[&] {
received += socket.readAll();
return received.size() >= size || socket.state() != QAbstractSocket::ConnectedState;
},
timeoutMs)
&& received.size() >= size;
};
socket.connectToHost(QHostAddress::LocalHost, socksPort);
if (!waitUntil([&] { return socket.state() == QAbstractSocket::ConnectedState; }, timeoutMs)) {
error = QStringLiteral("local socks is not reachable");
return {};
}
socket.write(QByteArray::fromHex("050100"));
if (!readAtLeast(2) || received.left(2) != QByteArray::fromHex("0500")) {
error = QStringLiteral("socks greeting failed");
return {};
}
received.remove(0, 2);
QByteArray connect = QByteArray::fromHex("05010001");
const quint32 ip = qToBigEndian(target.toIPv4Address());
connect.append(reinterpret_cast<const char *>(&ip), 4);
const quint16 port = qToBigEndian(targetPort);
connect.append(reinterpret_cast<const char *>(&port), 2);
socket.write(connect);
if (!readAtLeast(10) || received.at(1) != 0) {
error = QStringLiteral("socks connect failed");
return {};
}
received.remove(0, 10);
socket.write("GET " + path + " HTTP/1.1\r\nHost: runtime.test\r\nConnection: close\r\n\r\n");
waitUntil(
[&] {
received += socket.readAll();
return received.contains(path.mid(1)) || socket.state() != QAbstractSocket::ConnectedState;
},
timeoutMs);
received += socket.readAll();
if (!received.contains(path.mid(1)))
error = received.isEmpty() ? QStringLiteral("no response through the tunnel")
: QStringLiteral("unexpected response through the tunnel");
return received;
}
}
#endif
+230 -13
View File
@@ -74,6 +74,11 @@ void XrayConfigModel::setTransport(const QString &value)
srv.transport = value;
if (value == QLatin1String(protocols::xray::transportXhttp)) {
srv.applyDefaults(false);
srv.xhttp.applyRangeDefaults();
}
if (value == QLatin1String(protocols::xray::transportMkcp)
&& srv.security == QLatin1String(protocols::xray::securityReality)) {
srv.security = QString::fromLatin1(protocols::xray::securityTls);
}
emitChanged(wasUnsavedChanges);
}
@@ -108,6 +113,12 @@ void XrayConfigModel::setFingerprint(const QString &value)
assignField(m_protocolConfig.serverConfig.fingerprint, value);
}
QString XrayConfigModel::realityFingerprint() const
{
const auto &srv = m_protocolConfig.serverConfig;
return srv.isThirdPartyConfig ? srv.fingerprint : xrayEffective::realityFingerprint(srv.fingerprint);
}
QString XrayConfigModel::sni() const
{
return m_protocolConfig.serverConfig.sni;
@@ -585,6 +596,11 @@ void XrayConfigModel::setXmuxHKeepAlivePeriod(const QString &value)
assignField(m_protocolConfig.serverConfig.xhttp.xmux.hKeepAlivePeriod, value);
}
bool XrayConfigModel::xmuxKeepAliveApplies() const
{
return !xrayEffective::xhttpUsesHttp11(m_protocolConfig.serverConfig);
}
void XrayConfigModel::assignField(QString &field, const QString &value)
{
if (field == value) {
@@ -722,6 +738,7 @@ void XrayConfigModel::resetToDefaults()
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
applyDefaultsToServerConfig(m_protocolConfig.serverConfig, true);
m_protocolConfig.serverConfig.xhttp.applyRangeDefaults();
emitChanged(wasUnsavedChanges);
}
@@ -749,7 +766,7 @@ QStringList XrayConfigModel::flowOptions()
QStringList XrayConfigModel::securityOptions()
{
return { "none", "tls", "reality" };
return { "tls", "reality" };
}
QStringList XrayConfigModel::transportOptions()
@@ -762,6 +779,11 @@ QStringList XrayConfigModel::fingerprintOptions()
return { "hellochrome_120", "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" };
}
QStringList XrayConfigModel::realityFingerprintOptions()
{
return { "chrome", "firefox", "safari" };
}
QStringList XrayConfigModel::alpnOptions()
{
return { "h2", "http/1.1", "h2,http/1.1" };
@@ -779,12 +801,12 @@ QStringList XrayConfigModel::xhttpUplinkMethodOptions()
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
{
return { "Path", "Header", "Cookie", "Query", "None" };
return { "Path", "Header", "Cookie", "Query" };
}
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
{
return { "Path", "Header", "Cookie", "Query", "None" };
return { "Path", "Header", "Cookie", "Query" };
}
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
@@ -850,32 +872,32 @@ QString XrayConfigModel::xhttpUplinkChunkSizeDefault()
QString XrayConfigModel::scMaxEachPostBytesMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMin);
return QString::fromLatin1(protocols::xray::coreDefaultXhttpScMaxEachPostBytesMin);
}
QString XrayConfigModel::scMaxEachPostBytesMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMaxEachPostBytesMax);
return QString::fromLatin1(protocols::xray::coreDefaultXhttpScMaxEachPostBytesMax);
}
QString XrayConfigModel::scMinPostsIntervalMsMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMin);
return QString::fromLatin1(protocols::xray::coreDefaultXhttpScMinPostsIntervalMsMin);
}
QString XrayConfigModel::scMinPostsIntervalMsMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScMinPostsIntervalMsMax);
return QString::fromLatin1(protocols::xray::coreDefaultXhttpScMinPostsIntervalMsMax);
}
QString XrayConfigModel::scStreamUpServerSecsMinDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMin);
return QString::fromLatin1(protocols::xray::coreDefaultXhttpScStreamUpServerSecsMin);
}
QString XrayConfigModel::scStreamUpServerSecsMaxDefault()
{
return QString::fromLatin1(protocols::xray::defaultXhttpScStreamUpServerSecsMax);
return QString::fromLatin1(protocols::xray::coreDefaultXhttpScStreamUpServerSecsMax);
}
QString XrayConfigModel::xPaddingKeyDefault()
@@ -899,6 +921,22 @@ QString XrayConfigModel::xPaddingBytesMaxDefault()
}
namespace {
bool isValidDomainName(const QString &t)
{
static const QRegularExpression label(QStringLiteral(R"(^[A-Za-z0-9_]([A-Za-z0-9_-]{0,61}[A-Za-z0-9])?$)"));
static const QRegularExpression topLevel(QStringLiteral(R"(^([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{1,59})$)"));
const QStringList labels = t.split(QLatin1Char('.'));
if (labels.size() < 2) {
return false;
}
for (int i = 0; i < labels.size() - 1; ++i) {
if (!label.match(labels.at(i)).hasMatch()) {
return false;
}
}
return topLevel.match(labels.last()).hasMatch();
}
bool isValidSingleHost(const QString &t)
{
if (t.isEmpty() || t.length() > 253) {
@@ -915,9 +953,7 @@ namespace {
if (onlyDigits.match(t).hasMatch()) {
return false;
}
QRegExp re = NetworkUtilities::domainRegExp();
re.setCaseSensitivity(Qt::CaseInsensitive);
return re.exactMatch(t);
return isValidDomainName(t);
}
}
@@ -927,7 +963,7 @@ bool XrayConfigModel::isValidHost(const QString &host)
if (t.isEmpty()) {
return true;
}
return isValidSingleHost(t);
return QHostAddress(t).protocol() != QHostAddress::IPv6Protocol && isValidSingleHost(t);
}
bool XrayConfigModel::isValidSni(const QString &sni)
@@ -960,18 +996,199 @@ QStringList XrayConfigModel::validationErrors() const
if (!isValidSni(srv.sni)) {
errs << tr("SNI: enter a valid IP address or domain name");
}
} else if (!srv.isThirdPartyConfig) {
if (srv.transport == QLatin1String("mkcp") && srv.security == QLatin1String("reality")) {
errs << tr("mKCP does not work with REALITY. Choose TLS.");
} else if (security.isEmpty() || security == QLatin1String("none")) {
errs << tr("None is not supported by Xray 26.10. Choose TLS or REALITY.");
}
}
const QString fingerprint = srv.fingerprint.trimmed().toLower();
const bool tls12OnlyFingerprint = fingerprint == QLatin1String("android") || fingerprint == QLatin1String("360");
if (security == QLatin1String("reality")) {
if (tls12OnlyFingerprint) {
errs << tr("Fingerprints android and 360 do not support TLS 1.3 and do not work with REALITY");
}
const QHostAddress sniAddress(srv.sni.trimmed());
if (sniAddress.protocol() == QHostAddress::IPv4Protocol || sniAddress.protocol() == QHostAddress::IPv6Protocol) {
errs << tr("REALITY needs a domain name in SNI, not an IP address");
}
}
if (security == QLatin1String("tls") && tls12OnlyFingerprint && !xrayEffective::clientFlow(srv).isEmpty()) {
errs << tr("Fingerprints android and 360 do not support TLS 1.3 and do not work with xtls-rprx-vision");
}
if (!srv.isThirdPartyConfig) {
auto checkValue = [&errs](const QString &field, const QString &value, const QStringList &allowed) {
if (!allowed.contains(value)) {
errs << tr("%1: unsupported value \"%2\"").arg(field, value);
}
};
checkValue(QStringLiteral("Transport"), srv.transport, { "", "raw", "xhttp", "mkcp" });
checkValue(QStringLiteral("Security"), srv.security, { "", "none", "tls", "reality" });
checkValue(QStringLiteral("Flow"), srv.flow, flowOptions());
if (!fingerprint.isEmpty()) {
checkValue(QStringLiteral("Fingerprint"), fingerprint, fingerprintOptions());
}
if (security == QLatin1String("tls")) {
const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(protocols::xray::defaultAlpn) : srv.alpn;
for (const QString &a : xrayEffective::alpnList(alpnEff)) {
checkValue(QStringLiteral("ALPN"), a, { "h2", "http/1.1" });
}
}
if (srv.transport == QLatin1String("xhttp")) {
const auto &x = srv.xhttp;
const QString modeEff = xrayEffective::xhttpMode(x.mode);
checkValue(QStringLiteral("Mode"), modeEff, { "auto", "packet-up", "stream-up", "stream-one" });
QString method = x.uplinkMethod.toUpper();
if (method == QLatin1String("GET") && modeEff == QLatin1String("packet-up"))
method.clear();
checkValue(QStringLiteral("UplinkHTTPMethod"), method, { "", "POST", "PUT", "PATCH" });
checkValue(QStringLiteral("SessionPlacement"), xrayEffective::sessionSeqPlacement(x.sessionPlacement),
{ "", "path", "header", "cookie", "query" });
checkValue(QStringLiteral("SeqPlacement"), xrayEffective::sessionSeqPlacement(x.seqPlacement),
{ "", "path", "header", "cookie", "query" });
checkValue(QStringLiteral("UplinkDataPlacement"), xrayEffective::uplinkDataPlacement(x.uplinkDataPlacement),
{ "body", "auto", "header", "cookie" });
if (x.xPadding.obfsMode) {
checkValue(QStringLiteral("xPaddingPlacement"), xrayEffective::xPaddingPlacement(x.xPadding.placement),
{ "cookie", "header", "query", "queryInHeader" });
checkValue(QStringLiteral("xPaddingMethod"), xrayEffective::xPaddingMethod(x.xPadding.method),
{ "repeat-x", "tokenish" });
}
}
}
if (srv.transport == QLatin1String("xhttp")) {
if (security == QLatin1String("tls") && tls12OnlyFingerprint && !xrayEffective::xhttpUsesHttp11(srv)) {
errs << tr("Fingerprints android and 360 do not offer HTTP/2; with XHTTP use ALPN http/1.1");
}
if (!isValidHost(srv.xhttp.host)) {
errs << tr("Host: enter a valid IP address or domain name");
}
auto isWholeNumber = [](const QString &v, bool allowNegative) {
static const QRegularExpression unsignedNumber(QStringLiteral(R"(^\d+$)"));
static const QRegularExpression signedNumber(QStringLiteral(R"(^-?\d+$)"));
return v.isEmpty() || (allowNegative ? signedNumber : unsignedNumber).match(v).hasMatch();
};
auto checkNumber = [&errs, &isWholeNumber](const QString &field, const QString &v, bool allowNegative) {
if (isWholeNumber(v, allowNegative)) {
return true;
}
errs << (allowNegative ? tr("%1: enter a whole number").arg(field)
: tr("%1: enter a whole number, 0 or more").arg(field));
return false;
};
auto checkRange = [&errs, &isWholeNumber](const QString &field, const QString &from, const QString &to,
bool allowNegative) {
if (isWholeNumber(from, allowNegative) && isWholeNumber(to, allowNegative)) {
return true;
}
errs << (allowNegative ? tr("%1: enter whole numbers").arg(field)
: tr("%1: enter whole numbers, 0 or more").arg(field));
return false;
};
static const QRegularExpression keyName(QStringLiteral(R"(^[A-Za-z0-9_-]+$)"));
auto checkKey = [&errs](const QString &field, const QString &v) {
if (!v.isEmpty() && !keyName.match(v).hasMatch()) {
errs << tr("%1: use only Latin letters, digits, hyphen and underscore").arg(field);
}
};
const auto &x = srv.xhttp;
checkKey(QStringLiteral("sessionKey"), x.sessionKey);
checkKey(QStringLiteral("seqKey"), x.seqKey);
checkKey(QStringLiteral("uplinkDataKey"), x.uplinkDataKey);
checkKey(QStringLiteral("xPaddingKey"), x.xPadding.key);
checkKey(QStringLiteral("xPaddingHeader"), x.xPadding.header);
checkNumber(QStringLiteral("uplinkChunkSize"), x.uplinkChunkSize, true);
const bool scMaxBufferedPostsValid =
checkNumber(QStringLiteral("scMaxBufferedPosts"), x.scMaxBufferedPosts, false);
const bool xPaddingBytesValid =
checkRange(QStringLiteral("xPaddingBytes"), x.xPadding.bytesMin, x.xPadding.bytesMax, false);
const bool scMaxEachPostBytesValid = checkRange(QStringLiteral("scMaxEachPostBytes"), x.scMaxEachPostBytesMin,
x.scMaxEachPostBytesMax, false);
checkRange(QStringLiteral("scMinPostsIntervalMs"), x.scMinPostsIntervalMsMin, x.scMinPostsIntervalMsMax, true);
const bool scStreamUpServerSecsValid = checkRange(QStringLiteral("scStreamUpServerSecs"),
x.scStreamUpServerSecsMin, x.scStreamUpServerSecsMax, true);
const auto &xmux = srv.xhttp.xmux;
if (xmux.enabled) {
checkRange(QStringLiteral("maxConcurrency"), xmux.maxConcurrencyMin, xmux.maxConcurrencyMax, true);
checkRange(QStringLiteral("maxConnections"), xmux.maxConnectionsMin, xmux.maxConnectionsMax, true);
checkRange(QStringLiteral("cMaxReuseTimes"), xmux.cMaxReuseTimesMin, xmux.cMaxReuseTimesMax, true);
checkRange(QStringLiteral("hMaxRequestTimes"), xmux.hMaxRequestTimesMin, xmux.hMaxRequestTimesMax, true);
checkRange(QStringLiteral("hMaxReusableSecs"), xmux.hMaxReusableSecsMin, xmux.hMaxReusableSecsMax, true);
checkNumber(QStringLiteral("hKeepAlivePeriod"), xmux.hKeepAlivePeriod, true);
}
auto isSet = [](const QString &v) { return !v.isEmpty() && v != QLatin1String("0"); };
if (xmux.enabled && (isSet(xmux.maxConcurrencyMin) || isSet(xmux.maxConcurrencyMax))
&& (isSet(xmux.maxConnectionsMin) || isSet(xmux.maxConnectionsMax))) {
errs << tr("XMux: set either maxConcurrency or maxConnections, not both");
}
auto rangeValue = [](const QString &v, const char *fallback) {
return (v.isEmpty() ? QString::fromLatin1(fallback) : v).toLongLong();
};
const auto &pad = srv.xhttp.xPadding;
if (xPaddingBytesValid && (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty())) {
const qint64 from = rangeValue(pad.bytesMin, protocols::xray::defaultXPaddingBytesMin);
const qint64 to = qMax(from, rangeValue(pad.bytesMax, protocols::xray::defaultXPaddingBytesMax));
if (from == 0 && to > 0) {
errs << tr("xPaddingBytes: Min can be 0 only if Max is 0 too (0–0 = core default 100–1000)");
}
}
const QString mode = xrayEffective::xhttpMode(srv.xhttp.mode);
const bool packetUpEffective = mode == QLatin1String("packet-up")
|| (mode == QLatin1String("auto") && security != QLatin1String("reality"));
if (packetUpEffective && scMaxEachPostBytesValid
&& (!srv.xhttp.scMaxEachPostBytesMin.isEmpty() || !srv.xhttp.scMaxEachPostBytesMax.isEmpty())) {
const qint64 from = rangeValue(srv.xhttp.scMaxEachPostBytesMin,
protocols::xray::coreDefaultXhttpScMaxEachPostBytesMin);
const qint64 to = rangeValue(srv.xhttp.scMaxEachPostBytesMax,
protocols::xray::coreDefaultXhttpScMaxEachPostBytesMax);
if (qMin(from, to) == 0 && qMax(from, to) > 0) {
errs << tr("scMaxEachPostBytes: Min can be 0 only if Max is 0 too (0–0 = core default 1000000)");
} else if (qMin(from, to) > 0 && qMin(from, to) < 1000) {
errs << tr("scMaxEachPostBytes: minimum is 1000 (0–0 = core default 1000000)");
}
}
if (mode == QLatin1String("stream-up") && scStreamUpServerSecsValid
&& (!srv.xhttp.scStreamUpServerSecsMin.isEmpty() || !srv.xhttp.scStreamUpServerSecsMax.isEmpty())) {
const qint64 from = rangeValue(srv.xhttp.scStreamUpServerSecsMin,
protocols::xray::coreDefaultXhttpScStreamUpServerSecsMin);
const qint64 to = rangeValue(srv.xhttp.scStreamUpServerSecsMax,
protocols::xray::coreDefaultXhttpScStreamUpServerSecsMax);
if (qMin(from, to) == 0 && qMax(from, to) > 0) {
errs << tr("scStreamUpServerSecs: Min can be 0 only if Max is 0 too (0–0 = core default 20–80)");
}
}
if (scMaxBufferedPostsValid && srv.xhttp.scMaxBufferedPosts.toLongLong() > 100000) {
errs << tr("scMaxBufferedPosts: maximum is 100000 (Packet-up settings)");
}
}
if (srv.transport == QLatin1String("mkcp")) {
auto checkInt = [&errs](const QString &field, const QString &value, qint64 lo, qint64 hi) -> qint64 {
if (value.isEmpty()) {
return -1;
}
bool ok = false;
const qint64 n = value.toLongLong(&ok);
if (!ok || n < lo || n > hi) {
errs << tr("%1: allowed range is %2–%3").arg(field).arg(lo).arg(hi);
return -1;
}
return n;
};
const auto &k = srv.mkcp;
checkInt(QStringLiteral("TTI"), k.tti, 10, 100);
const qint64 mtu = checkInt(QStringLiteral("mtu"), k.mtu, 576, 1460);
checkInt(QStringLiteral("uplinkCapacity"), k.uplinkCapacity, 0, 4095);
checkInt(QStringLiteral("downlinkCapacity"), k.downlinkCapacity, 0, 4095);
checkInt(QStringLiteral("cwndMultiplier"), k.cwndMultiplier, 1, 5000);
const qint64 mtuEff = mtu > 0 ? mtu : QString::fromLatin1(protocols::xray::defaultMkcpMtu).toLongLong();
checkInt(QStringLiteral("maxSendingWindow"), k.maxSendingWindow, mtuEff, 2147483647);
}
return errs;
@@ -19,6 +19,7 @@ class XrayConfigModel : public QObject
Q_PROPERTY(QString security READ security WRITE setSecurity NOTIFY changed)
Q_PROPERTY(QString flow READ flow WRITE setFlow NOTIFY changed)
Q_PROPERTY(QString fingerprint READ fingerprint WRITE setFingerprint NOTIFY changed)
Q_PROPERTY(QString realityFingerprint READ realityFingerprint NOTIFY changed)
Q_PROPERTY(QString sni READ sni WRITE setSni NOTIFY changed)
Q_PROPERTY(QString alpn READ alpn WRITE setAlpn NOTIFY changed)
Q_PROPERTY(QString xhttpMode READ xhttpMode WRITE setXhttpMode NOTIFY changed)
@@ -66,6 +67,7 @@ class XrayConfigModel : public QObject
Q_PROPERTY(QString xmuxHMaxReusableSecsMin READ xmuxHMaxReusableSecsMin WRITE setXmuxHMaxReusableSecsMin NOTIFY changed)
Q_PROPERTY(QString xmuxHMaxReusableSecsMax READ xmuxHMaxReusableSecsMax WRITE setXmuxHMaxReusableSecsMax NOTIFY changed)
Q_PROPERTY(QString xmuxHKeepAlivePeriod READ xmuxHKeepAlivePeriod WRITE setXmuxHKeepAlivePeriod NOTIFY changed)
Q_PROPERTY(bool xmuxKeepAliveApplies READ xmuxKeepAliveApplies NOTIFY changed)
public:
explicit XrayConfigModel(QObject* parent = nullptr);
@@ -87,6 +89,7 @@ public:
QString fingerprint() const;
void setFingerprint(const QString &value);
QString realityFingerprint() const;
QString sni() const;
void setSni(const QString &value);
@@ -227,6 +230,7 @@ public:
void setXmuxHMaxReusableSecsMax(const QString &value);
QString xmuxHKeepAlivePeriod() const;
bool xmuxKeepAliveApplies() const;
void setXmuxHKeepAlivePeriod(const QString &value);
// ── Static option lists (for QML DropDown models) ─────────────────
@@ -234,6 +238,7 @@ public:
Q_INVOKABLE static QStringList securityOptions();
Q_INVOKABLE static QStringList transportOptions();
Q_INVOKABLE static QStringList fingerprintOptions();
Q_INVOKABLE static QStringList realityFingerprintOptions();
Q_INVOKABLE static QStringList alpnOptions();
Q_INVOKABLE static QStringList xhttpModeOptions();
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
@@ -49,6 +49,7 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Empty")
enabled: root.visionAllowed
checked: XrayConfigModel.flow === "" || !root.visionAllowed
onClicked: XrayConfigModel.flow = ""
}
@@ -16,6 +16,7 @@ PageType {
id: root
readonly property bool realityAllowed: XrayConfigModel.transport !== "mkcp"
readonly property bool securityIsNone: XrayConfigModel.security === "none" || (XrayConfigModel.security === "reality" && !realityAllowed)
FlickableType {
id: flickable
@@ -43,15 +44,6 @@ PageType {
title: qsTr("Security")
}
RadioItemType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("None")
checked: XrayConfigModel.security === "none" || (XrayConfigModel.security === "reality" && !root.realityAllowed)
onClicked: XrayConfigModel.security = "none"
}
RadioItemType {
Layout.fillWidth: true
Layout.leftMargin: 16
@@ -78,7 +70,17 @@ PageType {
Layout.topMargin: 12
visible: !root.realityAllowed
color: AmneziaStyle.color.textAccent
text: qsTr("REALITY is not supported with the mKCP transport. Use None or TLS.")
text: qsTr("REALITY is not supported with the mKCP transport. Use TLS.")
}
SmallTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 12
visible: root.securityIsNone
color: AmneziaStyle.color.textAccent
text: qsTr("None is not supported by Xray 26.10. Choose TLS or REALITY.")
}
ColumnLayout {
@@ -161,7 +163,7 @@ PageType {
headerText: qsTr("Server Name (SNI)")
placeholderText: XrayConfigModel.sniDefault()
textField.text: XrayConfigModel.sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._-]*$/ }
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:-]*$/ }
textField.onTextEdited: XrayConfigModel.sni = textField.text
textField.onEditingFinished: {
var v = textField.text.trim()
@@ -184,26 +186,26 @@ PageType {
id: realityFingerprintDropDown
fitContent: true
Layout.fillWidth: true
text: XrayConfigModel.fingerprint
text: XrayConfigModel.realityFingerprint
descriptionText: qsTr("Fingerprint")
headerText: qsTr("Fingerprint")
listView: RadioListType {
model: ListModel {
Component.onCompleted: {
var opts = XrayConfigModel.fingerprintOptions()
var opts = XrayConfigModel.realityFingerprintOptions()
for (var i = 0; i < opts.length; i++) {
append({name: opts[i]})
}
}
}
currentValue: XrayConfigModel.fingerprint
currentValue: XrayConfigModel.realityFingerprint
onActivated: {
XrayConfigModel.fingerprint = selectedText
realityFingerprintDropDown.close()
}
Component.onCompleted: {
for (var i = 0; i < model.count; i++) {
if (model.get(i).name === XrayConfigModel.fingerprint) {
if (model.get(i).name === XrayConfigModel.realityFingerprint) {
selectedIndex = i;
break
}
@@ -19,6 +19,19 @@ PageType {
var m = XrayConfigModel.xhttpMode.toLowerCase()
return m === "packet-up" || ((m === "" || m === "auto") && XrayConfigModel.security !== "reality")
}
readonly property bool packetUpExplicit: XrayConfigModel.xhttpMode.toLowerCase() === "packet-up"
readonly property bool streamUpExplicit: XrayConfigModel.xhttpMode.toLowerCase() === "stream-up"
readonly property bool streamOneEffective: {
var m = XrayConfigModel.xhttpMode.toLowerCase()
return m === "stream-one" || ((m === "" || m === "auto") && XrayConfigModel.security === "reality")
}
readonly property string uplinkDataPlacementEffective: XrayConfigModel.xhttpUplinkDataPlacement === ""
? "body" : XrayConfigModel.xhttpUplinkDataPlacement.toLowerCase()
readonly property bool uplinkDataUsesKey: uplinkDataPlacementEffective === "header" || uplinkDataPlacementEffective === "cookie"
function placementUsesKey(placement) {
var p = placement.toLowerCase()
return p === "header" || p === "cookie" || p === "query"
}
function clampInt(text, lo, hi) {
if (text === "")
@@ -119,14 +132,14 @@ PageType {
TextFieldType {
Layout.fillWidth: true
headerText: qsTr("uplinkCapacity")
helperText: qsTr("Uplink capacity (MB/s). Maximum: 2147483647.")
helperText: qsTr("Uplink capacity (MB/s). Maximum: 4095.")
placeholderText: XrayConfigModel.mkcpDefaultUplinkCapacity()
textField.text: XrayConfigModel.mkcpUplinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: XrayConfigModel.mkcpUplinkCapacity = textField.text
textField.onEditingFinished: {
var v = root.clampInt(textField.text, 0, 2147483647)
var v = root.clampInt(textField.text, 0, 4095)
if (v !== XrayConfigModel.mkcpUplinkCapacity) XrayConfigModel.mkcpUplinkCapacity = v
}
}
@@ -134,14 +147,14 @@ PageType {
TextFieldType {
Layout.fillWidth: true
headerText: qsTr("downlinkCapacity")
helperText: qsTr("Downlink capacity (MB/s). Maximum: 2147483647.")
helperText: qsTr("Downlink capacity (MB/s). Maximum: 4095.")
placeholderText: XrayConfigModel.mkcpDefaultDownlinkCapacity()
textField.text: XrayConfigModel.mkcpDownlinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: XrayConfigModel.mkcpDownlinkCapacity = textField.text
textField.onEditingFinished: {
var v = root.clampInt(textField.text, 0, 2147483647)
var v = root.clampInt(textField.text, 0, 4095)
if (v !== XrayConfigModel.mkcpDownlinkCapacity) XrayConfigModel.mkcpDownlinkCapacity = v
}
}
@@ -170,14 +183,14 @@ PageType {
TextFieldType {
Layout.fillWidth: true
headerText: qsTr("cwndMultiplier")
helperText: qsTr("Congestion window multiplier. Minimum: 1.")
helperText: qsTr("Congestion window multiplier. Range: 1–5000.")
placeholderText: XrayConfigModel.mkcpDefaultCwndMultiplier()
textField.text: XrayConfigModel.mkcpCwndMultiplier
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: XrayConfigModel.mkcpCwndMultiplier = textField.text
textField.onEditingFinished: {
var v = root.clampInt(textField.text, 1, 2147483647)
var v = root.clampInt(textField.text, 1, 5000)
if (v !== XrayConfigModel.mkcpCwndMultiplier) XrayConfigModel.mkcpCwndMultiplier = v
}
}
@@ -310,6 +323,7 @@ PageType {
SwitchRowType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: !root.packetUpEffective
text: qsTr("Disable gRPC Header")
descriptionText: qsTr("noGRPCHeader")
checked: XrayConfigModel.xhttpDisableGrpc
@@ -328,11 +342,13 @@ PageType {
SmallTextType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: !root.streamOneEffective
text: qsTr("Session & Sequence")
}
DropDownType {
id: sessionPlacementDropDown
visible: !root.streamOneEffective
fitContent: true
Layout.fillWidth: true
text: XrayConfigModel.xhttpSessionPlacement
@@ -365,6 +381,7 @@ PageType {
TextFieldType {
Layout.fillWidth: true
visible: !root.streamOneEffective && root.placementUsesKey(XrayConfigModel.xhttpSessionPlacement)
headerText: qsTr("SessionKey")
textField.text: XrayConfigModel.xhttpSessionKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
@@ -410,7 +427,7 @@ PageType {
TextFieldType {
Layout.fillWidth: true
visible: root.packetUpEffective
visible: root.packetUpEffective && root.placementUsesKey(XrayConfigModel.xhttpSeqPlacement)
headerText: qsTr("SeqKey")
textField.text: XrayConfigModel.xhttpSeqKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
@@ -423,7 +440,7 @@ PageType {
DropDownType {
id: uplinkDataPlacementDropDown
visible: root.packetUpEffective
visible: root.packetUpExplicit
fitContent: true
Layout.fillWidth: true
text: XrayConfigModel.xhttpUplinkDataPlacement
@@ -456,7 +473,7 @@ PageType {
TextFieldType {
Layout.fillWidth: true
visible: root.packetUpEffective
visible: root.packetUpExplicit && root.uplinkDataUsesKey
headerText: qsTr("UplinkDataKey")
textField.text: XrayConfigModel.xhttpUplinkDataKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
@@ -470,13 +487,15 @@ PageType {
SmallTextType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: !root.streamOneEffective
text: qsTr("Traffic Shaping")
}
TextFieldType {
Layout.fillWidth: true
visible: root.packetUpExplicit && root.uplinkDataUsesKey
headerText: qsTr("UplinkChunkSize")
helperText: qsTr("Uplink chunk size in bytes. Maximum: 2147483647. 0 = off.")
helperText: qsTr("Uplink chunk size in bytes. Maximum: 2147483647. 0 = core default (2–3 KB in cookie, 3–4 KB in header). Values below 64 are raised to 64.")
placeholderText: XrayConfigModel.xhttpUplinkChunkSizeDefault()
textField.text: XrayConfigModel.xhttpUplinkChunkSize
textField.maximumLength: 10
@@ -490,14 +509,15 @@ PageType {
TextFieldType {
Layout.fillWidth: true
visible: root.packetUpEffective
headerText: qsTr("scMaxBufferedPosts")
helperText: qsTr("Max buffered POSTs. Range: 0–2147483647.")
helperText: qsTr("Max buffered POSTs. Range: 0–100000.")
textField.text: XrayConfigModel.xhttpScMaxBufferedPosts
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: XrayConfigModel.xhttpScMaxBufferedPosts = textField.text
textField.onEditingFinished: {
var v = root.clampInt(textField.text, 0, 2147483647)
var v = root.clampInt(textField.text, 0, 100000)
if (v !== XrayConfigModel.xhttpScMaxBufferedPosts) XrayConfigModel.xhttpScMaxBufferedPosts = v
}
}
@@ -505,11 +525,14 @@ PageType {
SmallTextType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: root.packetUpEffective
text: qsTr("scMaxEachPostBytes")
}
MinMaxRowType {
Layout.fillWidth: true
visible: root.packetUpEffective
minValue: XrayConfigModel.xhttpScMaxEachPostBytesMin
helperText: qsTr("≤ 2147483647. Empty = core default (1000000).")
maxValue: XrayConfigModel.xhttpScMaxEachPostBytesMax
minPlaceholder: XrayConfigModel.scMaxEachPostBytesMinDefault()
maxPlaceholder: XrayConfigModel.scMaxEachPostBytesMaxDefault()
@@ -520,11 +543,14 @@ PageType {
SmallTextType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: root.streamUpExplicit
text: qsTr("scStreamUpServerSecs")
}
MinMaxRowType {
Layout.fillWidth: true
visible: root.streamUpExplicit
minValue: XrayConfigModel.xhttpScStreamUpServerSecsMin
helperText: qsTr("≤ 2147483647. Empty = core default (20–80).")
maxValue: XrayConfigModel.xhttpScStreamUpServerSecsMax
minPlaceholder: XrayConfigModel.scStreamUpServerSecsMinDefault()
maxPlaceholder: XrayConfigModel.scStreamUpServerSecsMaxDefault()
@@ -535,11 +561,14 @@ PageType {
SmallTextType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: root.packetUpEffective
text: qsTr("scMinPostsIntervalMs")
}
MinMaxRowType {
Layout.fillWidth: true
visible: root.packetUpEffective
minValue: XrayConfigModel.xhttpScMinPostsIntervalMsMin
helperText: qsTr("Min 0 = no pause between POSTs. Empty or 0–0 = core default (30 ms).")
maxValue: XrayConfigModel.xhttpScMinPostsIntervalMsMax
minPlaceholder: XrayConfigModel.scMinPostsIntervalMsMinDefault()
maxPlaceholder: XrayConfigModel.scMinPostsIntervalMsMaxDefault()
@@ -570,7 +599,7 @@ PageType {
ListItemType {
Layout.fillWidth: true
text: qsTr("XMux")
subtitle: XrayConfigModel.xmuxEnabled ? qsTr("On") : qsTr("Off")
subtitle: XrayConfigModel.xmuxEnabled ? qsTr("Custom") : qsTr("Core defaults")
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
onClicked: {
PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
@@ -15,6 +15,15 @@ import "../Components"
PageType {
id: root
readonly property string placementEffective: XrayConfigModel.xPaddingPlacement === ""
? "cookie" : XrayConfigModel.xPaddingPlacement.toLowerCase().replace(/ /g, "")
readonly property bool bytesDefault: (XrayConfigModel.xPaddingBytesMin === "" && XrayConfigModel.xPaddingBytesMax === "")
|| (XrayConfigModel.xPaddingBytesMin === "0" && XrayConfigModel.xPaddingBytesMax === "0")
readonly property string bytesMinEffective: XrayConfigModel.xPaddingBytesMin !== "" ? XrayConfigModel.xPaddingBytesMin
: XrayConfigModel.xPaddingBytesMinDefault()
readonly property string bytesMaxEffective: XrayConfigModel.xPaddingBytesMax !== "" ? XrayConfigModel.xPaddingBytesMax
: XrayConfigModel.xPaddingBytesMaxDefault()
FlickableType {
id: flickable
@@ -50,9 +59,10 @@ PageType {
ListItemType {
Layout.fillWidth: true
enabled: XrayConfigModel.xPaddingObfsMode
text: qsTr("xPaddingBytes")
subtitle: (XrayConfigModel.xPaddingBytesMin === "" && XrayConfigModel.xPaddingBytesMax === "") ? qsTr("Default") : (XrayConfigModel.xPaddingBytesMin !== "" ? XrayConfigModel.xPaddingBytesMin : XrayConfigModel.xPaddingBytesMinDefault()) + "—" + (XrayConfigModel.xPaddingBytesMax !== "" ? XrayConfigModel.xPaddingBytesMax : XrayConfigModel.xPaddingBytesMaxDefault())
subtitle: root.bytesDefault
? qsTr("Default (%1—%2)").arg(XrayConfigModel.xPaddingBytesMinDefault()).arg(XrayConfigModel.xPaddingBytesMaxDefault())
: root.bytesMinEffective + "—" + root.bytesMaxEffective
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
onClicked: {
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
@@ -69,7 +79,7 @@ PageType {
TextFieldType {
Layout.fillWidth: true
enabled: XrayConfigModel.xPaddingObfsMode
enabled: XrayConfigModel.xPaddingObfsMode && root.placementEffective !== "header"
Layout.topMargin: 12
headerText: qsTr("xPaddingKey")
placeholderText: XrayConfigModel.xPaddingKeyDefault()
@@ -85,6 +95,7 @@ PageType {
TextFieldType {
Layout.fillWidth: true
enabled: XrayConfigModel.xPaddingObfsMode
&& (root.placementEffective === "header" || root.placementEffective === "queryinheader")
headerText: qsTr("xPaddingHeader")
placeholderText: XrayConfigModel.xPaddingHeaderDefault()
textField.text: XrayConfigModel.xPaddingHeader
@@ -63,8 +63,8 @@ PageType {
SwitchRowType {
Layout.fillWidth: true
text: qsTr("xmux")
descriptionText: qsTr("Set either maxConcurrency or maxConnections, not both")
text: qsTr("Custom xmux parameters")
descriptionText: qsTr("Set either maxConcurrency or maxConnections, not both. Off = core defaults (maxConnections 3, hMaxRequestTimes 600–900, hMaxReusableSecs 1800–3000). If any field is set, the other defaults are not applied.")
checked: XrayConfigModel.xmuxEnabled
onToggled: XrayConfigModel.xmuxEnabled = checked
}
@@ -153,6 +153,7 @@ PageType {
TextFieldType {
Layout.fillWidth: true
Layout.topMargin: 12
visible: XrayConfigModel.xmuxKeepAliveApplies
headerText: qsTr("hKeepAlivePeriod")
helperText: qsTr("HTTP keep-alive period. Integer, may be negative.")
textField.text: XrayConfigModel.xmuxHKeepAlivePeriod