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.
This commit is contained in:
kazeyukiro committed 2026-09-30 14:50:37 +00:00
1 parent 066d93b5f5
commit 2937c4ea8d
4 files changed
+142 -37

No files matched your search

+17 -3
View File
@@ -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
}
@@ -235,25 +235,32 @@ export function configToFormValues(raw: string | undefined | null): Record<strin
values.obfs_opts_host = cfg['obfs-opts'].host;
}
// tlsmirror-config (vmess)
// tlsmirror-config (vmess) — full official inbound fields
if (cfg['tlsmirror-config'] && typeof cfg['tlsmirror-config'] === 'object') {
const m = cfg['tlsmirror-config'];
const m = cfg['tlsmirror-config'] as Record<string, any>;
values.tlsmirror_enabled = true;
values.tlsmirror_dest = m.dest;
values.tlsmirror_primary_key = m['primary-key'];
values.tlsmirror_proxy = m.proxy;
const advanced: Record<string, any> = {};
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<string, any> = {};
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<string, number> | 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<Props> = ({ protocol, autoSelectReality = f
<Input.Password />
</Form.Item>
<Form.Item name="tlsmirror_proxy" label={t('listeners.tlsMirrorProxy') || 'Proxy'} tooltip={fieldTip(t, 'listeners.tlsmirror_proxyHint')}>
<Input />
<Input placeholder="DIRECT" />
</Form.Item>
<Form.Item
name="tlsmirror_advanced_json"
label={t('listeners.tlsMirrorAdvanced') || 'TLS Mirror advanced options'}
tooltip={fieldTip(t, 'listeners.tlsMirrorAdvancedHint')}
name="tlsmirror_explicit_nonce_ciphersuites"
label={t('listeners.tlsMirrorCipherSuites') || 'Explicit nonce cipher suites'}
tooltip={fieldTip(t, 'listeners.tlsMirrorCipherSuitesHint')}
>
<Input.TextArea
rows={8}
placeholder={'{"explicit-nonce-ciphersuites":[...],"transport-layer-padding":{"enabled":true}}'}
<Select
mode="tags"
tokenSeparators={[',', ' ']}
placeholder="156, 157, 49195, …"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name="tlsmirror_defer_base_ns"
label={t('listeners.tlsMirrorDeferBase') || 'Defer write base (ns)'}
tooltip={fieldTip(t, 'listeners.tlsMirrorDeferBaseHint')}
>
<InputNumber min={0} style={{ width: '100%' }} placeholder="0" />
</Form.Item>
<Form.Item
name="tlsmirror_defer_random_ns"
label={t('listeners.tlsMirrorDeferRandom') || 'Defer write random max (ns)'}
tooltip={fieldTip(t, 'listeners.tlsMirrorDeferRandomHint')}
>
<InputNumber min={0} style={{ width: '100%' }} placeholder="0" />
</Form.Item>
<Form.Item
name="tlsmirror_padding_enabled"
label={t('listeners.tlsMirrorPadding') || 'Transport layer padding'}
valuePropName="checked"
tooltip={fieldTip(t, 'listeners.tlsMirrorPaddingHint')}
>
<Switch />
</Form.Item>
<Form.Item
name="tlsmirror_enrolment_outbound"
label={t('listeners.tlsMirrorEnrolment') || 'Enrolment primary ingress outbound'}
tooltip={fieldTip(t, 'listeners.tlsMirrorEnrolmentHint')}
>
<Input placeholder="" />
</Form.Item>
<Form.Item
name="tlsmirror_sequence_watermarking"
label={t('listeners.tlsMirrorWatermark') || 'Sequence watermarking'}
valuePropName="checked"
tooltip={fieldTip(t, 'listeners.tlsMirrorWatermarkHint')}
>
<Switch />
</Form.Item>
</EnableSection>
)}
+16 -1
View File
@@ -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.",
+16 -1
View File
@@ -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%。",