feat: add anytls support

This commit is contained in:
parhelia512 2025-08-12 09:28:39 +08:00
parent 6febb17e64
commit be51ed3e6f
16 changed files with 337 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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<V2rayStreamSettings> stream = std::make_shared<V2rayStreamSettings>();
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<JsonStore *>(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

View File

@ -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"

View File

@ -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();
};

View File

@ -0,0 +1,28 @@
#pragma once
#include <QWidget>
#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<Configs::ProxyEntity> _ent) override;
bool onEnd() override;
private:
Ui::EditAnyTls *ui;
std::shared_ptr<Configs::ProxyEntity> ent;
};

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditAnyTls</class>
<widget class="QWidget" name="EditAnyTls">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">EditAnyTls</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="MyLineEdit" name="password"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Idle Session Check Interval</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="interval"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Idle Session Timeout</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="timeout"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Min Idle Session</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="min"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MyLineEdit</class>
<extends>QLineEdit</extends>
<header>include/ui/utils/MyLineEdit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>password</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -5,6 +5,7 @@
#include <include/configs/proxy/SocksHttpBean.hpp>
#include <include/configs/proxy/TrojanVLESSBean.hpp>
#include <include/configs/proxy/VMessBean.hpp>
#include <include/configs/proxy/AnyTlsBean.hpp>
#include <include/configs/proxy/WireguardBean.h>
#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();

View File

@ -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;

View File

@ -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();

View File

@ -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") {

View File

@ -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 {

View File

@ -0,0 +1,39 @@
#include "include/ui/profile/edit_anytls.h"
#include "include/configs/proxy/AnyTlsBean.hpp"
#include <QUuid>
#include <QRegularExpressionValidator>
#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<Configs::ProxyEntity> _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;
}