mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-11-12 07:23:19 +09:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db00dce3a1 | ||
|
|
b4a26c6f53 | ||
|
|
a57a196a04 | ||
|
|
1023d04d4b | ||
|
|
ab776048c1 | ||
|
|
db734b346b | ||
|
|
e354dd7a6a | ||
|
|
9fc91e3bc8 | ||
|
|
6191b97ef2 | ||
|
|
70173d0c3c |
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";
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user