feat(sub): export TLS cipher suites as the cs query param in share links (#6825)

Hosts and TLS inbounds can set cipher suites, but they reached only the JSON and
Clash subscriptions; raw share links never carried them, so clients that read cs
from links (v2rayNG forks used with fp=unsafe) had to enter them by hand. Share
links now emit cs from the host or inbound TLS settings, the panel's own link
generator and previews do the same, and both link parsers (the Go one and the
panel's outbound import) read cs back into tlsSettings.cipherSuites.
This commit is contained in:
Rouzbeh
2026-10-24 06:44:24 +02:00
committed by MHSanaei
parent 3af0f81181
commit 2b759f3916
12 changed files with 139 additions and 5 deletions
+5
View File
@@ -235,6 +235,7 @@ func parseVmess(link string) (*ParseResult, error) {
tls["echConfigList"] = getString(j, "ech", "")
tls["verifyPeerCertByName"] = getString(j, "vcn", "")
tls["pinnedPeerCertSha256"] = getString(j, "pcs", "")
tls["cipherSuites"] = getString(j, "cs", "")
}
port := num(j["port"])
@@ -687,6 +688,7 @@ func buildStream(network, security string) map[string]any {
stream["tlsSettings"] = map[string]any{
"serverName": "", "alpn": []any{}, "fingerprint": "",
"echConfigList": "", "verifyPeerCertByName": "", "pinnedPeerCertSha256": "",
"cipherSuites": "",
}
case "reality":
stream["realitySettings"] = map[string]any{
@@ -783,6 +785,9 @@ func applySecurity(stream map[string]any, p url.Values) {
tls["echConfigList"] = p.Get("ech")
tls["verifyPeerCertByName"] = p.Get("vcn")
tls["pinnedPeerCertSha256"] = p.Get("pcs")
if cs := p.Get("cs"); cs != "" {
tls["cipherSuites"] = cs
}
case "reality":
re := stream["realitySettings"].(map[string]any)
re["serverName"] = p.Get("sni")
+15 -2
View File
@@ -146,12 +146,12 @@ func TestParse_RealitySettingsAreXrayFields(t *testing.T) {
}
func TestParse_TLSSecurityMapped(t *testing.T) {
res, err := ParseLink("trojan://pw@h.com:443?type=tcp&security=tls&sni=SNI&fp=chrome&alpn=h2,http/1.1&ech=ECH&vcn=VCN&pcs=PCS")
res, err := ParseLink("trojan://pw@h.com:443?type=tcp&security=tls&sni=SNI&fp=chrome&alpn=h2,http/1.1&ech=ECH&vcn=VCN&pcs=PCS&cs=TLS_AES_128_GCM_SHA256")
if err != nil {
t.Fatalf("parse: %v", err)
}
tls := streamSub(t, res, "tlsSettings")
if tls["serverName"] != "SNI" || tls["fingerprint"] != "chrome" || tls["echConfigList"] != "ECH" || tls["verifyPeerCertByName"] != "VCN" || tls["pinnedPeerCertSha256"] != "PCS" {
if tls["serverName"] != "SNI" || tls["fingerprint"] != "chrome" || tls["echConfigList"] != "ECH" || tls["verifyPeerCertByName"] != "VCN" || tls["pinnedPeerCertSha256"] != "PCS" || tls["cipherSuites"] != "TLS_AES_128_GCM_SHA256" {
t.Errorf("tlsSettings fields = %#v", tls)
}
if alpn, _ := tls["alpn"].([]string); !reflect.DeepEqual(alpn, []string{"h2", "http/1.1"}) {
@@ -259,6 +259,19 @@ func TestParse_VmessWSPathWithoutHostKey(t *testing.T) {
}
}
func TestParse_VmessCipherSuites(t *testing.T) {
inner := `{"v":"2","add":"h","port":443,"id":"11111111-2222-4333-8444-555555555555","net":"ws","tls":"tls","cs":"TLS_AES_128_GCM_SHA256"}`
link := "vmess://" + base64.StdEncoding.EncodeToString([]byte(inner))
res, err := ParseLink(link)
if err != nil {
t.Fatalf("parse: %v", err)
}
tls := streamSub(t, res, "tlsSettings")
if tls["cipherSuites"] != "TLS_AES_128_GCM_SHA256" {
t.Errorf("tlsSettings cipherSuites = %v, want TLS_AES_128_GCM_SHA256", tls["cipherSuites"])
}
}
func TestParse_Hysteria2VerifyPeerCertByName(t *testing.T) {
res, err := ParseLink("hysteria2://auth@h.com:443?security=tls&sni=decoy.com&vcn=real-cert.com#r")
if err != nil {