From e4ce46e1a1af6dbf6413803297ccdc76bbff8272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A7=81=E6=99=93=E7=A9=BA?= Date: Fri, 28 Aug 2026 15:10:34 +0800 Subject: [PATCH 1/6] fix(export): omit disabled packet encoding --- src/lib/client-export/common.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/client-export/common.ts b/src/lib/client-export/common.ts index 75358d3..2831a20 100644 --- a/src/lib/client-export/common.ts +++ b/src/lib/client-export/common.ts @@ -229,11 +229,12 @@ export function buildMultiplex( export function buildPacketEncoding( query: URLSearchParams, -): string | null | "invalid" { +): "packetaddr" | "xudp" | null | "invalid" { const value = queryValue(query, "packetEncoding", "packet_encoding"); if (!value) return null; const normalized = value.toLowerCase(); - if (["none", "packetaddr", "xudp"].includes(normalized)) { + if (normalized === "none") return null; + if (normalized === "packetaddr" || normalized === "xudp") { return normalized; } return "invalid"; From 5d60df1e8a8c0a34460b0b02cd6210f72b208a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A7=81=E6=99=93=E7=A9=BA?= Date: Fri, 28 Aug 2026 15:11:02 +0800 Subject: [PATCH 2/6] feat(export): preserve VMess legacy protocol options --- src/lib/client-export/vmess.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/lib/client-export/vmess.ts b/src/lib/client-export/vmess.ts index 17bb163..01e40c5 100644 --- a/src/lib/client-export/vmess.ts +++ b/src/lib/client-export/vmess.ts @@ -37,9 +37,14 @@ export function parseVmess( const security = stringValue(data.scy) ?? stringValue(data.security); if ( security && - !["auto", "none", "zero", "aes-128-gcm", "chacha20-poly1305"].includes( - security, - ) + ![ + "auto", + "none", + "zero", + "aes-128-gcm", + "chacha20-poly1305", + "aes-128-ctr", + ].includes(security) ) { return `Invalid vmess URI: unsupported security for ${fallbackTag}`; } @@ -60,6 +65,17 @@ export function parseVmess( } } + const globalPadding = booleanValue( + data.global_padding ?? data.globalPadding, + ); + if (globalPadding !== null) outbound.global_padding = globalPadding; + const authenticatedLength = booleanValue( + data.authenticated_length ?? data.authenticatedLength, + ); + if (authenticatedLength !== null) { + outbound.authenticated_length = authenticatedLength; + } + const network = stringValue(data.net) ?? stringValue(data.network); if (network === "tcp" || network === "udp") outbound.network = network; const transportQuery = new URLSearchParams(); From 3dbccb9cefe52ab62629eb8d2340d1f74e854512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A7=81=E6=99=93=E7=A9=BA?= Date: Fri, 28 Aug 2026 15:11:36 +0800 Subject: [PATCH 3/6] test(export): cover VMess legacy fidelity --- src/lib/client-export/vmess-fidelity.test.ts | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/lib/client-export/vmess-fidelity.test.ts diff --git a/src/lib/client-export/vmess-fidelity.test.ts b/src/lib/client-export/vmess-fidelity.test.ts new file mode 100644 index 0000000..58e3542 --- /dev/null +++ b/src/lib/client-export/vmess-fidelity.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "bun:test"; +import { parseProxyUriToSingBoxOutbound } from "./uri"; + +function vmessUri(payload: Record): string { + const json = JSON.stringify(payload); + const encoded = btoa( + String.fromCharCode(...new TextEncoder().encode(json)), + ); + return `vmess://${encoded}`; +} + +describe("sing-box VMess fidelity", () => { + it("omits packet_encoding when VLESS explicitly disables it", () => { + const result = parseProxyUriToSingBoxOutbound( + "vless://00000000-0000-4000-8000-000000000101@example.com:443?security=tls&packetEncoding=none#VLESS%20No%20Packet", + "fallback", + ); + + expect(result.warning).toBeNull(); + expect(result.outbound).not.toBeNull(); + expect(result.outbound && "packet_encoding" in result.outbound).toBe(false); + }); + + it("omits packet_encoding when VMess explicitly disables it", () => { + const result = parseProxyUriToSingBoxOutbound( + vmessUri({ + add: "example.com", + port: 443, + id: "00000000-0000-4000-8000-000000000102", + packetEncoding: "none", + }), + "VMess No Packet", + ); + + expect(result.warning).toBeNull(); + expect(result.outbound).not.toBeNull(); + expect(result.outbound && "packet_encoding" in result.outbound).toBe(false); + }); + + it("preserves legacy VMess security and protocol booleans", () => { + const result = parseProxyUriToSingBoxOutbound( + vmessUri({ + add: "legacy.example.com", + port: "443", + id: "00000000-0000-4000-8000-000000000103", + scy: "aes-128-ctr", + aid: "1", + globalPadding: "1", + authenticated_length: 0, + }), + "Legacy VMess", + ); + + expect(result.warning).toBeNull(); + expect(result.outbound).toEqual({ + type: "vmess", + tag: "Legacy VMess", + server: "legacy.example.com", + server_port: 443, + uuid: "00000000-0000-4000-8000-000000000103", + security: "aes-128-ctr", + alter_id: 1, + global_padding: true, + authenticated_length: false, + }); + }); + + it("continues to reject unknown packet encodings", () => { + const result = parseProxyUriToSingBoxOutbound( + vmessUri({ + add: "example.com", + port: 443, + id: "00000000-0000-4000-8000-000000000104", + packet_encoding: "unsupported", + }), + "Invalid VMess", + ); + + expect(result.outbound).toBeNull(); + expect(result.warning).toContain("unsupported packet encoding"); + }); +}); From cd0abbd620cf8af2865307c6d10678070f2993af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A7=81=E6=99=93=E7=A9=BA?= Date: Fri, 28 Aug 2026 15:12:00 +0800 Subject: [PATCH 4/6] docs(export): document VMess fidelity mappings --- docs/sing-box-export.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sing-box-export.md b/docs/sing-box-export.md index b68ccd3..5023f96 100644 --- a/docs/sing-box-export.md +++ b/docs/sing-box-export.md @@ -7,7 +7,7 @@ editing and presentation; Workspace publication goes through the existing revisioned mutation boundary. The URI mappings in this document were checked against the current sing-box -outbound documentation for the v1.13 line on 2026-08-24. A mapping is added +outbound documentation for the v1.14 line on 2026-08-28. A mapping is added only when the source URI field has a reliable sing-box equivalent. ## Supported outbound matrix @@ -15,7 +15,7 @@ only when the source URI field has a reliable sing-box equivalent. | Source URI | Generated outbound | Mapped fields | Notes | | --- | --- | --- | --- | | `vless://` | `vless` | server, port, UUID, flow, network, packet encoding, TLS/Reality, transport, multiplex | `none`, `tls`, and `reality` security; only `xtls-rprx-vision` flow is accepted | -| `vmess://` | `vmess` | base64 JSON server/port/UUID, security, alter ID, network, TLS, transport, packet encoding, multiplex | Payload must be valid base64 UTF-8 JSON | +| `vmess://` | `vmess` | base64 JSON server/port/UUID, security, alter ID, global padding, authenticated length, network, TLS, transport, packet encoding, multiplex | Includes the legacy `aes-128-ctr` security value; payload must be valid base64 UTF-8 JSON | | `trojan://` | `trojan` | server, port, password, TLS, transport, multiplex | Trojan's TLS outbound is enabled by default | | `ss://` | `shadowsocks` | method, password, server/port, SIP003 plugin/options, UDP-over-TCP, multiplex | Direct and base64 authority forms are accepted | | `hysteria2://`, `hy2://` | `hysteria2` | server/port, password, network, TLS/SNI/ALPN/insecure, Salamander obfuscation | Only the `salamander` obfuscation type is emitted | @@ -26,7 +26,9 @@ only when the source URI field has a reliable sing-box equivalent. TLS mappings include SNI, ALPN, certificate verification bypass (`insecure`), and the VLESS Reality public key/short ID when present. WebSocket, gRPC, HTTP, and HTTPUpgrade transport fields preserve their path, host, and service name -where the source format provides them. Unsupported transport, security, +where the source format provides them. Packet encoding values `packetaddr` and +`xudp` are emitted directly; an explicit `none` disables the field and is +therefore omitted from generated JSON. Unsupported transport, security, packet-encoding, or malformed percent/base64 values produce a warning for that line and do not discard other convertible lines. From 31407baf854653824b653b849ef4e0f69fbbdb37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A7=81=E6=99=93=E7=A9=BA?= Date: Fri, 28 Aug 2026 15:14:33 +0800 Subject: [PATCH 5/6] test(export): use declared Bun matchers --- src/lib/client-export/vmess-fidelity.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/client-export/vmess-fidelity.test.ts b/src/lib/client-export/vmess-fidelity.test.ts index 58e3542..b57ec95 100644 --- a/src/lib/client-export/vmess-fidelity.test.ts +++ b/src/lib/client-export/vmess-fidelity.test.ts @@ -17,7 +17,7 @@ describe("sing-box VMess fidelity", () => { ); expect(result.warning).toBeNull(); - expect(result.outbound).not.toBeNull(); + expect(Boolean(result.outbound)).toBe(true); expect(result.outbound && "packet_encoding" in result.outbound).toBe(false); }); @@ -33,7 +33,7 @@ describe("sing-box VMess fidelity", () => { ); expect(result.warning).toBeNull(); - expect(result.outbound).not.toBeNull(); + expect(Boolean(result.outbound)).toBe(true); expect(result.outbound && "packet_encoding" in result.outbound).toBe(false); }); From c9888abf97fceb3aca55e98bc99e815e04425f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A7=81=E6=99=93=E7=A9=BA?= Date: Fri, 28 Aug 2026 15:15:55 +0800 Subject: [PATCH 6/6] style(export): apply Biome formatting --- src/lib/client-export/vmess-fidelity.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/client-export/vmess-fidelity.test.ts b/src/lib/client-export/vmess-fidelity.test.ts index b57ec95..bde664a 100644 --- a/src/lib/client-export/vmess-fidelity.test.ts +++ b/src/lib/client-export/vmess-fidelity.test.ts @@ -3,9 +3,7 @@ import { parseProxyUriToSingBoxOutbound } from "./uri"; function vmessUri(payload: Record): string { const json = JSON.stringify(payload); - const encoded = btoa( - String.fromCharCode(...new TextEncoder().encode(json)), - ); + const encoded = btoa(String.fromCharCode(...new TextEncoder().encode(json))); return `vmess://${encoded}`; }