From 2b759f3916f9be28d5b3f687b701e6536d40da6b Mon Sep 17 00:00:00 2001 From: Rouzbeh <78313022+rqzbeh@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:44:24 +0200 Subject: [PATCH] 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. --- frontend/src/lib/hosts/host-link.ts | 2 ++ frontend/src/lib/xray/inbound-link.ts | 13 +++++++++ frontend/src/lib/xray/outbound-link-parser.ts | 3 ++ frontend/src/pages/hosts/HostFormModal.tsx | 7 ++++- .../protocols/stream/external-proxy.ts | 1 + frontend/src/test/host-link.test.ts | 2 ++ frontend/src/test/inbound-link.test.ts | 27 +++++++++++++++++ .../src/test/outbound-link-parser.test.ts | 11 +++++++ internal/sub/service.go | 27 +++++++++++++++-- internal/sub/service_test.go | 29 +++++++++++++++++++ internal/util/link/outbound.go | 5 ++++ internal/util/link/outbound_helpers_test.go | 17 +++++++++-- 12 files changed, 139 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/hosts/host-link.ts b/frontend/src/lib/hosts/host-link.ts index 7dc75d202..5c44c76e5 100644 --- a/frontend/src/lib/hosts/host-link.ts +++ b/frontend/src/lib/hosts/host-link.ts @@ -18,6 +18,7 @@ export type HostLinkInput = Pick< | 'pinnedPeerCertSha256' | 'verifyPeerCertByName' | 'echConfigList' + | 'cipherSuites' | 'overrideSniFromAddress' | 'keepSniBlank' | 'vlessRoute' @@ -55,6 +56,7 @@ export function hostToExternalProxyEntry(host: HostLinkInput): ExternalProxyEntr : undefined, verifyPeerCertByName: host.verifyPeerCertByName || undefined, echConfigList: host.echConfigList || undefined, + cipherSuites: host.cipherSuites || undefined, vlessRoute: host.vlessRoute || undefined, allowInsecure: host.allowInsecure || undefined, }; diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 58215b1f2..0ed703cd9 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -235,6 +235,8 @@ function applyExternalProxyTLSObj( } if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) obj.ech = externalProxy.echConfigList; + if (externalProxy.cipherSuites && externalProxy.cipherSuites.length > 0) + obj.cs = externalProxy.cipherSuites; } export interface GenVmessLinkInput { @@ -337,6 +339,9 @@ export function genVmessLink(input: GenVmessLinkInput): string { if (tlsSettings.settings.pinnedPeerCertSha256.length > 0) { obj.pcs = tlsSettings.settings.pinnedPeerCertSha256.join(','); } + if (tlsSettings.cipherSuites && tlsSettings.cipherSuites.length > 0) { + obj.cs = tlsSettings.cipherSuites; + } } applyExternalProxyTLSObj(externalProxy, obj, tls); @@ -394,6 +399,8 @@ function applyExternalProxyTLSParams( } if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) params.set('ech', externalProxy.echConfigList); + if (externalProxy.cipherSuites && externalProxy.cipherSuites.length > 0) + params.set('cs', externalProxy.cipherSuites); } export interface GenVlessLinkInput { @@ -497,6 +504,9 @@ export function genVlessLink(input: GenVlessLinkInput): string { if (tls.settings.pinnedPeerCertSha256.length > 0) { params.set('pcs', tls.settings.pinnedPeerCertSha256.join(',')); } + if (tls.cipherSuites && tls.cipherSuites.length > 0) { + params.set('cs', tls.cipherSuites); + } } applyExternalProxyTLSParams(externalProxy, params, security); } else if (security === 'reality') { @@ -602,6 +612,9 @@ function writeTlsParams( if (tls.settings.pinnedPeerCertSha256.length > 0) { params.set('pcs', tls.settings.pinnedPeerCertSha256.join(',')); } + if (tls.cipherSuites && tls.cipherSuites.length > 0) { + params.set('cs', tls.cipherSuites); + } } // Reality query-string writer shared by VLESS and Trojan. Preserves the diff --git a/frontend/src/lib/xray/outbound-link-parser.ts b/frontend/src/lib/xray/outbound-link-parser.ts index 30659462e..6e83d988f 100644 --- a/frontend/src/lib/xray/outbound-link-parser.ts +++ b/frontend/src/lib/xray/outbound-link-parser.ts @@ -489,6 +489,8 @@ function applySecurityParams(stream: Raw, params: URLSearchParams): void { tls.echConfigList = params.get('ech') ?? ''; tls.verifyPeerCertByName = params.get('vcn') ?? ''; tls.pinnedPeerCertSha256 = params.get('pcs') ?? ''; + const cs = params.get('cs'); + if (cs) tls.cipherSuites = cs; } else if (stream.security === 'reality') { const reality = stream.realitySettings as Raw; reality.serverName = params.get('sni') ?? ''; @@ -567,6 +569,7 @@ export function parseVmessLink(link: string): Raw | null { if (typeof json.ech === 'string') tls.echConfigList = json.ech; if (typeof json.vcn === 'string') tls.verifyPeerCertByName = json.vcn; if (typeof json.pcs === 'string') tls.pinnedPeerCertSha256 = json.pcs; + if (typeof json.cs === 'string') tls.cipherSuites = json.cs; } const port = Number(json.port) || 443; diff --git a/frontend/src/pages/hosts/HostFormModal.tsx b/frontend/src/pages/hosts/HostFormModal.tsx index 2cae68ed1..b126cff65 100644 --- a/frontend/src/pages/hosts/HostFormModal.tsx +++ b/frontend/src/pages/hosts/HostFormModal.tsx @@ -106,10 +106,12 @@ export default function HostFormModal({ const [loading, setLoading] = useState(false); const security = (useWatch({ control: methods.control, name: 'security' }) ?? 'same') as string; + const fingerprint = (useWatch({ control: methods.control, name: 'fingerprint' }) ?? '') as string; const hostEchConfig = useWatch({ control: methods.control, name: 'echConfigList' }) ?? ''; const hostSni = useWatch({ control: methods.control, name: 'sni' }) ?? ''; const showTls = security === 'tls' || security === 'reality' || security === 'same'; const showTlsExtras = security === 'tls' || security === 'same'; + const isCipherSuitesEnabled = showTlsExtras && (!fingerprint || fingerprint === 'unsafe'); const watchedHosts = useWatch({ control: methods.control, name: 'hosts' }); const hasCidr = Boolean(watchedHosts?.some((h) => typeof h === 'string' && h.includes('/'))); @@ -364,7 +366,10 @@ export default function HostFormModal({ name="cipherSuites" label={t('pages.inbounds.form.cipherSuites')} > - +