From ce221c33d038a7c6c0d06c5ba013c7694d7a10a3 Mon Sep 17 00:00:00 2001 From: libmur-dev Date: Fri, 2 Oct 2026 16:44:41 +0300 Subject: [PATCH] fix(sub): carry REALITY ML-KEM hint in VLESS links (#6712) * fix(sub): carry REALITY ML-KEM hint in VLESS links Keep raw share links in parity with Clash subscriptions for Xray 26.9.8+. Preserve the URI hint through Go and frontend imports, expose it in the outbound editor, and update the documentation tooling. * fix(link): accept REALITY ML-KEM boolean aliases * test(frontend): isolate Happ preset notifications * fix(link): keep the ML-KEM hint out of Xray REALITY settings support-x25519mlkem768 is a Mihomo reality-opts option; xray-core's REALITYConfig (infra/conf/transport_security.go) has no such field and its JSON loader drops unknown keys silently. The PR also stored it as realitySettings.supportX25519Mlkem768 in Xray outbounds (form switch, Go and TS link import, docs outbound builders) and as an inbound settings default that is stripped before Xray and read by no link generator. The outbound switch therefore did nothing, and imported links carried a dead key into the JSON subscription. The share-link hint itself stays: Go, frontend and docs still emit support-x25519mlkem768=true on VLESS REALITY links and drop it on a TLS host override. --------- Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com> Co-authored-by: MHSanaei --- .../tools/reality-config-generator.tsx | 1 + docs/content/docs/en/config/reality.mdx | 9 +++---- docs/content/docs/fa/config/reality.mdx | 4 +++- docs/content/docs/ru/config/reality.mdx | 11 +++++---- docs/content/docs/zh/config/reality.mdx | 2 +- docs/lib/xray/reality.test.ts | 2 ++ docs/lib/xray/reality.ts | 2 ++ docs/lib/xray/subscription.test.ts | 9 +++++++ docs/lib/xray/subscription.ts | 4 ++++ frontend/src/lib/xray/inbound-link.ts | 1 + .../__snapshots__/inbound-link.test.ts.snap | 4 ++-- .../src/test/happ-settings-presets.test.tsx | 3 +++ frontend/src/test/inbound-link.test.ts | 16 +++++++++++++ .../src/test/outbound-link-parser.test.ts | 10 ++++++++ internal/sub/endpoint.go | 10 +++++++- internal/sub/host_sub_test.go | 8 ++++++- internal/sub/service.go | 1 + internal/sub/service_sharelink_test.go | 1 + internal/util/link/outbound_helpers_test.go | 24 ++++++++++++++++++- 19 files changed, 106 insertions(+), 16 deletions(-) diff --git a/docs/components/tools/reality-config-generator.tsx b/docs/components/tools/reality-config-generator.tsx index d7b722317..7bbd2994f 100644 --- a/docs/components/tools/reality-config-generator.tsx +++ b/docs/components/tools/reality-config-generator.tsx @@ -66,6 +66,7 @@ export function RealityConfigGenerator() { fingerprint, spiderX: '/', flow: 'xtls-rprx-vision', + supportX25519Mlkem768: true, } : null; diff --git a/docs/content/docs/en/config/reality.mdx b/docs/content/docs/en/config/reality.mdx index d6874b5f5..4fa83c6ae 100644 --- a/docs/content/docs/en/config/reality.mdx +++ b/docs/content/docs/en/config/reality.mdx @@ -129,10 +129,11 @@ vless://@:443?security=reality&pbk=&sid=&sni nodes, including external links, and uses `chrome` when no fingerprint was set. Explicit fingerprints are preserved: choose one that offers ML-KEM (`chrome` with Mihomo's uTLS v1.8.7); enabling the flag cannot upgrade an old fingerprint. - Raw `vless://` links do not carry this Mihomo option, so clients importing them - directly still need a persistent override. Very old REALITY servers that reject - ML-KEM require a per-node client override setting this option to `false`, or a - server upgrade. Clearing the version limit alone does not fix the handshake. + Raw `vless://` links carry the equivalent `support-x25519mlkem768=true` hint; + clients that support this URI extension, including current Mihomo builds, apply + it on import. Very old REALITY servers that reject ML-KEM require a per-node + client override setting this option to `false`, or a server upgrade. Clearing + the version limit alone does not fix the handshake. diff --git a/docs/content/docs/fa/config/reality.mdx b/docs/content/docs/fa/config/reality.mdx index 399cc5861..5f2f44369 100644 --- a/docs/content/docs/fa/config/reality.mdx +++ b/docs/content/docs/fa/config/reality.mdx @@ -137,7 +137,9 @@ vless://@:443?security=reality&pbk=&sid=&sni در Mihomo از Chrome استفاده کنید. فعال کردن گزینه، اثر انگشت قدیمی را ارتقا نمی‌دهد. لینک خام `vless://` - این گزینه را منتقل نمی‌کند و هنگام ورود مستقیم، بازنویسی پایدار در کلاینت لازم است. + راهنمای معادل + `support-x25519mlkem768=true` + را منتقل می‌کند؛ کلاینت‌هایی که این افزونهٔ URI را پشتیبانی می‌کنند، از جمله نسخه‌های فعلی Mihomo، آن را هنگام ورود اعمال می‌کنند. برای سرورهای بسیار قدیمی که ML-KEM را رد می‌کنند، گزینه را برای همان گره در کلاینت روی `false` بگذارید یا سرور را ارتقا دهید. حذف محدودیت نسخه به‌تنهایی دست‌دهی را اصلاح نمی‌کند. diff --git a/docs/content/docs/ru/config/reality.mdx b/docs/content/docs/ru/config/reality.mdx index 74eb15fdb..b4cd5b632 100644 --- a/docs/content/docs/ru/config/reality.mdx +++ b/docs/content/docs/ru/config/reality.mdx @@ -133,11 +133,12 @@ vless://@:443?security=reality&pbk=&sid=&sni включает `reality-opts.support-x25519mlkem768` для REALITY, в том числе внешних ссылок, и выбирает `chrome`, если отпечаток не задан. Явный выбор сохраняется: нужен отпечаток с ML-KEM (`chrome` при uTLS v1.8.7 в Mihomo). Сам флаг не - обновляет старые отпечатки. Исходные ссылки `vless://` не передают эту настройку - Mihomo; при прямом импорте нужно постоянное переопределение в клиенте. Для очень - старых серверов REALITY, отвергающих ML-KEM, задайте `false` для соответствующего - узла в клиенте или обновите сервер. Снятие ограничения версии не исправляет - это рукопожатие. + обновляет старые отпечатки. Исходные ссылки `vless://` передают эквивалентную + подсказку `support-x25519mlkem768=true`; клиенты, поддерживающие это расширение + URI, включая актуальные сборки Mihomo, применяют её при импорте. Для очень старых + серверов REALITY, отвергающих ML-KEM, задайте `false` для соответствующего узла + в клиенте или обновите сервер. Снятие ограничения версии не исправляет это + рукопожатие. diff --git a/docs/content/docs/zh/config/reality.mdx b/docs/content/docs/zh/config/reality.mdx index 3659862d5..1eaeb2854 100644 --- a/docs/content/docs/zh/config/reality.mdx +++ b/docs/content/docs/zh/config/reality.mdx @@ -106,7 +106,7 @@ vless://@:443?security=reality&pbk=&sid=&sni - **私钥泄露。** 永远只把**公钥**分发给客户端。 - **流控设置错误。** REALITY + XTLS-Vision 要求在入站的客户端条目和分享链接上都设置 `flow = xtls-rprx-vision`。 - **客户端版本限制。** Xray-core v26.9.8+ 在**最小客户端版本**留空时不再设置默认下限,但已明确保存的限制仍生效。较早的内核可能使用内置下限(如 `26.3.27`),导致第三方客户端即使密钥正确也被拒绝。修改前先核对运行中的内核版本;降低限制也会放行较旧的指纹。 -- **Mihomo 与 ML-KEM。** Xray-core v26.9.8+ 还独立要求 `X25519MLKEM768` key share 位于可选的 `X25519` 之前。Clash/Mihomo YAML 订阅会为 REALITY 节点(含外部链接)启用 `reality-opts.support-x25519mlkem768`,未设置指纹时使用 `chrome`。明确选择的指纹会保留,必须选择支持 ML-KEM 的指纹(Mihomo 使用 uTLS v1.8.7 时可选 `chrome`);开关无法让旧指纹获得新能力。原始 `vless://` 链接不携带这个 Mihomo 配置项,直接导入时仍需持久覆写。对拒绝 ML-KEM 的很旧的 REALITY 服务端,需在客户端按节点将此项覆写为 `false`,或升级服务端。仅清空版本限制无法解决握手问题。 +- **Mihomo 与 ML-KEM。** Xray-core v26.9.8+ 还独立要求 `X25519MLKEM768` key share 位于可选的 `X25519` 之前。Clash/Mihomo YAML 订阅会为 REALITY 节点(含外部链接)启用 `reality-opts.support-x25519mlkem768`,未设置指纹时使用 `chrome`。明确选择的指纹会保留,必须选择支持 ML-KEM 的指纹(Mihomo 使用 uTLS v1.8.7 时可选 `chrome`);开关无法让旧指纹获得新能力。原始 `vless://` 链接会携带等效的 `support-x25519mlkem768=true` 提示;支持此 URI 扩展的客户端(包括当前 Mihomo 版本)会在导入时应用它。对拒绝 ML-KEM 的很旧的 REALITY 服务端,需在客户端按节点将此项覆写为 `false`,或升级服务端。仅清空版本限制无法解决握手问题。 diff --git a/docs/lib/xray/reality.test.ts b/docs/lib/xray/reality.test.ts index 8838ee73a..0a3fa9596 100644 --- a/docs/lib/xray/reality.test.ts +++ b/docs/lib/xray/reality.test.ts @@ -50,6 +50,7 @@ const CONFIG: RealityConfig = { fingerprint: 'chrome', spiderX: '/', flow: 'xtls-rprx-vision', + supportX25519Mlkem768: true, }; describe('realityClientLink', () => { @@ -61,6 +62,7 @@ describe('realityClientLink', () => { expect(parsed.port).toBe(443); expect(parsed.params.security).toBe('reality'); expect(parsed.params.pbk).toBe('PUB'); + expect(parsed.params['support-x25519mlkem768']).toBe('true'); expect(parsed.params.sid).toBe('ab12'); expect(parsed.params.sni).toBe('www.microsoft.com'); expect(parsed.params.flow).toBe('xtls-rprx-vision'); diff --git a/docs/lib/xray/reality.ts b/docs/lib/xray/reality.ts index c459bc0d0..c6c98bbd1 100644 --- a/docs/lib/xray/reality.ts +++ b/docs/lib/xray/reality.ts @@ -64,6 +64,7 @@ export interface RealityConfig { fingerprint: string; spiderX: string; flow: string; + supportX25519Mlkem768: boolean; } /** Server-side VLESS + REALITY inbound (Xray config shape). */ @@ -108,6 +109,7 @@ export function realityClientLink(c: RealityConfig): string { sid: c.shortIds[0] ?? '', spx: c.spiderX, flow: c.flow, + ...(c.supportX25519Mlkem768 ? { 'support-x25519mlkem768': 'true' } : {}), }, name: `${c.address}-reality`, }); diff --git a/docs/lib/xray/subscription.test.ts b/docs/lib/xray/subscription.test.ts index a79f78705..d28a19cd6 100644 --- a/docs/lib/xray/subscription.test.ts +++ b/docs/lib/xray/subscription.test.ts @@ -74,6 +74,7 @@ describe('buildShareLinks', () => { expect(parsed.port).toBe(443); expect(parsed.credential).toBe('11111111-2222-3333-4444-555555555555'); expect(parsed.params.security).toBe('reality'); + expect(parsed.params['support-x25519mlkem768']).toBe('true'); expect(parsed.name).toBe('HK-01'); }); }); @@ -120,6 +121,14 @@ describe('buildJsonSubscription', () => { expect(cfg.remarks).toBe('HK-01'); }); + it('keeps the Mihomo-only ML-KEM hint out of the Xray realitySettings', () => { + const cfg = JSON.parse(buildJsonSubscription([vlessClient])); + expect(cfg.outbounds[0].streamSettings.realitySettings.publicKey).toBe(vlessClient.publicKey); + expect(cfg.outbounds[0].streamSettings.realitySettings).not.toHaveProperty( + 'supportX25519Mlkem768', + ); + }); + it('uses the iOS-compatible SOCKS inbound while preserving the mixed tag and HTTP inbound', () => { const cfg = JSON.parse(buildJsonSubscription([vlessClient])); const socks = cfg.inbounds.find((inbound: { port: number }) => inbound.port === 10808); diff --git a/docs/lib/xray/subscription.ts b/docs/lib/xray/subscription.ts index c06337a78..b6d09931a 100644 --- a/docs/lib/xray/subscription.ts +++ b/docs/lib/xray/subscription.ts @@ -47,6 +47,7 @@ export interface SubClient { serviceName?: string; publicKey?: string; // reality shortId?: string; // reality + supportX25519Mlkem768?: boolean; // reality client compatibility } function normPath(p: string): string { @@ -77,6 +78,9 @@ function streamParams(c: SubClient): Record { if (c.serviceName) p.serviceName = c.serviceName; if (c.publicKey) p.pbk = c.publicKey; if (c.shortId) p.sid = c.shortId; + if (c.security === 'reality' && c.publicKey && c.supportX25519Mlkem768 !== false) { + p['support-x25519mlkem768'] = 'true'; + } return p; } diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index bf5a62ed3..701c89dc0 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -453,6 +453,7 @@ export function genVlessLink(input: GenVlessLinkInput): string { applyExternalProxyTLSParams(externalProxy, params, security); } else if (security === 'reality') { params.set('security', 'reality'); + params.set('support-x25519mlkem768', 'true'); if (stream.security === 'reality') { const reality = stream.realitySettings; params.set('pbk', reality.settings.publicKey); diff --git a/frontend/src/test/__snapshots__/inbound-link.test.ts.snap b/frontend/src/test/__snapshots__/inbound-link.test.ts.snap index e41bf18c4..b844d9f4c 100644 --- a/frontend/src/test/__snapshots__/inbound-link.test.ts.snap +++ b/frontend/src/test/__snapshots__/inbound-link.test.ts.snap @@ -8,7 +8,7 @@ exports[`genInboundLinks orchestrator > shadowsocks-tcp-2022: byte-stable 1`] = exports[`genInboundLinks orchestrator > trojan-ws-tls: byte-stable 1`] = `"trojan://trojan-test-pw-XYZ@override.test:443?type=ws&path=%2Ftrojan&host=trojan.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=trojan.example.test#parity-test"`; -exports[`genInboundLinks orchestrator > vless-tcp-reality: byte-stable 1`] = `"vless://22222222-3333-4444-9555-666666666666@override.test:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sni=yahoo.com&sid=a3f1&spx=%2Fdafd018f50a389b&flow=xtls-rprx-vision#parity-test"`; +exports[`genInboundLinks orchestrator > vless-tcp-reality: byte-stable 1`] = `"vless://22222222-3333-4444-9555-666666666666@override.test:443?type=tcp&encryption=none&security=reality&support-x25519mlkem768=true&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sni=yahoo.com&sid=a3f1&spx=%2Fdafd018f50a389b&flow=xtls-rprx-vision#parity-test"`; exports[`genInboundLinks orchestrator > vless-ws-tls: byte-stable 1`] = `"vless://8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02@override.test:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`; @@ -36,7 +36,7 @@ exports[`genShadowsocksLink > shadowsocks-tcp-2022: byte-stable 1`] = `"ss://202 exports[`genTrojanLink > trojan-ws-tls: byte-stable 1`] = `"trojan://trojan-test-pw-XYZ@example.test:443?type=ws&path=%2Ftrojan&host=trojan.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=trojan.example.test#parity-test"`; -exports[`genVlessLink > vless-tcp-reality: byte-stable 1`] = `"vless://22222222-3333-4444-9555-666666666666@example.test:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sni=yahoo.com&sid=a3f1&spx=%2Fd08ed99bd9afc60&flow=xtls-rprx-vision#parity-test"`; +exports[`genVlessLink > vless-tcp-reality: byte-stable 1`] = `"vless://22222222-3333-4444-9555-666666666666@example.test:443?type=tcp&encryption=none&security=reality&support-x25519mlkem768=true&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sni=yahoo.com&sid=a3f1&spx=%2Fd08ed99bd9afc60&flow=xtls-rprx-vision#parity-test"`; exports[`genVlessLink > vless-ws-tls: byte-stable 1`] = `"vless://8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02@example.test:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`; diff --git a/frontend/src/test/happ-settings-presets.test.tsx b/frontend/src/test/happ-settings-presets.test.tsx index 845776f54..3b7c34266 100644 --- a/frontend/src/test/happ-settings-presets.test.tsx +++ b/frontend/src/test/happ-settings-presets.test.tsx @@ -1,12 +1,15 @@ import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { fireEvent, screen } from '@testing-library/react'; +import { message } from 'antd'; import { AllSetting } from '@/models/setting'; import HappSettingsContent from '@/pages/settings/HappSettingsContent'; import { renderWithProviders } from './test-utils'; +vi.spyOn(message, 'success').mockImplementation(() => undefined as never); + const chinaProfile = { Name: 'Bypass-CN', GlobalProxy: 'true', diff --git a/frontend/src/test/inbound-link.test.ts b/frontend/src/test/inbound-link.test.ts index abf3d1d02..abda78ae8 100644 --- a/frontend/src/test/inbound-link.test.ts +++ b/frontend/src/test/inbound-link.test.ts @@ -86,6 +86,22 @@ describe('genVlessLink', () => { const fixtures = fixturesForProtocol('vless'); expect(fixtures.length, 'need at least one vless full-inbound fixture').toBeGreaterThan(0); + it('enables X25519MLKEM768 in REALITY share links', () => { + const entry = fixtures.find(([name]) => name === 'vless-tcp-reality'); + expect(entry, 'need a VLESS REALITY fixture').toBeDefined(); + const [, raw] = entry!; + const typed = InboundSchema.parse(raw); + const client = (raw as { settings: { clients: Array<{ id: string }> } }).settings.clients[0]; + + const link = genVlessLink({ + inbound: typed, + address: 'example.test', + clientId: client.id, + }); + + expect(new URL(link).searchParams.get('support-x25519mlkem768')).toBe('true'); + }); + for (const [name, raw] of fixtures) { it(`${name}: byte-stable`, () => { const typed = InboundSchema.parse(raw); diff --git a/frontend/src/test/outbound-link-parser.test.ts b/frontend/src/test/outbound-link-parser.test.ts index 8b2a437cf..6a69a6dc7 100644 --- a/frontend/src/test/outbound-link-parser.test.ts +++ b/frontend/src/test/outbound-link-parser.test.ts @@ -9,6 +9,7 @@ import { parseHysteria2Link, parseWireguardLink, } from '@/lib/xray/outbound-link-parser'; +import { formValuesToWirePayload, rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter'; import { Base64 } from '@/utils'; // Focused acceptance tests for the share-link parsers — one happy-path @@ -248,6 +249,7 @@ describe('parseVlessLink', () => { const link = 'vless://11111111-2222-4333-8444-555555555555@srv.example:443' + '?type=tcp&security=reality&pbk=pubkey&sid=abcd&fp=chrome&sni=cloudflare.com&flow=xtls-rprx-vision' + + '&support-x25519mlkem768=true' + '#imported-vless'; const out = parseVlessLink(link); expect(out?.protocol).toBe('vless'); @@ -263,6 +265,14 @@ describe('parseVlessLink', () => { expect(reality.publicKey).toBe('pubkey'); expect(reality.shortId).toBe('abcd'); expect(reality.serverName).toBe('cloudflare.com'); + // The hint is for Mihomo; xray-core's REALITYConfig has no such field. + expect(reality).not.toHaveProperty('supportX25519Mlkem768'); + + const form = rawOutboundToFormValues(out!); + const saved = formValuesToWirePayload(form); + const savedReality = (saved.streamSettings as Record) + .realitySettings as Record; + expect(savedReality).not.toHaveProperty('supportX25519Mlkem768'); }); it('parses encryption + pqv (post-quantum) into settings and mldsa65Verify', () => { diff --git a/internal/sub/endpoint.go b/internal/sub/endpoint.go index d00640ad5..5d8c9d34d 100644 --- a/internal/sub/endpoint.go +++ b/internal/sub/endpoint.go @@ -98,7 +98,15 @@ func dropBaseRealityParams(params map[string]string, baseSecurity, securityToApp } // sni and fp name the master's reality dest, not this endpoint's own // certificate; the host's values are re-applied right after this. - for _, k := range []string{"pbk", "sid", "spx", "pqv", "sni", "fp"} { + for _, k := range []string{ + "pbk", + "sid", + "spx", + "pqv", + "support-x25519mlkem768", + "sni", + "fp", + } { delete(params, k) } } diff --git a/internal/sub/host_sub_test.go b/internal/sub/host_sub_test.go index c36fc1d65..5ae3ff9a2 100644 --- a/internal/sub/host_sub_test.go +++ b/internal/sub/host_sub_test.go @@ -434,7 +434,13 @@ func TestSub_HostTlsOverRealityDropsRealityParams(t *testing.T) { if !strings.Contains(joined, "security=tls") { t.Fatalf("host forces tls, link must say so: %s", joined) } - for _, leaked := range []string{"pbk=", "sid=", "spx=", "sni=master-dest.example.com"} { + for _, leaked := range []string{ + "pbk=", + "sid=", + "spx=", + "support-x25519mlkem768=", + "sni=master-dest.example.com", + } { if strings.Contains(joined, leaked) { t.Fatalf("reality parameter %q survived a tls host override: %s", leaked, joined) } diff --git a/internal/sub/service.go b/internal/sub/service.go index 07043eee9..d777f6986 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -1240,6 +1240,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { applyShareTLSParams(stream, params) case "reality": applyShareRealityParams(stream, params, subKey(client)) + params["support-x25519mlkem768"] = "true" default: params["security"] = "none" } diff --git a/internal/sub/service_sharelink_test.go b/internal/sub/service_sharelink_test.go index 88cc80a69..7da4367fb 100644 --- a/internal/sub/service_sharelink_test.go +++ b/internal/sub/service_sharelink_test.go @@ -70,6 +70,7 @@ func TestGenVlessLink_RealityParamsMapped(t *testing.T) { wants := []string{ "security=reality", + "support-x25519mlkem768=true", "sni=reality.example.com", "pbk=PBKvalue", "sid=ab12cd", diff --git a/internal/util/link/outbound_helpers_test.go b/internal/util/link/outbound_helpers_test.go index 787f81c0c..a9d7f7f64 100644 --- a/internal/util/link/outbound_helpers_test.go +++ b/internal/util/link/outbound_helpers_test.go @@ -1,11 +1,15 @@ package link import ( + "bytes" "encoding/base64" + "encoding/json" "net/url" "reflect" "slices" "testing" + + "github.com/xtls/xray-core/infra/conf" ) func TestDefaultPort(t *testing.T) { @@ -111,7 +115,7 @@ func streamSub(t *testing.T, res *ParseResult, key string) map[string]any { } func TestParse_RealitySecurityMapped(t *testing.T) { - res, err := ParseLink("vless://uuid@h.com:443?type=tcp&security=reality&pbk=PBK&sid=SID&sni=SNI&fp=firefox&spx=%2Fspx&pqv=PQV") + res, err := ParseLink("vless://uuid@h.com:443?type=tcp&security=reality&pbk=PBK&sid=SID&sni=SNI&fp=firefox&spx=%2Fspx&pqv=PQV&support-x25519mlkem768=true") if err != nil { t.Fatalf("parse: %v", err) } @@ -123,6 +127,24 @@ func TestParse_RealitySecurityMapped(t *testing.T) { } } +// Xray-core drops unknown JSON keys silently, so a key its REALITYConfig lacks +// would reach the outbound as a setting that does nothing. +func TestParse_RealitySettingsAreXrayFields(t *testing.T) { + res, err := ParseLink("vless://uuid@h.com:443?type=tcp&security=reality&pbk=PBK&sid=SID&sni=SNI&fp=firefox&spx=%2Fspx&pqv=PQV&support-x25519mlkem768=true") + if err != nil { + t.Fatalf("parse: %v", err) + } + raw, err := json.Marshal(streamSub(t, res, "realitySettings")) + if err != nil { + t.Fatalf("marshal: %v", err) + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&conf.REALITYConfig{}); err != nil { + t.Fatalf("realitySettings %s is not an xray-core REALITY config: %v", raw, err) + } +} + 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") if err != nil {