From 2937c4ea8dd91fa9089f50875ced87fb2b2a7fa7 Mon Sep 17 00:00:00 2001 From: kazeyukiro Date: Wed, 30 Sep 2026 14:50:37 +0000 Subject: [PATCH] feat(ui): expand TLS Mirror into full official form fields Replace advanced JSON with dest, primary-key, proxy, cipher suites, defer write times, padding, enrolment outbound, and sequence watermark matching Meta listener tlsmirror-config. Client export omits inbound-only dest/proxy. --- backend/internal/converter/client.go | 20 ++- .../src/components/ListenerConfigFields.tsx | 125 +++++++++++++----- frontend/src/i18n/locales/en.ts | 17 ++- frontend/src/i18n/locales/zh-CN.ts | 17 ++- 4 files changed, 142 insertions(+), 37 deletions(-) diff --git a/backend/internal/converter/client.go b/backend/internal/converter/client.go index 4654f78..67809ab 100644 --- a/backend/internal/converter/client.go +++ b/backend/internal/converter/client.go @@ -1340,9 +1340,23 @@ func tlsmirrorClientOptions(src map[string]interface{}) map[string]interface{} { if !ok || len(cfg) == 0 { return nil } - result := make(map[string]interface{}, len(cfg)) - for k, v := range cfg { - result[k] = v + // Client tlsmirror-opts (proxies/tls): no dest/proxy (inbound-only). + result := map[string]interface{}{} + for _, key := range []string{ + "primary-key", + "explicit-nonce-ciphersuites", + "defer-instance-derived-write-time", + "transport-layer-padding", + "connection-enrolment", + "sequence-watermarking-enabled", + "embedded-traffic-generator", + } { + if v, ok := cfg[key]; ok && v != nil { + result[key] = v + } + } + if len(result) == 0 { + return nil } return result } diff --git a/frontend/src/components/ListenerConfigFields.tsx b/frontend/src/components/ListenerConfigFields.tsx index b3e5a61..722bd0d 100644 --- a/frontend/src/components/ListenerConfigFields.tsx +++ b/frontend/src/components/ListenerConfigFields.tsx @@ -235,25 +235,32 @@ export function configToFormValues(raw: string | undefined | null): Record; values.tlsmirror_enabled = true; values.tlsmirror_dest = m.dest; values.tlsmirror_primary_key = m['primary-key']; values.tlsmirror_proxy = m.proxy; - const advanced: Record = {}; - for (const key of [ - 'explicit-nonce-ciphersuites', - 'defer-instance-derived-write-time', - 'transport-layer-padding', - 'connection-enrolment', - 'sequence-watermarking-enabled', - 'embedded-traffic-generator', - ]) { - if (m[key] !== undefined) advanced[key] = m[key]; + if (Array.isArray(m['explicit-nonce-ciphersuites'])) { + values.tlsmirror_explicit_nonce_ciphersuites = m['explicit-nonce-ciphersuites'].map((x: any) => String(x)); + } + const defer = m['defer-instance-derived-write-time']; + if (defer && typeof defer === 'object') { + values.tlsmirror_defer_base_ns = defer['base-nanoseconds']; + values.tlsmirror_defer_random_ns = defer['uniform-random-multiplier-nanoseconds']; + } + const pad = m['transport-layer-padding']; + if (pad && typeof pad === 'object') { + values.tlsmirror_padding_enabled = !!pad.enabled; + } + const enrol = m['connection-enrolment']; + if (enrol && typeof enrol === 'object') { + values.tlsmirror_enrolment_outbound = enrol['primary-ingress-outbound']; + } + if (m['sequence-watermarking-enabled'] !== undefined) { + values.tlsmirror_sequence_watermarking = !!m['sequence-watermarking-enabled']; } - if (Object.keys(advanced).length) values.tlsmirror_advanced_json = JSON.stringify(advanced, null, 2); } // shadowquic jls-upstream @@ -372,7 +379,7 @@ const FORM_OWNED_KEYS = new Set([ 'key', 'aead-method', 'padding-min', 'padding-max', 'table-type', 'enable-pure-downlink', 'custom-table', 'custom-tables', 'fallback', 'httpmask', 'certificate', 'private-key', 'client-auth-type', 'client-auth-cert', 'ech-key', 'allow-insecure', - 'reality-config', 'users', 'tlsmirror_advanced_json', 'simple-obfs', 'shadow-tls', 'res-tls', 'jls-config', 'tlsmirror-config', 'mux-option', + 'reality-config', 'users', 'simple-obfs', 'shadow-tls', 'res-tls', 'jls-config', 'tlsmirror-config', 'mux-option', 'kcp-tun', 'xhttp-config', 'mkcp-config', 'mekya-config', 'obfs-opts', 'jls-upstream', 'realm-opts', 'network', 'bbr-profile', 'quic-versions', 'cwnd', 'max-datagram-frame-size', 'recv-window-conn', 'recv-window', 'disable-mtu-discovery', 'traffic-pattern', 'user-hint-is-mandatory', ]); @@ -751,25 +758,40 @@ export function formValuesToConfig( } } - // tlsmirror-config (vmess) — require dest + primary-key + // tlsmirror-config (vmess) — official inbound shape only (wiki listeners/vmess) if (TLSMIRROR_PROTOCOLS.has(protocol) && values.tlsmirror_enabled) { const dest = typeof values.tlsmirror_dest === 'string' ? values.tlsmirror_dest.trim() : ''; const primaryKey = typeof values.tlsmirror_primary_key === 'string' ? values.tlsmirror_primary_key.trim() : ''; if (dest && primaryKey) { - let advanced: Record = {}; - if (typeof values.tlsmirror_advanced_json === 'string' && values.tlsmirror_advanced_json.trim()) { - try { - const parsed = JSON.parse(values.tlsmirror_advanced_json); - if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) advanced = parsed; - } catch { - throw new Error('TLS Mirror advanced settings must be valid JSON'); - } + const suitesRaw = values.tlsmirror_explicit_nonce_ciphersuites; + let suites: number[] | undefined; + if (Array.isArray(suitesRaw) && suitesRaw.length) { + suites = suitesRaw + .map((x) => (typeof x === 'number' ? x : parseInt(String(x).trim(), 10))) + .filter((n) => Number.isFinite(n)); + if (!suites.length) suites = undefined; } + const deferBase = values.tlsmirror_defer_base_ns; + const deferRand = values.tlsmirror_defer_random_ns; + let defer: Record | undefined; + if (deferBase != null && deferBase !== '' || deferRand != null && deferRand !== '') { + defer = {}; + if (deferBase != null && deferBase !== '') defer['base-nanoseconds'] = Number(deferBase); + if (deferRand != null && deferRand !== '') defer['uniform-random-multiplier-nanoseconds'] = Number(deferRand); + } + const enrolOut = + typeof values.tlsmirror_enrolment_outbound === 'string' + ? values.tlsmirror_enrolment_outbound.trim() + : ''; cfg['tlsmirror-config'] = cleanObj({ - ...advanced, dest, 'primary-key': primaryKey, - proxy: values.tlsmirror_proxy, + proxy: typeof values.tlsmirror_proxy === 'string' ? values.tlsmirror_proxy.trim() || undefined : values.tlsmirror_proxy, + 'explicit-nonce-ciphersuites': suites, + 'defer-instance-derived-write-time': defer && Object.keys(defer).length ? defer : undefined, + 'transport-layer-padding': values.tlsmirror_padding_enabled ? { enabled: true } : undefined, + 'connection-enrolment': enrolOut ? { 'primary-ingress-outbound': enrolOut } : undefined, + 'sequence-watermarking-enabled': values.tlsmirror_sequence_watermarking === true ? true : undefined, }); } } @@ -1456,18 +1478,57 @@ const ListenerConfigFields: React.FC = ({ protocol, autoSelectReality = f - + - + + + + + + + + + + + + + + + )} diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index ba1532b..fb6e612 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -499,7 +499,22 @@ export default { "realm_private_keyHint": "Private key PEM matching the realm client certificate.", "realmAlpn": "Realm ALPN", "realm_alpnHint": "Optional ALPN list for the realm TLS handshake.", - "tlsMirrorHint": "Advanced. Requires dest and primary-key; mutually exclusive with certificate TLS.", + "tlsMirrorCipherSuites": "Explicit nonce cipher suites", + "tlsMirrorCipherSuitesHint": "TLS 1.2 cipher suite IDs that use explicit nonces (comma or tags). Leave empty for kernel defaults.", + "tlsMirrorDeferBase": "Defer write base (ns)", + "tlsMirrorDeferBaseHint": "Fixed delay before the first derived write, in nanoseconds.", + "tlsMirrorDeferRandom": "Defer write random max (ns)", + "tlsMirrorDeferRandomHint": "Upper bound for extra random delay before the first write, in nanoseconds.", + "tlsMirrorPadding": "Transport layer padding", + "tlsMirrorPaddingHint": "Enable transport-layer padding (transport-layer-padding.enabled).", + "tlsMirrorEnrolment": "Enrolment primary ingress outbound", + "tlsMirrorEnrolmentHint": "v2ray-compatible connection-enrolment.primary-ingress-outbound tag.", + "tlsMirrorWatermark": "Sequence watermarking", + "tlsMirrorWatermarkHint": "Enable sequence-watermarking-enabled.", + "tlsmirror_destHint": "Mirror target host:port, e.g. www.example.com:443.", + "tlsmirror_primary_keyHint": "Required. Base64 of a 32-byte primary key.", + "tlsmirror_proxyHint": "Optional outbound name used when dialing dest (e.g. DIRECT).", + "tlsMirrorHint": "Mutually exclusive with certificate/private-key. Requires dest and 32-byte primary-key (base64).", "tlsPairAutoHint": "Certificate + private-key required. Leave both empty → panel self-signed on save.", "tlsPairHint": "Certificate and private-key for this listener; leave both empty for no TLS.", "trafficMultiplierHint": "Charged traffic is the real traffic multiplied by this factor. Default 1; 1.5 counts 50% extra.", diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 34d6db6..8613f70 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -499,7 +499,22 @@ export default { "realm_private_keyHint": "与 Realm 客户端证书匹配的私钥 PEM。", "realmAlpn": "Realm ALPN", "realm_alpnHint": "Realm TLS 握手可选 ALPN 列表。", - "tlsMirrorHint": "高级选项。需要 dest 与 primary-key,与证书型 TLS 互斥。", + "tlsMirrorCipherSuites": "显式 nonce 加密套件", + "tlsMirrorCipherSuitesHint": "TLS 1.2 使用显式 nonce 的 cipher suite ID(逗号或标签)。留空则用内核默认。", + "tlsMirrorDeferBase": "首次写入延迟基数 (ns)", + "tlsMirrorDeferBaseHint": "首次派生写入前的固定延迟,单位纳秒。", + "tlsMirrorDeferRandom": "首次写入随机延迟上限 (ns)", + "tlsMirrorDeferRandomHint": "首次写入额外随机延迟上限,单位纳秒。", + "tlsMirrorPadding": "传输层填充", + "tlsMirrorPaddingHint": "启用 transport-layer-padding.enabled。", + "tlsMirrorEnrolment": "连接登记出站", + "tlsMirrorEnrolmentHint": "v2ray 兼容 connection-enrolment.primary-ingress-outbound。", + "tlsMirrorWatermark": "序列水印", + "tlsMirrorWatermarkHint": "启用 sequence-watermarking-enabled。", + "tlsmirror_destHint": "镜像目标 host:port,例如 www.example.com:443。", + "tlsmirror_primary_keyHint": "必填。32 字节主密钥的 base64。", + "tlsmirror_proxyHint": "拨号 dest 时使用的出站名(如 DIRECT)。", + "tlsMirrorHint": "与 certificate/private-key 互斥。需要 dest 与 32 字节主密钥(base64)。", "tlsPairAutoHint": "证书与私钥为必填。两者都留空时保存会生成面板自签证书。", "tlsPairHint": "该监听器的证书与私钥对;两者都留空表示不使用 TLS。", "trafficMultiplierHint": "实际流量 × 倍率计入用户配额。默认 1,例如 1.5 表示多计 50%。",