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:
1 parent
066d93b5f5
commit
2937c4ea8d
4 files changed
+142
-37
No files matched your search
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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%。",
|
||||
|
||||
Reference in new issue
Block a user