Add hysteria gui

This commit is contained in:
arm64v8a
2023-04-27 21:00:21 +09:00
parent bc9c5799b1
commit 8b52d46a06
22 changed files with 769 additions and 121 deletions

View File

@@ -213,6 +213,10 @@ set(PROJECT_SOURCES
ui/edit/edit_naive.cpp ui/edit/edit_naive.cpp
ui/edit/edit_naive.ui ui/edit/edit_naive.ui
ui/edit/edit_hysteria.h
ui/edit/edit_hysteria.cpp
ui/edit/edit_hysteria.ui
ui/edit/edit_custom.h ui/edit/edit_custom.h
ui/edit/edit_custom.cpp ui/edit/edit_custom.cpp
ui/edit/edit_custom.ui ui/edit/edit_custom.ui

View File

@@ -9,6 +9,7 @@
#include <QFileInfo> #include <QFileInfo>
#define BOX_UNDERLYING_DNS NekoRay::dataStore->core_box_underlying_dns.isEmpty() ? "underlying://0.0.0.0" : NekoRay::dataStore->core_box_underlying_dns #define BOX_UNDERLYING_DNS NekoRay::dataStore->core_box_underlying_dns.isEmpty() ? "underlying://0.0.0.0" : NekoRay::dataStore->core_box_underlying_dns
#define BOX_UNDERLYING_DNS_EXPORT NekoRay::dataStore->core_box_underlying_dns.isEmpty() ? "local" : NekoRay::dataStore->core_box_underlying_dns
namespace NekoRay { namespace NekoRay {
@@ -769,7 +770,7 @@ namespace NekoRay {
// Direct // Direct
auto directDNSAddress = dataStore->direct_dns; auto directDNSAddress = dataStore->direct_dns;
if (directDNSAddress == "localhost") directDNSAddress = BOX_UNDERLYING_DNS; if (directDNSAddress == "localhost") directDNSAddress = BOX_UNDERLYING_DNS_EXPORT;
if (!status->forTest) if (!status->forTest)
dnsServers += QJsonObject{ dnsServers += QJsonObject{
{"tag", "dns-direct"}, {"tag", "dns-direct"},
@@ -782,7 +783,7 @@ namespace NekoRay {
// Underlying 100% Working DNS // Underlying 100% Working DNS
dnsServers += QJsonObject{ dnsServers += QJsonObject{
{"tag", "dns-local"}, {"tag", "dns-local"},
{"address", BOX_UNDERLYING_DNS}, {"address", BOX_UNDERLYING_DNS_EXPORT},
{"detour", "direct"}, {"detour", "direct"},
}; };

View File

@@ -85,6 +85,8 @@ namespace NekoRay {
bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_VLESS); bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_VLESS);
} else if (type == "naive") { } else if (type == "naive") {
bean = new fmt::NaiveBean(); bean = new fmt::NaiveBean();
} else if (type == "hysteria") {
bean = new fmt::HysteriaBean();
} else if (type == "custom") { } else if (type == "custom") {
bean = new fmt::CustomBean(); bean = new fmt::CustomBean();
} else { } else {

View File

@@ -16,6 +16,8 @@ namespace NekoRay {
class NaiveBean; class NaiveBean;
class HysteriaBean;
class CustomBean; class CustomBean;
class ChainBean; class ChainBean;
@@ -63,6 +65,10 @@ namespace NekoRay {
return (fmt::NaiveBean *) bean.get(); return (fmt::NaiveBean *) bean.get();
}; };
[[nodiscard]] fmt::HysteriaBean *HysteriaBean() const {
return (fmt::HysteriaBean *) bean.get();
};
[[nodiscard]] fmt::CustomBean *CustomBean() const { [[nodiscard]] fmt::CustomBean *CustomBean() const {
return (fmt::CustomBean *) bean.get(); return (fmt::CustomBean *) bean.get();
}; };

View File

@@ -135,6 +135,34 @@ namespace NekoRay::fmt {
return result; return result;
} }
CoreObjOutboundBuildResult HysteriaBean::BuildCoreObjSingBox() {
CoreObjOutboundBuildResult result;
QJsonObject coreTlsObj{
{"enabled", true},
{"insecure", allowInsecure},
};
if (!alpn.trimmed().isEmpty()) coreTlsObj["alpn"] = QJsonArray{alpn};
QJsonObject coreHysteriaObj{
{"type", "hysteria"},
{"server", serverAddress},
{"server_port", serverPort},
{"disable_mtu_discovery", disableMtuDiscovery},
{"recv_window", streamReceiveWindow},
{"recv_window_conn", connectionReceiveWindow},
{"up_mbps", uploadMbps},
{"down_mbps", downloadMbps},
{"tls", coreTlsObj},
};
if (authPayloadType == hysteria_auth_base64) coreHysteriaObj["auth"] = authPayload;
if (authPayloadType == hysteria_auth_string) coreHysteriaObj["auth_str"] = authPayload;
result.outbound = coreHysteriaObj;
return result;
}
CoreObjOutboundBuildResult CustomBean::BuildCoreObjSingBox() { CoreObjOutboundBuildResult CustomBean::BuildCoreObjSingBox() {
CoreObjOutboundBuildResult result; CoreObjOutboundBuildResult result;

View File

@@ -31,13 +31,28 @@ namespace NekoRay::fmt {
return 1; return 1;
} }
int CustomBean::NeedExternal(bool isFirstProfile, bool isVPN) { int HysteriaBean::NeedExternal(bool isFirstProfile, bool isVPN) {
if (core == "internal" || core == "internal-full") return 0; if (IS_NEKO_BOX) {
if (core == "hysteria") { if (protocol == hysteria_protocol_udp && hopPort.trimmed().isEmpty()) {
// sing-box support
return 0;
} else {
// hysteria core support
if (isFirstProfile && !isVPN) { if (isFirstProfile && !isVPN) {
return 2; return 2;
} }
return 1;
} }
} else {
if (isFirstProfile && !isVPN) {
return 2;
}
return 1;
}
}
int CustomBean::NeedExternal(bool isFirstProfile, bool isVPN) {
if (core == "internal" || core == "internal-full") return 0;
return 1; return 1;
} }
@@ -64,8 +79,8 @@ namespace NekoRay::fmt {
if (domain_address != connect_address) if (domain_address != connect_address)
result.arguments += "--host-resolver-rules=MAP " + domain_address + " " + connect_address; result.arguments += "--host-resolver-rules=MAP " + domain_address + " " + connect_address;
if (insecure_concurrency > 0) result.arguments += "--insecure-concurrency=" + Int2String(insecure_concurrency); if (insecure_concurrency > 0) result.arguments += "--insecure-concurrency=" + Int2String(insecure_concurrency);
if (!extra_headers.isEmpty()) result.arguments += "--extra-headers=" + extra_headers; if (!extra_headers.trimmed().isEmpty()) result.arguments += "--extra-headers=" + extra_headers;
if (!certificate.isEmpty()) { if (!certificate.trimmed().isEmpty()) {
WriteTempFile("naive_" + GetRandomString(10) + ".crt", certificate.toUtf8()); WriteTempFile("naive_" + GetRandomString(10) + ".crt", certificate.toUtf8());
result.env += "SSL_CERT_FILE=" + TempFile; result.env += "SSL_CERT_FILE=" + TempFile;
} }
@@ -77,6 +92,67 @@ namespace NekoRay::fmt {
return result; return result;
} }
ExternalBuildResult HysteriaBean::BuildExternal(int mapping_port, int socks_port, int external_stat) {
ExternalBuildResult result{dataStore->extraCore->Get("hysteria")};
QJsonObject config;
// determine server format
auto is_direct = external_stat == 2;
auto sni2 = sni;
if (sni.isEmpty() && is_direct) sni2 = serverAddress;
auto server = serverAddress;
if (!hopPort.trimmed().isEmpty()) {
server = WrapIPV6Host(server) + ":" + hopPort;
} else {
server = WrapIPV6Host(server) + ":" + Int2String(serverPort);
}
config["server"] = is_direct ? server : "127.0.0.1:" + Int2String(mapping_port);
// listen
config["socks5"] = QJsonObject{
{"listen", "127.0.0.1:" + Int2String(socks_port)},
};
// misc
config["retry"] = 5;
config["fast_open"] = true;
config["lazy_start"] = true;
config["obfs"] = obfsPassword;
config["up_mbps"] = uploadMbps;
config["down_mbps"] = downloadMbps;
if (authPayloadType == hysteria_auth_base64) config["auth"] = authPayload;
if (authPayloadType == hysteria_auth_string) config["auth_str"] = authPayload;
if (protocol == hysteria_protocol_facktcp) config["protocol"] = "faketcp";
if (protocol == hysteria_protocol_wechat_video) config["protocol"] = "wechat-video";
if (!sni2.isEmpty()) config["server_name"] = sni2;
if (!alpn.isEmpty()) config["alpn"] = alpn;
if (!caText.trimmed().isEmpty()) {
WriteTempFile("hysteria_" + GetRandomString(10) + ".crt", caText.toUtf8());
config["ca"] = TempFile;
}
if (allowInsecure) config["insecure"] = true;
if (streamReceiveWindow > 0) config["recv_window_conn"] = streamReceiveWindow;
if (connectionReceiveWindow > 0) config["recv_window"] = connectionReceiveWindow;
if (disableMtuDiscovery) config["disable_mtu_discovery"] = true;
config["hop_interval"] = hopInterval;
//
result.config_export = QJsonObject2QString(config, false);
WriteTempFile("hysteria_" + GetRandomString(10) + ".json", result.config_export.toUtf8());
result.arguments = QStringList{"--no-check", "-c", TempFile};
return result;
}
ExternalBuildResult CustomBean::BuildExternal(int mapping_port, int socks_port, int external_stat) { ExternalBuildResult CustomBean::BuildExternal(int mapping_port, int socks_port, int external_stat) {
ExternalBuildResult result{dataStore->extraCore->Get(core)}; ExternalBuildResult result{dataStore->extraCore->Get(core)};
@@ -107,14 +183,6 @@ namespace NekoRay::fmt {
suffix = ".json"; suffix = ".json";
} }
// known core direct out
if (external_stat == 2) {
if (core == "hysteria") {
config = config.replace(QString("\"127.0.0.1:%1\"").arg(mapping_port),
"\"" + DisplayAddress() + "\"");
}
}
// write config // write config
WriteTempFile("custom_" + GetRandomString(10) + suffix, config.toUtf8()); WriteTempFile("custom_" + GetRandomString(10) + suffix, config.toUtf8());
for (int i = 0; i < result.arguments.count(); i++) { for (int i = 0; i < result.arguments.count(); i++) {

View File

@@ -102,7 +102,7 @@ namespace NekoRay::fmt {
{"tls", stream->security == "tls" ? "tls" : ""}, {"tls", stream->security == "tls" ? "tls" : ""},
{"sni", stream->sni}, {"sni", stream->sni},
}; };
return "vmess://" + QJsonObject2QString(N, false).toUtf8().toBase64(); return "vmess://" + QJsonObject2QString(N, true).toUtf8().toBase64();
} }
QString NaiveBean::ToShareLink() { QString NaiveBean::ToShareLink() {
@@ -116,4 +116,30 @@ namespace NekoRay::fmt {
return url.toString(QUrl::FullyEncoded); return url.toString(QUrl::FullyEncoded);
} }
QString HysteriaBean::ToShareLink() {
QUrl url;
url.setScheme("hysteria");
url.setHost(serverAddress);
url.setPort(serverPort);
QUrlQuery q;
q.addQueryItem("upmbps", Int2String(uploadMbps));
q.addQueryItem("downmbps", Int2String(downloadMbps));
if (!obfsPassword.isEmpty()) {
q.addQueryItem("obfs", "xplus");
q.addQueryItem("obfsParam", obfsPassword);
}
if (authPayloadType == hysteria_auth_string) q.addQueryItem("auth", authPayload);
if (protocol == hysteria_protocol_facktcp) q.addQueryItem("protocol", "faketcp");
if (protocol == hysteria_protocol_wechat_video) q.addQueryItem("protocol", "wechat-video");
if (!hopPort.trimmed().isEmpty()) q.addQueryItem("mport", hopPort);
if (allowInsecure) q.addQueryItem("insecure", "1");
if (!sni.isEmpty()) q.addQueryItem("peer", sni);
if (!alpn.isEmpty()) q.addQueryItem("alpn", alpn);
if (connectionReceiveWindow > 0) q.addQueryItem("recv_window", Int2String(connectionReceiveWindow));
if (streamReceiveWindow > 0) q.addQueryItem("recv_window_conn", Int2String(streamReceiveWindow));
if (!q.isEmpty()) url.setQuery(q);
if (!name.isEmpty()) url.setFragment(name);
return url.toString(QUrl::FullyEncoded);
}
} // namespace NekoRay::fmt } // namespace NekoRay::fmt

80
fmt/HysteriaBean.hpp Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#include "fmt/AbstractBean.hpp"
namespace NekoRay::fmt {
class HysteriaBean : public AbstractBean {
public:
static constexpr int hysteria_protocol_udp = 0;
static constexpr int hysteria_protocol_facktcp = 1;
static constexpr int hysteria_protocol_wechat_video = 2;
int protocol = 0;
//
static constexpr int hysteria_auth_none = 0;
static constexpr int hysteria_auth_string = 1;
static constexpr int hysteria_auth_base64 = 2;
int authPayloadType = 0;
QString authPayload = "";
QString obfsPassword = "";
//
int uploadMbps = 100;
int downloadMbps = 100;
qint64 streamReceiveWindow = 0;
qint64 connectionReceiveWindow = 0;
bool disableMtuDiscovery = false;
bool allowInsecure = false;
QString sni = "";
QString alpn = ""; // only 1
QString caText = "";
//
int hopInterval = 10;
QString hopPort = "";
HysteriaBean() : AbstractBean(0) {
_add(new configItem("protocol", &protocol, itemType::integer));
_add(new configItem("authPayloadType", &authPayloadType, itemType::integer));
_add(new configItem("authPayload", &authPayload, itemType::string));
_add(new configItem("obfsPassword", &obfsPassword, itemType::string));
_add(new configItem("uploadMbps", &uploadMbps, itemType::integer));
_add(new configItem("downloadMbps", &downloadMbps, itemType::integer));
_add(new configItem("streamReceiveWindow", &streamReceiveWindow, itemType::integer64));
_add(new configItem("connectionReceiveWindow", &connectionReceiveWindow, itemType::integer64));
_add(new configItem("disableMtuDiscovery", &disableMtuDiscovery, itemType::boolean));
_add(new configItem("allowInsecure", &allowInsecure, itemType::boolean));
_add(new configItem("sni", &sni, itemType::string));
_add(new configItem("alpn", &alpn, itemType::string));
_add(new configItem("caText", &caText, itemType::string));
_add(new configItem("hopInterval", &hopInterval, itemType::integer));
_add(new configItem("hopPort", &hopPort, itemType::string));
};
QString DisplayAddress() override {
if (!hopPort.trimmed().isEmpty()) return WrapIPV6Host(serverAddress) + ":" + hopPort;
return ::DisplayAddress(serverAddress, serverPort);
}
QString DisplayCoreType() override { return NeedExternal(false, false) == 0 ? software_core_name : "Hysteria"; };
QString DisplayType() override { return "Hysteria"; };
int NeedExternal(bool isFirstProfile, bool isVPN) override;
ExternalBuildResult BuildExternal(int mapping_port, int socks_port, int external_stat) override;
CoreObjOutboundBuildResult BuildCoreObjSingBox() override;
bool TryParseLink(const QString &link);
QString ToShareLink() override;
};
} // namespace NekoRay::fmt

View File

@@ -176,4 +176,41 @@ namespace NekoRay::fmt {
return !(username.isEmpty() || password.isEmpty()); return !(username.isEmpty() || password.isEmpty());
} }
bool HysteriaBean::TryParseLink(const QString &link) {
// https://hysteria.network/docs/uri-scheme/
auto url = QUrl(link);
auto query = QUrlQuery(url.query());
if (url.host().isEmpty() || url.port() == -1 || !query.hasQueryItem("upmbps") || !query.hasQueryItem("downmbps")) return false;
name = url.fragment();
serverAddress = url.host();
serverPort = url.port();
serverAddress = url.host(); // default sni
hopPort = query.queryItemValue("mport");
obfsPassword = query.queryItemValue("obfsParam");
allowInsecure = query.queryItemValue("insecure") == "1";
uploadMbps = query.queryItemValue("upmbps").toInt();
downloadMbps = query.queryItemValue("downmbps").toInt();
auto protocolStr = (query.hasQueryItem("protocol") ? query.queryItemValue("protocol") : "udp").toLower();
if (protocolStr == "faketcp") {
protocol = fmt::HysteriaBean::hysteria_protocol_facktcp;
} else if (protocolStr.startsWith("wechat")) {
protocol = fmt::HysteriaBean::hysteria_protocol_wechat_video;
}
if (query.hasQueryItem("auth")) {
authPayload = query.queryItemValue("auth");
authPayloadType = fmt::HysteriaBean::hysteria_auth_string;
}
alpn = query.queryItemValue("alpn");
sni = FIRST_OR_SECOND(query.queryItemValue("peer"), query.queryItemValue("sni"));
connectionReceiveWindow = query.queryItemValue("recv_window").toInt();
streamReceiveWindow = query.queryItemValue("recv_window_conn").toInt();
return true;
}
} // namespace NekoRay::fmt } // namespace NekoRay::fmt

View File

@@ -1,21 +1,6 @@
#pragma once #pragma once
namespace Preset { namespace Preset {
namespace Hysteria {
inline const char *command = "--no-check -c %config%";
inline const char *config =
"{\n"
" \"server\": \"127.0.0.1:%mapping_port%\",\n"
" \"server_name\": \"example.com\",\n"
" \"obfs\": \"fuck me till the daylight\",\n"
" \"up_mbps\": 10,\n"
" \"down_mbps\": 50,\n"
" \"socks5\": {\n"
" \"listen\": \"127.0.0.1:%socks_port%\"\n"
" }\n"
"}";
} // namespace Hysteria
namespace V2Ray { namespace V2Ray {
inline QStringList UtlsFingerPrint = {"", "randomized", "randomizedalpn", "randomizednoalpn", "firefox_auto", "firefox_55", "firefox_56", "firefox_63", "firefox_65", "firefox_99", "firefox_102", "firefox_105", "chrome_auto", "chrome_58", "chrome_62", "chrome_70", "chrome_72", "chrome_83", "chrome_87", "chrome_96", "chrome_100", "chrome_102", "ios_auto", "ios_11_1", "ios_12_1", "ios_13", "ios_14", "android_11_okhttp", "edge_auto", "edge_85", "edge_106", "safari_auto", "safari_16_0", "360_auto", "360_7_5", "360_11_0", "qq_auto", "qq_11_1"}; inline QStringList UtlsFingerPrint = {"", "randomized", "randomizedalpn", "randomizednoalpn", "firefox_auto", "firefox_55", "firefox_56", "firefox_63", "firefox_65", "firefox_99", "firefox_102", "firefox_105", "chrome_auto", "chrome_58", "chrome_62", "chrome_70", "chrome_72", "chrome_83", "chrome_87", "chrome_96", "chrome_100", "chrome_102", "ios_auto", "ios_11_1", "ios_12_1", "ios_13", "ios_14", "android_11_okhttp", "edge_auto", "edge_85", "edge_106", "safari_auto", "safari_16_0", "360_auto", "360_7_5", "360_11_0", "qq_auto", "qq_11_1"};
} // namespace V2Ray } // namespace V2Ray

View File

@@ -6,4 +6,5 @@
#include "VMessBean.hpp" #include "VMessBean.hpp"
#include "TrojanVLESSBean.hpp" #include "TrojanVLESSBean.hpp"
#include "NaiveBean.hpp" #include "NaiveBean.hpp"
#include "HysteriaBean.hpp"
#include "CustomBean.hpp" #include "CustomBean.hpp"

View File

@@ -17,16 +17,17 @@
// NekoRay Save&Load // NekoRay Save&Load
#define P_C_LOAD_STRING(a) CACHE.a = bean->a;
#define P_C_SAVE_STRING(a) bean->a = CACHE.a;
#define D_C_LOAD_STRING(a) CACHE.a = NekoRay::dataStore->a;
#define D_C_SAVE_STRING(a) NekoRay::dataStore->a = CACHE.a;
#define P_LOAD_STRING(a) ui->a->setText(bean->a); #define P_LOAD_STRING(a) ui->a->setText(bean->a);
#define P_SAVE_STRING(a) bean->a = ui->a->text(); #define P_SAVE_STRING(a) bean->a = ui->a->text();
#define P_SAVE_STRING_QTEXTEDIT(a) bean->a = ui->a->toPlainText(); #define P_SAVE_STRING_QTEXTEDIT(a) bean->a = ui->a->toPlainText();
#define D_LOAD_STRING(a) ui->a->setText(NekoRay::dataStore->a); #define D_LOAD_STRING(a) ui->a->setText(NekoRay::dataStore->a);
#define D_SAVE_STRING(a) NekoRay::dataStore->a = ui->a->text(); #define D_SAVE_STRING(a) NekoRay::dataStore->a = ui->a->text();
#define D_SAVE_STRING_QTEXTEDIT(a) NekoRay::dataStore->a = ui->a->toPlainText(); #define D_SAVE_STRING_QTEXTEDIT(a) NekoRay::dataStore->a = ui->a->toPlainText();
#define P_C_LOAD_STRING(a) CACHE.a = bean->a;
#define P_C_SAVE_STRING(a) bean->a = CACHE.a;
#define D_C_LOAD_STRING(a) CACHE.a = NekoRay::dataStore->a;
#define D_C_SAVE_STRING(a) NekoRay::dataStore->a = CACHE.a;
#define P_LOAD_INT(a) \ #define P_LOAD_INT(a) \
ui->a->setText(Int2String(bean->a)); \ ui->a->setText(Int2String(bean->a)); \
ui->a->setValidator(QRegExpValidator_Number); ui->a->setValidator(QRegExpValidator_Number);
@@ -35,10 +36,14 @@
ui->a->setText(Int2String(NekoRay::dataStore->a)); \ ui->a->setText(Int2String(NekoRay::dataStore->a)); \
ui->a->setValidator(QRegExpValidator_Number); ui->a->setValidator(QRegExpValidator_Number);
#define D_SAVE_INT(a) NekoRay::dataStore->a = ui->a->text().toInt(); #define D_SAVE_INT(a) NekoRay::dataStore->a = ui->a->text().toInt();
#define P_LOAD_COMBO(a) ui->a->setCurrentText(bean->a); #define P_LOAD_COMBO_STR(a) ui->a->setCurrentText(bean->a);
#define P_SAVE_COMBO(a) bean->a = ui->a->currentText(); #define P_SAVE_COMBO_STR(a) bean->a = ui->a->currentText();
#define P_LOAD_COMBO_INT(a) ui->a->setCurrentIndex(bean->a);
#define P_SAVE_COMBO_INT(a) bean->a = ui->a->currentIndex();
#define D_LOAD_BOOL(a) ui->a->setChecked(NekoRay::dataStore->a); #define D_LOAD_BOOL(a) ui->a->setChecked(NekoRay::dataStore->a);
#define D_SAVE_BOOL(a) NekoRay::dataStore->a = ui->a->isChecked(); #define D_SAVE_BOOL(a) NekoRay::dataStore->a = ui->a->isChecked();
#define P_LOAD_BOOL(a) ui->a->setChecked(bean->a);
#define P_SAVE_BOOL(a) bean->a = ui->a->isChecked();
#define D_LOAD_INT_ENABLE(i, e) \ #define D_LOAD_INT_ENABLE(i, e) \
if (NekoRay::dataStore->i > 0) { \ if (NekoRay::dataStore->i > 0) { \

View File

@@ -25,6 +25,8 @@ inline std::function<void(QString, QString)> MW_dialog_message;
// String // String
#define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a
inline const QString UNICODE_LRO = QString::fromUtf8(QByteArray::fromHex("E280AD")); inline const QString UNICODE_LRO = QString::fromUtf8(QByteArray::fromHex("E280AD"));
#define Int2String(num) QString::number(num) #define Int2String(num) QString::number(num)

View File

@@ -16,8 +16,6 @@
#endif #endif
#define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a
namespace NekoRay::sub { namespace NekoRay::sub {
GroupUpdater *groupUpdater = new GroupUpdater; GroupUpdater *groupUpdater = new GroupUpdater;
@@ -130,28 +128,9 @@ namespace NekoRay::sub {
// Hysteria // Hysteria
if (str.startsWith("hysteria://")) { if (str.startsWith("hysteria://")) {
needFix = false; needFix = false;
// https://github.com/HyNetwork/hysteria/wiki/URI-Scheme ent = ProfileManager::NewProxyEntity("hysteria");
ent = ProfileManager::NewProxyEntity("custom"); auto ok = ent->HysteriaBean()->TryParseLink(str);
auto bean = ent->CustomBean(); if (!ok) return;
auto url = QUrl(str);
auto query = QUrlQuery(url.query());
if (url.host().isEmpty() || url.port() == -1 || !query.hasQueryItem("upmbps")) return;
bean->name = url.fragment();
bean->serverAddress = url.host();
bean->serverPort = url.port();
bean->core = "hysteria";
bean->command = QString(Preset::Hysteria::command).split(" ");
auto result = QString2QJsonObject(Preset::Hysteria::config);
result["server_name"] = url.host(); // default sni
result["obfs"] = query.queryItemValue("obfsParam");
result["insecure"] = query.queryItemValue("insecure") == "1";
result["up_mbps"] = query.queryItemValue("upmbps").toInt();
result["down_mbps"] = query.queryItemValue("downmbps").toInt();
result["protocol"] = query.hasQueryItem("protocol") ? query.queryItemValue("protocol") : "udp";
if (query.hasQueryItem("auth")) result["auth_str"] = query.queryItemValue("auth");
if (query.hasQueryItem("alpn")) result["alpn"] = query.queryItemValue("alpn");
if (query.hasQueryItem("peer")) result["server_name"] = query.queryItemValue("peer");
bean->config_simple = QJsonObject2QString(result, false);
} }
if (ent == nullptr) return; if (ent == nullptr) return;
@@ -237,7 +216,6 @@ namespace NekoRay::sub {
if (type == "ss" || type == "ssr") type = "shadowsocks"; if (type == "ss" || type == "ssr") type = "shadowsocks";
if (type == "socks5") type = "socks"; if (type == "socks5") type = "socks";
if (type == "hysteria") type = "custom";
auto ent = ProfileManager::NewProxyEntity(type); auto ent = ProfileManager::NewProxyEntity(type);
if (ent->bean->version == -114514) continue; if (ent->bean->version == -114514) continue;
@@ -379,45 +357,37 @@ namespace NekoRay::sub {
} }
} }
} else if (type_clash == "hysteria") { } else if (type_clash == "hysteria") {
if (!IS_NEKO_BOX) { auto bean = ent->HysteriaBean();
MW_show_log("Found Clash Meta format hysteria. This is only supported in NekoBox, please switch the core.");
continue; bean->allowInsecure = Node2Bool(proxy["skip-cert-verify"]);
auto alpn = Node2QStringList(proxy["alpn"]);
if (!alpn.isEmpty()) bean->alpn = alpn[0];
bean->sni = Node2QString(proxy["sni"]);
bean->name = Node2QString(proxy["name"]);
bean->serverAddress = Node2QString(proxy["server"]);
bean->serverPort = Node2Int(proxy["port"]);
if (bean->serverPort == 0) bean->hopPort = Node2QString(proxy["port"]);
auto auth_str = FIRST_OR_SECOND(Node2QString(proxy["auth_str"]), Node2QString(proxy["auth-str"]));
auto auth = Node2QString(proxy["auth"]);
if (!auth_str.isEmpty()) {
bean->authPayloadType = fmt::HysteriaBean::hysteria_auth_string;
bean->authPayload = auth_str;
}
if (!auth.isEmpty()) {
bean->authPayloadType = fmt::HysteriaBean::hysteria_auth_base64;
bean->authPayload = auth;
} }
auto bean = ent->CustomBean(); if (Node2Bool(proxy["disable_mtu_discovery"]) || Node2Bool(proxy["disable-mtu-discovery"])) bean->disableMtuDiscovery = true;
bean->core = "internal"; bean->streamReceiveWindow = Node2Int(proxy["recv-window"]);
bean->connectionReceiveWindow = Node2Int(proxy["recv-window-conn"]);
QJsonObject coreTlsObj{ auto upMbps = Node2QString(proxy["up"]).split(" ")[0].toInt();
{"enabled", true}, auto downMbps = Node2QString(proxy["down"]).split(" ")[0].toInt();
{"insecure", Node2Bool(proxy["skip-cert-verify"])}, if (upMbps > 0) bean->uploadMbps = upMbps;
{"alpn", QList2QJsonArray(Node2QStringList(proxy["alpn"]))}, if (downMbps > 0) bean->downloadMbps = downMbps;
{"server_name", Node2QString(proxy["sni"])},
};
QJsonObject coreHysteriaObj{
{"type", "hysteria"},
{"tag", Node2QString(proxy["name"])},
{"server", Node2QString(proxy["server"])},
{"server_port", Node2Int(proxy["port"])},
{"auth_str", FIRST_OR_SECOND(Node2QString(proxy["auth_str"]), Node2QString(proxy["auth-str"]))},
{"disable_mtu_discovery", Node2Bool(proxy["disable_mtu_discovery"])},
{"recv_window", Node2Int(proxy["recv-window"])},
{"recv_window_conn", Node2Int(proxy["recv-window-conn"])},
{"tls", coreTlsObj},
};
if (!Node2QString(proxy["up"]).contains("bps")) {
coreHysteriaObj["up_mbps"] = Node2Int(proxy["up"]);
} else {
coreHysteriaObj["up"] = Node2QString(proxy["up"]);
}
if (!Node2QString(proxy["down"]).contains("bps")) {
coreHysteriaObj["down_mbps"] = Node2Int(proxy["down"]);
} else {
coreHysteriaObj["down"] = Node2QString(proxy["down"]);
}
bean->config_simple = QJsonObject2QString(coreHysteriaObj, false);
} else { } else {
continue; continue;
} }

View File

@@ -719,10 +719,6 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun</source>
<source>Config Suffix</source> <source>Config Suffix</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Please read the documentation. If you don&apos;t understand, use a share link instead.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Outbound JSON, please read the documentation.</source> <source>Outbound JSON, please read the documentation.</source>
<translation>JSON خروجی، لطفاً مستندات را بخوانید.</translation> <translation>JSON خروجی، لطفاً مستندات را بخوانید.</translation>
@@ -752,6 +748,57 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>EditHysteria</name>
<message>
<source>Certificate</source>
<translation type="unfinished">گواهی</translation>
</message>
<message>
<source>Auth Type</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Protocol</source>
<translation type="unfinished">پروتکل</translation>
</message>
<message>
<source>Download (Mbps)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Disable MTU Discovery</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hop Interval (s)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Allow Insecure</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hop Port</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Upload (Mbps)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Obfs Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SNI</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Auth Payload</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>EditNaive</name> <name>EditNaive</name>
<message> <message>

View File

@@ -716,10 +716,6 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun</translation>
<source>Outbound JSON, please read the documentation.</source> <source>Outbound JSON, please read the documentation.</source>
<translation> JSON </translation> <translation> JSON </translation>
</message> </message>
<message>
<source>Please read the documentation. If you don&apos;t understand, use a share link instead.</source>
<translation> hysteria:// 链接。</translation>
</message>
<message> <message>
<source>Config Suffix</source> <source>Config Suffix</source>
<translation></translation> <translation></translation>
@@ -745,6 +741,57 @@ https://matsuridayo.github.io/n-configuration/#vpn-tun</translation>
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>EditHysteria</name>
<message>
<source>Certificate</source>
<translation></translation>
</message>
<message>
<source>Auth Type</source>
<translation></translation>
</message>
<message>
<source>Protocol</source>
<translation></translation>
</message>
<message>
<source>Download (Mbps)</source>
<translation> (Mbps)</translation>
</message>
<message>
<source>Disable MTU Discovery</source>
<translation> MTU </translation>
</message>
<message>
<source>Hop Interval (s)</source>
<translation> ()</translation>
</message>
<message>
<source>Allow Insecure</source>
<translation></translation>
</message>
<message>
<source>Hop Port</source>
<translation></translation>
</message>
<message>
<source>Upload (Mbps)</source>
<translation> (Mbps)</translation>
</message>
<message>
<source>Obfs Password</source>
<translation></translation>
</message>
<message>
<source>SNI</source>
<translation>SNI</translation>
</message>
<message>
<source>Auth Payload</source>
<translation></translation>
</message>
</context>
<context> <context>
<name>EditNaive</name> <name>EditNaive</name>
<message> <message>

View File

@@ -7,6 +7,7 @@
#include "ui/edit/edit_vmess.h" #include "ui/edit/edit_vmess.h"
#include "ui/edit/edit_trojan_vless.h" #include "ui/edit/edit_trojan_vless.h"
#include "ui/edit/edit_naive.h" #include "ui/edit/edit_naive.h"
#include "ui/edit/edit_hysteria.h"
#include "ui/edit/edit_custom.h" #include "ui/edit/edit_custom.h"
#include "fmt/includes.h" #include "fmt/includes.h"
@@ -123,7 +124,7 @@ DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId,
LOAD_TYPE("vmess"); LOAD_TYPE("vmess");
LOAD_TYPE("vless"); LOAD_TYPE("vless");
LOAD_TYPE("naive"); LOAD_TYPE("naive");
ui->type->addItem("Hysteria", "hysteria"); LOAD_TYPE("hysteria");
ui->type->addItem(tr("Custom (%1 outbound)").arg(software_core_name), "internal"); ui->type->addItem(tr("Custom (%1 outbound)").arg(software_core_name), "internal");
ui->type->addItem(tr("Custom (%1 config)").arg(software_core_name), "internal-full"); ui->type->addItem(tr("Custom (%1 config)").arg(software_core_name), "internal-full");
ui->type->addItem(tr("Custom (Extra Core)"), "custom"); ui->type->addItem(tr("Custom (Extra Core)"), "custom");
@@ -177,7 +178,11 @@ void DialogEditProfile::typeSelected(const QString &newType) {
auto _innerWidget = new EditNaive(this); auto _innerWidget = new EditNaive(this);
innerWidget = _innerWidget; innerWidget = _innerWidget;
innerEditor = _innerWidget; innerEditor = _innerWidget;
} else if (type == "custom" || type == "internal" || type == "internal-full" || type == "hysteria") { } else if (type == "hysteria") {
auto _innerWidget = new EditHysteria(this);
innerWidget = _innerWidget;
innerEditor = _innerWidget;
} else if (type == "custom" || type == "internal" || type == "internal-full") {
auto _innerWidget = new EditCustom(this); auto _innerWidget = new EditCustom(this);
innerWidget = _innerWidget; innerWidget = _innerWidget;
innerEditor = _innerWidget; innerEditor = _innerWidget;

View File

@@ -26,10 +26,10 @@ EditCustom::~EditCustom() {
} }
#define SAVE_CUSTOM_BEAN \ #define SAVE_CUSTOM_BEAN \
P_SAVE_COMBO(core) \ P_SAVE_COMBO_STR(core) \
bean->command = ui->command->text().split(" "); \ bean->command = ui->command->text().split(" "); \
P_SAVE_STRING_QTEXTEDIT(config_simple) \ P_SAVE_STRING_QTEXTEDIT(config_simple) \
P_SAVE_COMBO(config_suffix) \ P_SAVE_COMBO_STR(config_suffix) \
P_SAVE_INT(mapping_port) \ P_SAVE_INT(mapping_port) \
P_SAVE_INT(socks_port) P_SAVE_INT(socks_port)
@@ -43,13 +43,7 @@ void EditCustom::onStart(QSharedPointer<NekoRay::ProxyEntity> _ent) {
if (key == "naive" || key == "hysteria") continue; if (key == "naive" || key == "hysteria") continue;
ui->core->addItem(key); ui->core->addItem(key);
} }
if (preset_core == "hysteria") { if (preset_core == "internal") {
preset_command = Preset::Hysteria::command;
preset_config = Preset::Hysteria::config;
ui->config_simple->setPlaceholderText("");
ui->core->hide();
ui->core_l->setText(tr("Please read the documentation. If you don't understand, use a share link instead."));
} else if (preset_core == "internal") {
preset_command = preset_config = ""; preset_command = preset_config = "";
ui->config_simple->setPlaceholderText( ui->config_simple->setPlaceholderText(
"{\n" "{\n"
@@ -66,10 +60,10 @@ void EditCustom::onStart(QSharedPointer<NekoRay::ProxyEntity> _ent) {
} }
// load core ui // load core ui
P_LOAD_COMBO(core) P_LOAD_COMBO_STR(core)
ui->command->setText(bean->command.join(" ")); ui->command->setText(bean->command.join(" "));
P_LOAD_STRING(config_simple) P_LOAD_STRING(config_simple)
P_LOAD_COMBO(config_suffix) P_LOAD_COMBO_STR(config_suffix)
P_LOAD_INT(mapping_port) P_LOAD_INT(mapping_port)
P_LOAD_INT(socks_port) P_LOAD_INT(socks_port)

72
ui/edit/edit_hysteria.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include "edit_hysteria.h"
#include "ui_edit_hysteria.h"
#include "fmt/HysteriaBean.hpp"
#include <QInputDialog>
EditHysteria::EditHysteria(QWidget *parent) : QWidget(parent), ui(new Ui::EditHysteria) {
ui->setupUi(this);
}
EditHysteria::~EditHysteria() {
delete ui;
}
void EditHysteria::onStart(QSharedPointer<NekoRay::ProxyEntity> _ent) {
this->ent = _ent;
auto bean = this->ent->HysteriaBean();
P_LOAD_STRING(hopPort);
P_LOAD_INT(hopInterval);
P_LOAD_INT(uploadMbps);
P_LOAD_INT(downloadMbps);
P_LOAD_COMBO_INT(protocol);
P_LOAD_BOOL(disableMtuDiscovery)
P_LOAD_STRING(obfsPassword);
P_LOAD_COMBO_INT(authPayloadType);
P_LOAD_STRING(authPayload);
P_LOAD_STRING(sni);
P_LOAD_STRING(alpn);
P_LOAD_BOOL(allowInsecure)
P_C_LOAD_STRING(caText);
P_LOAD_INT(streamReceiveWindow);
P_LOAD_INT(connectionReceiveWindow);
}
bool EditHysteria::onEnd() {
auto bean = this->ent->HysteriaBean();
P_SAVE_STRING(hopPort);
P_SAVE_INT(hopInterval);
P_SAVE_INT(uploadMbps);
P_SAVE_INT(downloadMbps);
P_SAVE_COMBO_INT(protocol);
P_SAVE_BOOL(disableMtuDiscovery)
P_SAVE_STRING(obfsPassword);
P_SAVE_COMBO_INT(authPayloadType);
P_SAVE_STRING(authPayload);
P_SAVE_STRING(sni);
P_SAVE_STRING(alpn);
P_SAVE_BOOL(allowInsecure)
P_C_SAVE_STRING(caText);
P_SAVE_INT(streamReceiveWindow);
P_SAVE_INT(connectionReceiveWindow);
return true;
}
QList<QPair<QPushButton *, QString>> EditHysteria::get_editor_cached() {
return {
{ui->certificate, CACHE.caText},
};
}
void EditHysteria::on_certificate_clicked() {
bool ok;
auto txt = QInputDialog::getMultiLineText(this, tr("Certificate"), "", CACHE.caText, &ok);
if (ok) {
CACHE.caText = txt;
editor_cache_updated();
}
}

37
ui/edit/edit_hysteria.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <QWidget>
#include "profile_editor.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class EditHysteria;
}
QT_END_NAMESPACE
class EditHysteria : public QWidget, public ProfileEditor {
Q_OBJECT
public:
explicit EditHysteria(QWidget *parent = nullptr);
~EditHysteria() override;
void onStart(QSharedPointer<NekoRay::ProxyEntity> _ent) override;
bool onEnd() override;
QList<QPair<QPushButton *, QString>> get_editor_cached() override;
private:
Ui::EditHysteria *ui;
QSharedPointer<NekoRay::ProxyEntity> ent;
struct {
QString caText;
} CACHE;
private slots:
void on_certificate_clicked();
};

231
ui/edit/edit_hysteria.ui Normal file
View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditHysteria</class>
<widget class="QWidget" name="EditHysteria">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">EditHysteria</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="MyLineEdit" name="hopPort"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Auth Type</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Protocol</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="MyLineEdit" name="obfsPassword"/>
</item>
<item row="10" column="1">
<widget class="MyLineEdit" name="sni"/>
</item>
<item row="2" column="1">
<widget class="MyLineEdit" name="uploadMbps"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Download (Mbps)</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="protocol">
<item>
<property name="text">
<string notr="true">QUIC</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">FakeTCP</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">wechat-video</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QCheckBox" name="disableMtuDiscovery">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Disable MTU Discovery</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Hop Interval (s)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="MyLineEdit" name="hopInterval"/>
</item>
<item row="13" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Certificate</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QComboBox" name="authPayloadType">
<item>
<property name="text">
<string notr="true">NONE</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">STRING</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">BASE64</string>
</property>
</item>
</widget>
</item>
<item row="12" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="MyLineEdit" name="alpn"/>
</item>
<item>
<widget class="QCheckBox" name="allowInsecure">
<property name="text">
<string>Allow Insecure</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Hop Port</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QPushButton" name="certificate">
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Upload (Mbps)</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Obfs Password</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>SNI</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="MyLineEdit" name="downloadMbps"/>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string notr="true">Only 1 value</string>
</property>
<property name="text">
<string notr="true">ALPN</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="MyLineEdit" name="streamReceiveWindow"/>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Auth Payload</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="MyLineEdit" name="authPayload"/>
</item>
<item row="15" column="1">
<widget class="MyLineEdit" name="connectionReceiveWindow"/>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string notr="true">recv_window</string>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string notr="true">recv_window_conn</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MyLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/widget/MyLineEdit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>uploadMbps</tabstop>
<tabstop>downloadMbps</tabstop>
<tabstop>sni</tabstop>
<tabstop>certificate</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -19,7 +19,7 @@ void EditNaive::onStart(QSharedPointer<NekoRay::ProxyEntity> _ent) {
P_LOAD_STRING(username); P_LOAD_STRING(username);
P_LOAD_STRING(password); P_LOAD_STRING(password);
P_LOAD_COMBO(protocol); P_LOAD_COMBO_STR(protocol);
P_C_LOAD_STRING(extra_headers); P_C_LOAD_STRING(extra_headers);
P_LOAD_STRING(sni); P_LOAD_STRING(sni);
P_C_LOAD_STRING(certificate); P_C_LOAD_STRING(certificate);
@@ -31,7 +31,7 @@ bool EditNaive::onEnd() {
P_SAVE_STRING(username); P_SAVE_STRING(username);
P_SAVE_STRING(password); P_SAVE_STRING(password);
P_SAVE_COMBO(protocol); P_SAVE_COMBO_STR(protocol);
P_C_SAVE_STRING(extra_headers); P_C_SAVE_STRING(extra_headers);
P_SAVE_STRING(sni); P_SAVE_STRING(sni);
P_C_SAVE_STRING(certificate); P_C_SAVE_STRING(certificate);