diff --git a/CMakeLists.txt b/CMakeLists.txt index db9b752..60c9746 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,9 @@ set(PROJECT_SOURCES include/ui/profile/edit_trojan_vless.h src/ui/profile/edit_trojan_vless.cpp include/ui/profile/edit_trojan_vless.ui + include/ui/profile/edit_anytls.h + src/ui/profile/edit_anytls.cpp + include/ui/profile/edit_anytls.ui include/ui/profile/edit_quic.h src/ui/profile/edit_quic.cpp diff --git a/README.md b/README.md index 8fcc46c..e625890 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Apple platforms have a very strict security policy and since Throne does not hav - TUIC - Hysteria - Hysteria2 +- AnyTls - Wireguard - SSH - Custom Outbound diff --git a/include/configs/proxy/AnyTlsBean.hpp b/include/configs/proxy/AnyTlsBean.hpp new file mode 100644 index 0000000..5aab7d9 --- /dev/null +++ b/include/configs/proxy/AnyTlsBean.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "AbstractBean.hpp" +#include "V2RayStreamSettings.hpp" +#include "Preset.hpp" + +namespace Configs { + class AnyTlsBean : public AbstractBean { + public: + QString password = ""; + int idle_session_check_interval = 30; + int idle_session_timeout = 30; + int min_idle_session = 0; + + std::shared_ptr stream = std::make_shared(); + + AnyTlsBean() : AbstractBean(0) { + _add(new configItem("password", &password, itemType::string)); + _add(new configItem("idle_session_check_interval", &idle_session_check_interval, itemType::integer)); + _add(new configItem("idle_session_timeout", &idle_session_timeout, itemType::integer)); + _add(new configItem("min_idle_session", &min_idle_session, itemType::integer)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + }; + + QString DisplayType() override { return "AnyTls"; }; + + CoreObjOutboundBuildResult BuildCoreObjSingBox() override; + + bool TryParseLink(const QString &link); + + bool TryParseJson(const QJsonObject &obj); + + QString ToShareLink() override; + }; +} // namespace Configs diff --git a/include/configs/proxy/includes.h b/include/configs/proxy/includes.h index 8219e34..2e7cc5d 100644 --- a/include/configs/proxy/includes.h +++ b/include/configs/proxy/includes.h @@ -6,6 +6,7 @@ #include "VMessBean.hpp" #include "TrojanVLESSBean.hpp" #include "QUICBean.hpp" +#include "AnyTlsBean.hpp" #include "WireguardBean.h" #include "SSHBean.h" #include "CustomBean.hpp" diff --git a/include/dataStore/ProxyEntity.hpp b/include/dataStore/ProxyEntity.hpp index 482c2c0..080c0e0 100644 --- a/include/dataStore/ProxyEntity.hpp +++ b/include/dataStore/ProxyEntity.hpp @@ -18,6 +18,8 @@ namespace Configs { class QUICBean; + class AnyTlsBean; + class WireguardBean; class SSHBean; @@ -76,6 +78,10 @@ namespace Configs { return (Configs::QUICBean *) bean.get(); }; + [[nodiscard]] Configs::AnyTlsBean *AnyTlsBean() const { + return (Configs::AnyTlsBean *) bean.get(); + }; + [[nodiscard]] Configs::WireguardBean *WireguardBean() const { return (Configs::WireguardBean *) bean.get(); }; diff --git a/include/ui/profile/edit_anytls.h b/include/ui/profile/edit_anytls.h new file mode 100644 index 0000000..b1bea70 --- /dev/null +++ b/include/ui/profile/edit_anytls.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "profile_editor.h" +#include "ui_edit_anytls.h" + +QT_BEGIN_NAMESPACE +namespace Ui { + class EditAnyTls; +} +QT_END_NAMESPACE + +class EditAnyTls : public QWidget, public ProfileEditor { + Q_OBJECT + +public: + explicit EditAnyTls(QWidget *parent = nullptr); + + ~EditAnyTls() override; + + void onStart(std::shared_ptr _ent) override; + + bool onEnd() override; + +private: + Ui::EditAnyTls *ui; + std::shared_ptr ent; +}; diff --git a/include/ui/profile/edit_anytls.ui b/include/ui/profile/edit_anytls.ui new file mode 100644 index 0000000..95a5997 --- /dev/null +++ b/include/ui/profile/edit_anytls.ui @@ -0,0 +1,71 @@ + + + EditAnyTls + + + + 0 + 0 + 400 + 300 + + + + EditAnyTls + + + + + + Password + + + + + + + + + + Idle Session Check Interval + + + + + + + + + + Idle Session Timeout + + + + + + + + + + Min Idle Session + + + + + + + + + + + MyLineEdit + QLineEdit +
include/ui/utils/MyLineEdit.h
+
+
+ + password + + + +
diff --git a/src/configs/ConfigBuilder.cpp b/src/configs/ConfigBuilder.cpp index 68ca848..663846e 100644 --- a/src/configs/ConfigBuilder.cpp +++ b/src/configs/ConfigBuilder.cpp @@ -374,7 +374,7 @@ namespace Configs { auto stream = GetStreamSettings(ent->bean.get()); if (stream != nullptr) { - if (stream->network == "grpc" || stream->network == "quic" || (stream->network == "http" && stream->security == "tls")) { + if (stream->network == "grpc" || stream->network == "quic" || stream->network == "anytls" || (stream->network == "http" && stream->security == "tls")) { needMux = false; } } diff --git a/src/configs/proxy/Bean2CoreObj_box.cpp b/src/configs/proxy/Bean2CoreObj_box.cpp index dc20243..89904a9 100644 --- a/src/configs/proxy/Bean2CoreObj_box.cpp +++ b/src/configs/proxy/Bean2CoreObj_box.cpp @@ -171,6 +171,24 @@ namespace Configs { return result; } + CoreObjOutboundBuildResult AnyTlsBean::BuildCoreObjSingBox() { + CoreObjOutboundBuildResult result; + + QJsonObject outbound{ + {"type", "anytls"}, + {"server", serverAddress}, + {"server_port", serverPort}, + {"password", password}, + {"idle_session_check_interval", Int2String(idle_session_check_interval)+"s"}, + {"idle_session_timeout", Int2String(idle_session_timeout)+"s"}, + {"min_idle_session", min_idle_session}, + }; + + stream->BuildStreamSettingsSingBox(&outbound); + result.outbound = outbound; + return result; + } + CoreObjOutboundBuildResult VMessBean::BuildCoreObjSingBox() { CoreObjOutboundBuildResult result; diff --git a/src/configs/proxy/Bean2Link.cpp b/src/configs/proxy/Bean2Link.cpp index 4d8b2b7..a2322f1 100644 --- a/src/configs/proxy/Bean2Link.cpp +++ b/src/configs/proxy/Bean2Link.cpp @@ -23,6 +23,35 @@ namespace Configs { return url.toString(QUrl::FullyEncoded); } + QString AnyTlsBean::ToShareLink() { + QUrl url; + QUrlQuery query; + url.setScheme("anytls"); + url.setUserName(password); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(name); + + // security + query.addQueryItem("security", stream->security == "" ? "none" : stream->security); + + if (!stream->sni.isEmpty()) query.addQueryItem("sni", stream->sni); + if (!stream->alpn.isEmpty()) query.addQueryItem("alpn", stream->alpn); + if (stream->allow_insecure) query.addQueryItem("insecure", "1"); + if (!stream->utlsFingerprint.isEmpty()) query.addQueryItem("fp", stream->utlsFingerprint); + if (stream->enable_tls_fragment) query.addQueryItem("fragment", "1"); + if (!stream->tls_fragment_fallback_delay.isEmpty()) query.addQueryItem("fragment_fallback_delay", stream->tls_fragment_fallback_delay); + if (stream->enable_tls_record_fragment) query.addQueryItem("record_fragment", "1"); + + if (stream->security == "reality") { + query.addQueryItem("pbk", stream->reality_pbk); + if (!stream->reality_sid.isEmpty()) query.addQueryItem("sid", stream->reality_sid); + } + + url.setQuery(query); + return url.toString(QUrl::FullyEncoded); + } + QString TrojanVLESSBean::ToShareLink() { QUrl url; QUrlQuery query; diff --git a/src/configs/proxy/Json2Bean.cpp b/src/configs/proxy/Json2Bean.cpp index 615445c..cc5b771 100644 --- a/src/configs/proxy/Json2Bean.cpp +++ b/src/configs/proxy/Json2Bean.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "include/configs/proxy/ExtraCore.h" @@ -213,6 +214,32 @@ namespace Configs return true; } + bool AnyTlsBean::TryParseJson(const QJsonObject& obj) + { + name = obj["tag"].toString(); + serverAddress = obj["server"].toString(); + serverPort = obj["server_port"].toInt(); + password = obj["password"].toString(); + idle_session_check_interval = obj["idle_session_check_interval"].toInt(); + idle_session_timeout = obj["idle_session_timeout"].toInt(); + min_idle_session = obj["min_idle_session"].toInt(); + stream->security = obj["tls"].isObject() ? "tls" : ""; + if (obj["tls"].toObject()["reality"].toObject()["enabled"].toBool()) + { + stream->security = "reality"; + } + stream->reality_pbk = obj["tls"].toObject()["reality"].toObject()["public_key"].toString(); + stream->reality_sid = obj["tls"].toObject()["reality"].toObject()["short_id"].toString(); + stream->utlsFingerprint = obj["tls"].toObject()["utls"].toObject()["fingerprint"].toString(); + stream->enable_tls_fragment = obj["tls"].toObject()["fragment"].toBool(); + stream->tls_fragment_fallback_delay = obj["tls"].toObject()["fragment_fallback_delay"].toString(); + stream->enable_tls_record_fragment = obj["tls"].toObject()["record_fragment"].toBool(); + stream->sni = obj["tls"].toObject()["server_name"].toString(); + stream->alpn = obj["tls"].toObject()["alpn"].isArray() ? QJsonArray2QListString(obj["tls"].toObject()["alpn"].toArray()).join(",") : obj["tls"].toObject()["alpn"].toString(); + stream->allow_insecure = obj["tls"].toObject()["insecure"].toBool(); + return true; + } + bool WireguardBean::TryParseJson(const QJsonObject& obj) { name = obj["tag"].toString(); diff --git a/src/configs/proxy/Link2Bean.cpp b/src/configs/proxy/Link2Bean.cpp index 9c5abdc..db5c854 100644 --- a/src/configs/proxy/Link2Bean.cpp +++ b/src/configs/proxy/Link2Bean.cpp @@ -42,6 +42,43 @@ namespace Configs { return !serverAddress.isEmpty(); } + bool AnyTlsBean::TryParseLink(const QString &link) { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = GetQuery(url); + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + password = url.userName(); + if (serverPort == -1) serverPort = 443; + + // security + + stream->security = GetQueryValue(query, "security", "").replace("none", ""); + auto sni1 = GetQueryValue(query, "sni"); + auto sni2 = GetQueryValue(query, "peer"); + if (!sni1.isEmpty()) stream->sni = sni1; + if (!sni2.isEmpty()) stream->sni = sni2; + stream->alpn = GetQueryValue(query, "alpn"); + stream->allow_insecure = QStringList{"1", "true"}.contains(query.queryItemValue("insecure")); + stream->reality_pbk = GetQueryValue(query, "pbk", ""); + stream->reality_sid = GetQueryValue(query, "sid", ""); + stream->utlsFingerprint = GetQueryValue(query, "fp", ""); + if (query.queryItemValue("fragment") == "1") stream->enable_tls_fragment = true; + stream->tls_fragment_fallback_delay = query.queryItemValue("fragment_fallback_delay"); + if (query.queryItemValue("record_fragment") == "1") stream->enable_tls_record_fragment = true; + if (stream->utlsFingerprint.isEmpty()) { + stream->utlsFingerprint = dataStore->utlsFingerprint; + } + if (stream->security.isEmpty()) { + if (!sni1.isEmpty() || !sni2.isEmpty()) stream->security = "tls"; + if (!stream->reality_pbk.isEmpty()) stream->security = "reality"; + } + + return !(password.isEmpty() || serverAddress.isEmpty()); + } + bool TrojanVLESSBean::TryParseLink(const QString &link) { auto url = QUrl(link); if (!url.isValid()) return false; diff --git a/src/configs/sub/GroupUpdater.cpp b/src/configs/sub/GroupUpdater.cpp index 894430c..a443435 100644 --- a/src/configs/sub/GroupUpdater.cpp +++ b/src/configs/sub/GroupUpdater.cpp @@ -172,6 +172,13 @@ namespace Subscription { if (!ok) return; } + // AnyTls + if (str.startsWith("anytls://")) { + ent = Configs::ProfileManager::NewProxyEntity("anytls"); + auto ok = ent->AnyTlsBean()->TryParseLink(str); + if (!ok) return; + } + // Hysteria1 if (str.startsWith("hysteria://")) { needFix = false; @@ -290,6 +297,13 @@ namespace Subscription { if (!ok) continue; } + // AnyTls + if (out["type"] == "anytls") { + ent = Configs::ProfileManager::NewProxyEntity("anytls"); + auto ok = ent->AnyTlsBean()->TryParseJson(out); + if (!ok) continue; + } + // Hysteria1 if (out["type"] == "hysteria") { ent = Configs::ProfileManager::NewProxyEntity("hysteria"); @@ -526,7 +540,6 @@ namespace Subscription { if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; bean->stream->utlsFingerprint = Node2QString(proxy["client-fingerprint"]); - bean->stream->utlsFingerprint = Node2QString(proxy["client-fingerprint"]); if (bean->stream->utlsFingerprint.isEmpty()) { bean->stream->utlsFingerprint = Configs::dataStore->utlsFingerprint; } @@ -593,6 +606,24 @@ namespace Subscription { break; } } + } else if (type == "anytls") { + needFix = true; + auto bean = ent->AnyTlsBean(); + bean->password = Node2QString(proxy["password"]); + if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); + bean->stream->alpn = Node2QStringList(proxy["alpn"]).join(","); + bean->stream->utlsFingerprint = Node2QString(proxy["client-fingerprint"]); + if (bean->stream->utlsFingerprint.isEmpty()) { + bean->stream->utlsFingerprint = Configs::dataStore->utlsFingerprint; + } + + auto reality = NodeChild(proxy, {"reality-opts"}); + if (reality.is_mapping()) { + bean->stream->reality_pbk = Node2QString(reality["public-key"]); + bean->stream->reality_sid = Node2QString(reality["short-id"]); + } } else if (type == "hysteria") { auto bean = ent->QUICBean(); diff --git a/src/dataStore/Database.cpp b/src/dataStore/Database.cpp index 3e2fead..a85d1fe 100644 --- a/src/dataStore/Database.cpp +++ b/src/dataStore/Database.cpp @@ -176,6 +176,8 @@ namespace Configs { bean = new Configs::QUICBean(Configs::QUICBean::proxy_Hysteria2); } else if (type == "tuic") { bean = new Configs::QUICBean(Configs::QUICBean::proxy_TUIC); + } else if (type == "anytls") { + bean = new Configs::AnyTlsBean(); } else if (type == "wireguard") { bean = new Configs::WireguardBean(Configs::WireguardBean()); } else if (type == "ssh") { diff --git a/src/ui/profile/dialog_edit_profile.cpp b/src/ui/profile/dialog_edit_profile.cpp index d130744..9bf146e 100644 --- a/src/ui/profile/dialog_edit_profile.cpp +++ b/src/ui/profile/dialog_edit_profile.cpp @@ -6,6 +6,7 @@ #include "include/ui/profile/edit_vmess.h" #include "include/ui/profile/edit_trojan_vless.h" #include "include/ui/profile/edit_quic.h" +#include "include/ui/profile/edit_anytls.h" #include "include/ui/profile/edit_wireguard.h" #include "include/ui/profile/edit_ssh.h" #include "include/ui/profile/edit_custom.h" @@ -166,6 +167,7 @@ DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId, LOAD_TYPE("hysteria") LOAD_TYPE("hysteria2") LOAD_TYPE("tuic") + LOAD_TYPE("anytls") LOAD_TYPE("wireguard") LOAD_TYPE("ssh") ui->type->addItem(tr("Custom (%1 outbound)").arg(software_core_name), "internal"); @@ -233,6 +235,10 @@ void DialogEditProfile::typeSelected(const QString &newType) { auto _innerWidget = new EditQUIC(this); innerWidget = _innerWidget; innerEditor = _innerWidget; + } else if (type == "anytls") { + auto _innerWidget = new EditAnyTls(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; } else if (type == "wireguard") { auto _innerWidget = new EditWireguard(this); innerWidget = _innerWidget; @@ -375,7 +381,7 @@ void DialogEditProfile::typeSelected(const QString &newType) { ui->network->setVisible(false); ui->network_box->setVisible(false); } - if (type == "vmess" || type == "vless" || type == "trojan" || type == "http") { + if (type == "vmess" || type == "vless" || type == "trojan" || type == "http" || type == "anytls") { ui->security->setVisible(true); ui->security_l->setVisible(true); } else { diff --git a/src/ui/profile/edit_anytls.cpp b/src/ui/profile/edit_anytls.cpp new file mode 100644 index 0000000..4b4c97e --- /dev/null +++ b/src/ui/profile/edit_anytls.cpp @@ -0,0 +1,39 @@ +#include "include/ui/profile/edit_anytls.h" + +#include "include/configs/proxy/AnyTlsBean.hpp" + +#include +#include +#include "include/global/GuiUtils.hpp" + +EditAnyTls::EditAnyTls(QWidget *parent) : QWidget(parent), ui(new Ui::EditAnyTls) { + ui->setupUi(this); + ui->interval->setValidator(QRegExpValidator_Number); + ui->timeout->setValidator(QRegExpValidator_Number); + ui->min->setValidator(QRegExpValidator_Number); +} + +EditAnyTls::~EditAnyTls() { + delete ui; +} + +void EditAnyTls::onStart(std::shared_ptr _ent) { + this->ent = _ent; + auto bean = this->ent->AnyTlsBean(); + + ui->password->setText(bean->password); + ui->interval->setText(Int2String(bean->idle_session_check_interval)); + ui->timeout->setText(Int2String(bean->idle_session_timeout)); + ui->min->setText(Int2String(bean->min_idle_session)); +} + +bool EditAnyTls::onEnd() { + auto bean = this->ent->AnyTlsBean(); + + bean->password = ui->password->text(); + bean->idle_session_check_interval = ui->interval->text().toInt(); + bean->idle_session_timeout = ui->timeout->text().toInt(); + bean->min_idle_session = ui->min->text().toInt(); + + return true; +}