From 750a2709554f7f468c22c301eb0170e7ccd1fbf8 Mon Sep 17 00:00:00 2001 From: Muse Date: Thu, 22 Oct 2026 18:14:58 +0000 Subject: [PATCH] ITEM6 B0: golden fixtures + fixture validation skeleton for sing-box migration 26 cases (protocol x representative transport): current-shape input, expected sing-box fragment, and share-link baseline generated by running current main@8e89960 GetLink. Blocked/partial cases (xhttp, kcp, fallbacks, finalmask, vlessenc, removed SS ciphers, tunnel) record their unmapped concepts under _b0 for the Q1 decision instead of fabricating output. Link-route baselines only: JSON/Clash subscription families are being removed wholesale per the 2026-10-23 decision, Q2 closed. --- internal/singbox/README.md | 64 ++++++++++ internal/singbox/doc.go | 20 +++ internal/singbox/golden_test.go | 120 ++++++++++++++++++ .../golden/amneziawg/expected-link.txt | 1 + .../golden/amneziawg/expected-singbox.json | 7 + .../testdata/golden/amneziawg/input.json | 80 ++++++++++++ .../golden/http-auth/expected-link.txt | 0 .../golden/http-auth/expected-singbox.json | 20 +++ .../testdata/golden/http-auth/input.json | 40 ++++++ .../golden/hysteria1/expected-link.txt | 1 + .../golden/hysteria1/expected-singbox.json | 29 +++++ .../testdata/golden/hysteria1/input.json | 82 ++++++++++++ .../hysteria2-salamander/expected-link.txt | 1 + .../expected-singbox.json | 33 +++++ .../golden/hysteria2-salamander/input.json | 100 +++++++++++++++ .../golden/mixed-auth/expected-link.txt | 1 + .../golden/mixed-auth/expected-singbox.json | 24 ++++ .../testdata/golden/mixed-auth/input.json | 57 +++++++++ .../testdata/golden/mtproto/expected-link.txt | 1 + .../golden/mtproto/expected-singbox.json | 7 + .../testdata/golden/mtproto/input.json | 50 ++++++++ .../golden/shadowsocks-2022/expected-link.txt | 1 + .../shadowsocks-2022/expected-singbox.json | 26 ++++ .../golden/shadowsocks-2022/input.json | 88 +++++++++++++ .../shadowsocks-legacy/expected-link.txt | 1 + .../shadowsocks-legacy/expected-singbox.json | 22 ++++ .../golden/shadowsocks-legacy/input.json | 74 +++++++++++ .../expected-link.txt | 1 + .../expected-singbox.json | 17 +++ .../shadowsocks-removed-cipher/input.json | 74 +++++++++++ .../golden/socks-auth/expected-link.txt | 1 + .../golden/socks-auth/expected-singbox.json | 24 ++++ .../testdata/golden/socks-auth/input.json | 57 +++++++++ .../trojan-tcp-tls-flow/expected-link.txt | 1 + .../trojan-tcp-tls-flow/expected-singbox.json | 39 ++++++ .../golden/trojan-tcp-tls-flow/input.json | 87 +++++++++++++ .../golden/trojan-tcp-tls/expected-link.txt | 1 + .../trojan-tcp-tls/expected-singbox.json | 30 +++++ .../testdata/golden/trojan-tcp-tls/input.json | 85 +++++++++++++ .../testdata/golden/tun/expected-link.txt | 0 .../testdata/golden/tun/expected-singbox.json | 39 ++++++ .../singbox/testdata/golden/tun/input.json | 46 +++++++ .../golden/tunnel-dokodemo/expected-link.txt | 0 .../tunnel-dokodemo/expected-singbox.json | 22 ++++ .../golden/tunnel-dokodemo/input.json | 38 ++++++ .../golden/vless-grpc-tls/expected-link.txt | 1 + .../vless-grpc-tls/expected-singbox.json | 35 +++++ .../testdata/golden/vless-grpc-tls/input.json | 87 +++++++++++++ .../vless-httpupgrade-tls/expected-link.txt | 1 + .../expected-singbox.json | 35 +++++ .../golden/vless-httpupgrade-tls/input.json | 87 +++++++++++++ .../vless-tcp-finalmask/expected-link.txt | 1 + .../vless-tcp-finalmask/expected-singbox.json | 52 ++++++++ .../golden/vless-tcp-finalmask/input.json | 108 ++++++++++++++++ .../expected-link.txt | 1 + .../expected-singbox.json | 52 ++++++++ .../vless-tcp-reality-fallbacks/input.json | 105 +++++++++++++++ .../vless-tcp-reality/expected-link.txt | 1 + .../vless-tcp-reality/expected-singbox.json | 37 ++++++ .../golden/vless-tcp-reality/input.json | 97 ++++++++++++++ .../golden/vless-vlessenc/expected-link.txt | 1 + .../vless-vlessenc/expected-singbox.json | 30 +++++ .../testdata/golden/vless-vlessenc/input.json | 73 +++++++++++ .../golden/vless-ws-tls/expected-link.txt | 1 + .../golden/vless-ws-tls/expected-singbox.json | 38 ++++++ .../testdata/golden/vless-ws-tls/input.json | 91 +++++++++++++ .../golden/vless-xhttp-tls/expected-link.txt | 1 + .../vless-xhttp-tls/expected-singbox.json | 23 ++++ .../golden/vless-xhttp-tls/input.json | 96 ++++++++++++++ .../golden/vmess-kcp/expected-link.txt | 1 + .../golden/vmess-kcp/expected-singbox.json | 25 ++++ .../testdata/golden/vmess-kcp/input.json | 76 +++++++++++ .../vmess-tcp-httpheader/expected-link.txt | 1 + .../expected-singbox.json | 33 +++++ .../golden/vmess-tcp-httpheader/input.json | 82 ++++++++++++ .../golden/vmess-ws-tls/expected-link.txt | 1 + .../golden/vmess-ws-tls/expected-singbox.json | 40 ++++++ .../testdata/golden/vmess-ws-tls/input.json | 87 +++++++++++++ .../golden/wireguard/expected-link.txt | 1 + .../golden/wireguard/expected-singbox.json | 29 +++++ .../testdata/golden/wireguard/input.json | 65 ++++++++++ 81 files changed, 3007 insertions(+) create mode 100644 internal/singbox/README.md create mode 100644 internal/singbox/doc.go create mode 100644 internal/singbox/golden_test.go create mode 100644 internal/singbox/testdata/golden/amneziawg/expected-link.txt create mode 100644 internal/singbox/testdata/golden/amneziawg/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/amneziawg/input.json create mode 100644 internal/singbox/testdata/golden/http-auth/expected-link.txt create mode 100644 internal/singbox/testdata/golden/http-auth/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/http-auth/input.json create mode 100644 internal/singbox/testdata/golden/hysteria1/expected-link.txt create mode 100644 internal/singbox/testdata/golden/hysteria1/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/hysteria1/input.json create mode 100644 internal/singbox/testdata/golden/hysteria2-salamander/expected-link.txt create mode 100644 internal/singbox/testdata/golden/hysteria2-salamander/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/hysteria2-salamander/input.json create mode 100644 internal/singbox/testdata/golden/mixed-auth/expected-link.txt create mode 100644 internal/singbox/testdata/golden/mixed-auth/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/mixed-auth/input.json create mode 100644 internal/singbox/testdata/golden/mtproto/expected-link.txt create mode 100644 internal/singbox/testdata/golden/mtproto/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/mtproto/input.json create mode 100644 internal/singbox/testdata/golden/shadowsocks-2022/expected-link.txt create mode 100644 internal/singbox/testdata/golden/shadowsocks-2022/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/shadowsocks-2022/input.json create mode 100644 internal/singbox/testdata/golden/shadowsocks-legacy/expected-link.txt create mode 100644 internal/singbox/testdata/golden/shadowsocks-legacy/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/shadowsocks-legacy/input.json create mode 100644 internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-link.txt create mode 100644 internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/shadowsocks-removed-cipher/input.json create mode 100644 internal/singbox/testdata/golden/socks-auth/expected-link.txt create mode 100644 internal/singbox/testdata/golden/socks-auth/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/socks-auth/input.json create mode 100644 internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-link.txt create mode 100644 internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/trojan-tcp-tls-flow/input.json create mode 100644 internal/singbox/testdata/golden/trojan-tcp-tls/expected-link.txt create mode 100644 internal/singbox/testdata/golden/trojan-tcp-tls/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/trojan-tcp-tls/input.json create mode 100644 internal/singbox/testdata/golden/tun/expected-link.txt create mode 100644 internal/singbox/testdata/golden/tun/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/tun/input.json create mode 100644 internal/singbox/testdata/golden/tunnel-dokodemo/expected-link.txt create mode 100644 internal/singbox/testdata/golden/tunnel-dokodemo/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/tunnel-dokodemo/input.json create mode 100644 internal/singbox/testdata/golden/vless-grpc-tls/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-grpc-tls/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-grpc-tls/input.json create mode 100644 internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-httpupgrade-tls/input.json create mode 100644 internal/singbox/testdata/golden/vless-tcp-finalmask/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-tcp-finalmask/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-tcp-finalmask/input.json create mode 100644 internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/input.json create mode 100644 internal/singbox/testdata/golden/vless-tcp-reality/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-tcp-reality/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-tcp-reality/input.json create mode 100644 internal/singbox/testdata/golden/vless-vlessenc/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-vlessenc/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-vlessenc/input.json create mode 100644 internal/singbox/testdata/golden/vless-ws-tls/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-ws-tls/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-ws-tls/input.json create mode 100644 internal/singbox/testdata/golden/vless-xhttp-tls/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vless-xhttp-tls/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vless-xhttp-tls/input.json create mode 100644 internal/singbox/testdata/golden/vmess-kcp/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vmess-kcp/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vmess-kcp/input.json create mode 100644 internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vmess-tcp-httpheader/input.json create mode 100644 internal/singbox/testdata/golden/vmess-ws-tls/expected-link.txt create mode 100644 internal/singbox/testdata/golden/vmess-ws-tls/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/vmess-ws-tls/input.json create mode 100644 internal/singbox/testdata/golden/wireguard/expected-link.txt create mode 100644 internal/singbox/testdata/golden/wireguard/expected-singbox.json create mode 100644 internal/singbox/testdata/golden/wireguard/input.json diff --git a/internal/singbox/README.md b/internal/singbox/README.md new file mode 100644 index 0000000..b36a26b --- /dev/null +++ b/internal/singbox/README.md @@ -0,0 +1,64 @@ +# ITEM6 golden fixtures (B0) + +Per-protocol × representative-transport fixtures for the xray→sing-box +migration. Consumed by B1 (data model + migrator) and B2 (config builder) +as the acceptance baseline; see `ITEM6-CONVERSION-SPEC.md` in +`~/workspace/xui-lite-work/` for the normative mapping behind every file. + +## Layout + +``` +testdata/golden// + input.json current-shape sample (see below) + expected-singbox.json expected converter output fragment + expected-link.txt expected share link for the primary client +``` + +`input.json` fields: `case`, `title`, `source` (where the shape was taken +from — frontend defaults / backend tests / production backup traits), +`disposition`, `risk` (ITEM6-SCOPE N-number or null), `notes`, `inbound` +(a `model.Inbound` row shape: `settings` / `streamSettings` / `sniffing` +as embedded objects), `clientEmail`, `client` (the primary client's +settings-mirror entry — the same object embedded in `settings.clients` +where the protocol carries one). + +`expected-singbox.json` is a fragment: `inbounds[]` (and `endpoints[]` +for WireGuard). Keys starting with `_` are fixture metadata (`_b0`: +disposition, unmapped concepts awaiting Q1, notes) and are stripped +before comparison. Blocked cases intentionally contain **no** fabricated +output — `inbounds` is empty and `_b0.unmapped` records exactly what has +no sing-box carrier, pending the user's Q1 decision. + +## Provenance of the link baselines + +`expected-link.txt` files were generated on 2026-10-23 by running the +**current** code (main `8e89960`) `SubService.GetLink` over each +`input.json` inbound — i.e. they are the pre-migration outputs the +migration must reproduce byte for byte (ITEM6-SCOPE §2.4). A throwaway +harness test produced them and was deleted afterwards; regenerate only +by re-running `GetLink` against the same inputs on pre-migration code. +Empty files are real current behaviour for `http-auth`, `tun` and +`tunnel-dokodemo` (no share link exists today). + +All credentials in this corpus are fake (sequential-byte X25519/WireGuard +keys, `fx-` passwords, TEST-NET `203.0.113.10`). In fixtures, `listen` +doubles as the resolved share address, mirroring the characterization +tests in `internal/sub`; bind-address defaults are B2 base-config +business, not fixture content. + +## Baseline scope: links only + +Per the 2026-10-23 product decision, the JSON and Clash subscription +families are being deleted wholesale (separate work line); subscriptions +converge on the link format only. No Clash/JSON baselines are built or +kept here, and ITEM6-SCOPE Q2 is closed by that decision. + +## Dispositions + +- `direct` — full mapping defined; B2 must reproduce the fragment. +- `partial-pending-q1` — mappable body is in the fragment; `_b0.unmapped` + lists concepts with no sing-box carrier, awaiting Q1. +- `blocked-pending-q1` — no valid output exists until Q1 picks a carrier; + fragment is an explicit marker, not a silent drop. +- `external-runtime` — protocol never enters the core (mtproto, + amneziawg); the migration duty is verbatim data passthrough only. diff --git a/internal/singbox/doc.go b/internal/singbox/doc.go new file mode 100644 index 0000000..7f81dda --- /dev/null +++ b/internal/singbox/doc.go @@ -0,0 +1,20 @@ +// Package singbox hosts the L-UI xray→sing-box migration (ITEM6) assets. +// +// B0 deliverable: the golden fixture corpus under testdata/golden — for each +// protocol × representative transport, a current-shape sample (input.json), +// the expected sing-box fragment (expected-singbox.json), and the expected +// share-link output (expected-link.txt) produced by the CURRENT code at +// main 8e89960. B1 (data model + migrator) and B2 (config builder) consume +// these fixtures as their acceptance baseline: +// +// - the converter's output for input.json must equal expected-singbox.json +// (keys starting with "_" are fixture metadata, stripped before compare); +// - subscription links regenerated from the converted model must equal +// expected-link.txt byte for byte (ITEM6-SCOPE §2.4 invariants). +// +// Subscription baselines cover the LINK route only. Per the 2026-10-23 +// product decision, the JSON and Clash subscription families are being +// removed entirely (separate work line); no baselines are kept for them. +// +// The conversion spec lives in ~/workspace/xui-lite-work/ITEM6-CONVERSION-SPEC.md. +package singbox diff --git a/internal/singbox/golden_test.go b/internal/singbox/golden_test.go new file mode 100644 index 0000000..5f78453 --- /dev/null +++ b/internal/singbox/golden_test.go @@ -0,0 +1,120 @@ +package singbox + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "strings" + "testing" +) + +// goldenCase mirrors the fixture envelope shared by input.json and +// expected-singbox.json. The tests in this file are READ-ONLY fixture +// validation: they pin the corpus so B1/B2 can build the converter +// against it. They never write to testdata. +type goldenEnvelope struct { + Case string `json:"case"` + B0 *struct { + Disposition string `json:"disposition"` + Unmapped []json.RawMessage `json:"unmapped"` + } `json:"_b0"` + Inbounds []json.RawMessage `json:"inbounds"` +} + +var knownDispositions = map[string]bool{ + "direct": true, + "partial-pending-q1": true, + "blocked-pending-q1": true, + "external-runtime": true, + "external": true, // used on the input side +} + +func goldenDirs(t *testing.T) []string { + t.Helper() + root := filepath.Join("testdata", "golden") + entries, err := os.ReadDir(root) + if err != nil { + t.Fatalf("read golden root: %v", err) + } + var dirs []string + for _, e := range entries { + if e.IsDir() { + dirs = append(dirs, filepath.Join(root, e.Name())) + } + } + sort.Strings(dirs) + if len(dirs) == 0 { + t.Fatal("no golden fixture cases found") + } + return dirs +} + +func readJSON(t *testing.T, path string, v any) { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if err := json.Unmarshal(raw, v); err != nil { + t.Fatalf("parse %s: %v", path, err) + } +} + +// TestGoldenFixturesWellFormed pins corpus completeness: every case dir +// carries input.json, expected-singbox.json and expected-link.txt; every +// JSON parses; dispositions are from the known taxonomy; blocked cases +// must not smuggle in a fabricated sing-box output — they carry an empty +// inbounds list plus at least one _b0.unmapped entry awaiting Q1. +func TestGoldenFixturesWellFormed(t *testing.T) { + for _, dir := range goldenDirs(t) { + name := filepath.Base(dir) + + var input struct { + Case string `json:"case"` + Disposition string `json:"disposition"` + } + readJSON(t, filepath.Join(dir, "input.json"), &input) + if input.Case != name { + t.Errorf("%s: input case %q != dir name", name, input.Case) + } + if !knownDispositions[input.Disposition] { + t.Errorf("%s: unknown input disposition %q", name, input.Disposition) + } + + var expected goldenEnvelope + readJSON(t, filepath.Join(dir, "expected-singbox.json"), &expected) + if expected.B0 == nil { + t.Errorf("%s: expected-singbox.json missing _b0 metadata", name) + continue + } + disp := expected.B0.Disposition + if !knownDispositions[disp] { + t.Errorf("%s: unknown expected disposition %q", name, disp) + } + if strings.HasPrefix(disp, "blocked") { + if len(expected.Inbounds) != 0 { + t.Errorf("%s: blocked case must not fabricate sing-box inbounds", name) + } + if len(expected.B0.Unmapped) == 0 { + t.Errorf("%s: blocked case must record >=1 unmapped concept for Q1", name) + } + } + if strings.HasPrefix(disp, "partial") && len(expected.B0.Unmapped) == 0 { + t.Errorf("%s: partial case must record its unmapped concepts", name) + } + + if _, err := os.Stat(filepath.Join(dir, "expected-link.txt")); err != nil { + t.Errorf("%s: missing expected-link.txt: %v", name, err) + } + } +} + +// TestGoldenConversion is the B1/B2 hook: once the converter exists, feed +// each input.json through it and compare against expected-singbox.json +// (metadata keys stripped), then regenerate share links from the converted +// model and compare against expected-link.txt byte for byte. Skipped until +// the converter lands. +func TestGoldenConversion(t *testing.T) { + t.Skip("converter lands in ITEM6 B1/B2; fixtures are the baseline it must meet") +} diff --git a/internal/singbox/testdata/golden/amneziawg/expected-link.txt b/internal/singbox/testdata/golden/amneziawg/expected-link.txt new file mode 100644 index 0000000..71b9bca --- /dev/null +++ b/internal/singbox/testdata/golden/amneziawg/expected-link.txt @@ -0,0 +1 @@ +vpn://W0ludGVyZmFjZV0KUHJpdmF0ZUtleSA9IE16UTFOamM0T1RvN1BEMCtQMEJCUWtORVJVWkhTRWxLUzB4TlRrOVFVVkkKQWRkcmVzcyA9IDEwLjguMS4yLzMyCkROUyA9IDguOC44LjgsIDguOC40LjQKSmMgPSA0CkptaW4gPSA0MApKbWF4ID0gNzAKUzEgPSAyMApTMiA9IDMwCkgxID0gMTAwMDAwMQpIMiA9IDEwMDAwMDIKSDMgPSAxMDAwMDAzCkg0ID0gMTAwMDAwNAoKIyBmeC1hbW5lemlhd2ctdXNlckBleGFtcGxlLmNvbQpbUGVlcl0KUHVibGljS2V5ID0gd1YwaVpVV1VWY24vRlc1c0hhYS90NUVMdUs5UThyTDUrRlBxa3lVbG5VMD0KQWxsb3dlZElQcyA9IDAuMC4wLjAvMCwgOjovMApFbmRwb2ludCA9IDIwMy4wLjExMy4xMDozODQ0Mw \ No newline at end of file diff --git a/internal/singbox/testdata/golden/amneziawg/expected-singbox.json b/internal/singbox/testdata/golden/amneziawg/expected-singbox.json new file mode 100644 index 0000000..fa05f61 --- /dev/null +++ b/internal/singbox/testdata/golden/amneziawg/expected-singbox.json @@ -0,0 +1,7 @@ +{ + "inbounds": [], + "_b0": { + "disposition": "external-runtime", + "notes": "不进核心(internal/amneziawg 外部运行时)。sing-box wireguard endpoint 表达不了 Amnezia 混淆参数(jc/jmin/jmax/s1-s4/h1-h4/i1-i5)与 per-client forwardedPorts;新模型义务仅为 server/clients 数据原样透传,vpn:// 链接与 .conf 不变。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/amneziawg/input.json b/internal/singbox/testdata/golden/amneziawg/input.json new file mode 100644 index 0000000..34a396e --- /dev/null +++ b/internal/singbox/testdata/golden/amneziawg/input.json @@ -0,0 +1,80 @@ +{ + "case": "amneziawg", + "title": "AmneziaWG(外部运行时承载,不进核心)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts", + "internal/amneziawg/types.go ServerSettings" + ], + "disposition": "external", + "risk": null, + "notes": "sing-box wireguard endpoint 表达不了 Amnezia 混淆参数(jc/jmin/jmax/s/h/i)与 per-client 转发端口;运行时与参数原样保留,仅数据模型透传。", + "inbound": { + "id": 126, + "listen": "203.0.113.10", + "port": 38443, + "protocol": "amneziawg", + "remark": "fx-amneziawg", + "tag": "inbound-38443", + "enable": true, + "settings": { + "server": { + "privateKey": "ycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+g", + "publicKey": "wV0iZUWUVcn/FW5sHaa/t5ELuK9Q8rL5+FPqkyUlnU0=", + "subnetIp": "10.8.1.0", + "subnetCidr": 24, + "primaryDns": "8.8.8.8", + "secondaryDns": "8.8.4.4", + "externalInterface": "", + "ipv6Enabled": false, + "ipv6Subnet": "", + "ipv6ExternalInterface": "", + "jc": 4, + "jmin": 40, + "jmax": 70, + "s1": 20, + "s2": 30, + "h1": "1000001", + "h2": "1000002", + "h3": "1000003", + "h4": "1000004" + }, + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "privateKey": "MzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVI", + "publicKey": "wV0iZUWUVcn/FW5sHaa/t5ELuK9Q8rL5+FPqkyUlnU0=", + "allowedIPs": [ + "10.8.1.2/32" + ], + "forwardedPorts": "" + } + ] + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "privateKey": "MzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVI", + "publicKey": "wV0iZUWUVcn/FW5sHaa/t5ELuK9Q8rL5+FPqkyUlnU0=", + "allowedIPs": [ + "10.8.1.2/32" + ], + "forwardedPorts": "" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/http-auth/expected-link.txt b/internal/singbox/testdata/golden/http-auth/expected-link.txt new file mode 100644 index 0000000..e69de29 diff --git a/internal/singbox/testdata/golden/http-auth/expected-singbox.json b/internal/singbox/testdata/golden/http-auth/expected-singbox.json new file mode 100644 index 0000000..88f22c1 --- /dev/null +++ b/internal/singbox/testdata/golden/http-auth/expected-singbox.json @@ -0,0 +1,20 @@ +{ + "inbounds": [ + { + "type": "http", + "tag": "inbound-33443", + "listen": "203.0.113.10", + "listen_port": 33443, + "users": [ + { + "username": "fxhttp", + "password": "fxhttppass" + } + ] + } + ], + "_b0": { + "disposition": "direct", + "notes": "allowTransparent=false 与 sing-box(无透明代理对应)行为一致,无损;allowTransparent=true 的存量处置见规范(无对应,待 Q1)。http 无分享链接(expected-link.txt 为空,属现行行为)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/http-auth/input.json b/internal/singbox/testdata/golden/http-auth/input.json new file mode 100644 index 0000000..4a343bb --- /dev/null +++ b/internal/singbox/testdata/golden/http-auth/input.json @@ -0,0 +1,40 @@ +{ + "case": "http-auth", + "title": "HTTP 代理 / 账号", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "GetLink 对 http 返回空(无分享链);Clash/JSON 两路有输出。sing-box http 入站 users 改名映射,allowTransparent 无对应(本样本 false,无损)。", + "inbound": { + "id": 119, + "listen": "203.0.113.10", + "port": 33443, + "protocol": "http", + "remark": "fx-http", + "tag": "inbound-33443", + "enable": true, + "settings": { + "accounts": [ + { + "user": "fxhttp", + "pass": "fxhttppass" + } + ], + "allowTransparent": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0 + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/hysteria1/expected-link.txt b/internal/singbox/testdata/golden/hysteria1/expected-link.txt new file mode 100644 index 0000000..f2df7a6 --- /dev/null +++ b/internal/singbox/testdata/golden/hysteria1/expected-link.txt @@ -0,0 +1 @@ +hysteria://fx-hy1-auth@203.0.113.10:35443?alpn=h3&fp=chrome&security=tls&sni=hy1.example.com#fx-hy1-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/hysteria1/expected-singbox.json b/internal/singbox/testdata/golden/hysteria1/expected-singbox.json new file mode 100644 index 0000000..b2c9722 --- /dev/null +++ b/internal/singbox/testdata/golden/hysteria1/expected-singbox.json @@ -0,0 +1,29 @@ +{ + "inbounds": [ + { + "type": "hysteria", + "tag": "inbound-35443", + "listen": "203.0.113.10", + "listen_port": 35443, + "users": [ + { + "name": "user@example.com", + "auth": "fx-hy1-auth" + } + ], + "tls": { + "enabled": true, + "server_name": "hy1.example.com", + "alpn": [ + "h3" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + } + } + ], + "_b0": { + "disposition": "direct", + "notes": "v1 按 type:hysteria 分发;users 字段为 auth(v1 语义)。带宽字段(up/down_mbps)现形状无来源(hysteriaSettings 只有 version/udpIdleTimeout),省略走核心默认;B1 落形时以 v1.13.21 文档复核字段名。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/hysteria1/input.json b/internal/singbox/testdata/golden/hysteria1/input.json new file mode 100644 index 0000000..1ad9088 --- /dev/null +++ b/internal/singbox/testdata/golden/hysteria1/input.json @@ -0,0 +1,82 @@ +{ + "case": "hysteria1", + "title": "Hysteria v1", + "source": [ + "internal/database/model/hysteria_version_test.go" + ], + "disposition": "direct", + "risk": null, + "notes": "按 version 分发:v1 → sing-box type:hysteria(auth 语义字段)。现网前端强制 v2、本样本为存量兜底;生成侧有 version 自愈逻辑,B1 迁移器须以 settings.version 原值为准。", + "inbound": { + "id": 121, + "listen": "203.0.113.10", + "port": 35443, + "protocol": "hysteria", + "remark": "fx-hy1", + "tag": "inbound-35443", + "enable": true, + "settings": { + "version": 1, + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "auth": "fx-hy1-auth" + } + ] + }, + "streamSettings": { + "network": "hysteria", + "security": "tls", + "hysteriaSettings": { + "version": 1, + "udpIdleTimeout": 60 + }, + "tlsSettings": { + "serverName": "hy1.example.com", + "alpn": [ + "h3" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "auth": "fx-hy1-auth" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/hysteria2-salamander/expected-link.txt b/internal/singbox/testdata/golden/hysteria2-salamander/expected-link.txt new file mode 100644 index 0000000..43ce4ad --- /dev/null +++ b/internal/singbox/testdata/golden/hysteria2-salamander/expected-link.txt @@ -0,0 +1 @@ +hysteria2://fx-hy2-auth@203.0.113.10:34443?alpn=h3&fp=chrome&obfs=salamander&obfs-password=fx-obfs-pass&security=tls&sni=hy.example.com#fx-hy2-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/hysteria2-salamander/expected-singbox.json b/internal/singbox/testdata/golden/hysteria2-salamander/expected-singbox.json new file mode 100644 index 0000000..d7125cc --- /dev/null +++ b/internal/singbox/testdata/golden/hysteria2-salamander/expected-singbox.json @@ -0,0 +1,33 @@ +{ + "inbounds": [ + { + "type": "hysteria2", + "tag": "inbound-34443", + "listen": "203.0.113.10", + "listen_port": 34443, + "users": [ + { + "name": "user@example.com", + "password": "fx-hy2-auth" + } + ], + "obfs": { + "type": "salamander", + "password": "fx-obfs-pass" + }, + "tls": { + "enabled": true, + "server_name": "hy.example.com", + "alpn": [ + "h3" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + } + } + ], + "_b0": { + "disposition": "direct", + "notes": "finalmask.udp[salamander] → hysteria2 obfs 直映(现行订阅链接已带 obfs/obfs-password 参数,服务端必须同值否则握手失败——见 expected-link.txt)。masquerade 未启用(type 空)省略。hysteriaSettings.udpIdleTimeout 在 sing-box hysteria2 无对应字段(QUIC 层自理),见规范 §4.2 小损项,随 Q1 一并确认。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/hysteria2-salamander/input.json b/internal/singbox/testdata/golden/hysteria2-salamander/input.json new file mode 100644 index 0000000..b9a081d --- /dev/null +++ b/internal/singbox/testdata/golden/hysteria2-salamander/input.json @@ -0,0 +1,100 @@ +{ + "case": "hysteria2-salamander", + "title": "Hysteria2 + finalmask salamander obfs", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts", + "internal/sub/service.go genHysteriaLink 的 salamander 桥" + ], + "disposition": "direct", + "risk": null, + "notes": "关键桥:现订阅代码已把 finalmask.udp[salamander] 翻成 hysteria2 URI 的 obfs 参数;sing-box hysteria2 原生 obfs{type:salamander,password} 直映——这是 finalmask 里唯一有直映路径的子集。masquerade 未启用(空)。", + "inbound": { + "id": 120, + "listen": "203.0.113.10", + "port": 34443, + "protocol": "hysteria", + "remark": "fx-hy2", + "tag": "inbound-34443", + "enable": true, + "settings": { + "version": 2, + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "auth": "fx-hy2-auth" + } + ] + }, + "streamSettings": { + "network": "hysteria", + "security": "tls", + "hysteriaSettings": { + "version": 2, + "udpIdleTimeout": 60, + "masquerade": { + "type": "", + "dir": "", + "url": "", + "rewriteHost": false, + "statusCode": 0 + } + }, + "tlsSettings": { + "serverName": "hy.example.com", + "alpn": [ + "h3" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + }, + "finalmask": { + "udp": [ + { + "type": "salamander", + "settings": { + "password": "fx-obfs-pass" + } + } + ] + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "auth": "fx-hy2-auth" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/mixed-auth/expected-link.txt b/internal/singbox/testdata/golden/mixed-auth/expected-link.txt new file mode 100644 index 0000000..d045304 --- /dev/null +++ b/internal/singbox/testdata/golden/mixed-auth/expected-link.txt @@ -0,0 +1 @@ +socks://user%40example.com:fx-mixed-pass@203.0.113.10:31443#fx-mixed-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/mixed-auth/expected-singbox.json b/internal/singbox/testdata/golden/mixed-auth/expected-singbox.json new file mode 100644 index 0000000..ddcb467 --- /dev/null +++ b/internal/singbox/testdata/golden/mixed-auth/expected-singbox.json @@ -0,0 +1,24 @@ +{ + "inbounds": [ + { + "type": "mixed", + "tag": "inbound-31443", + "listen": "203.0.113.10", + "listen_port": 31443, + "users": [ + { + "username": "fxacct", + "password": "fxacctpass" + }, + { + "username": "user@example.com", + "password": "fx-mixed-pass" + } + ] + } + ], + "_b0": { + "disposition": "direct", + "notes": "顺序沿现生成语义:settings 固定 accounts 居首、身份层客户随后。mixed 的 udp 由协议原生 UDP Associate 承载(settings.udp=true 已含);settings.ip(UDP 回源地址)无直对应,本样本 127.0.0.1 为默认值、实际无损,规范 §3.5 标注待 Q1 确认。流量归因键:sing-box 以 username 记 user 计数(B3 复核)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/mixed-auth/input.json b/internal/singbox/testdata/golden/mixed-auth/input.json new file mode 100644 index 0000000..63030e9 --- /dev/null +++ b/internal/singbox/testdata/golden/mixed-auth/input.json @@ -0,0 +1,57 @@ +{ + "case": "mixed-auth", + "title": "Mixed / 密码账号(settings accounts + 身份层客户)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "现链路:身份层客户在生成时折成 accounts(落地固定账号居首)。sing-box mixed 用 users[{username,password}],username=email。streamSettings 存量为空串(备份库实证),本样本不带 streamSettings。", + "inbound": { + "id": 117, + "listen": "203.0.113.10", + "port": 31443, + "protocol": "mixed", + "remark": "fx-mixed", + "tag": "inbound-31443", + "enable": true, + "settings": { + "auth": "password", + "accounts": [ + { + "user": "fxacct", + "pass": "fxacctpass" + } + ], + "udp": true, + "ip": "127.0.0.1", + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-mixed-pass" + } + ] + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-mixed-pass" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/mtproto/expected-link.txt b/internal/singbox/testdata/golden/mtproto/expected-link.txt new file mode 100644 index 0000000..30163ee --- /dev/null +++ b/internal/singbox/testdata/golden/mtproto/expected-link.txt @@ -0,0 +1 @@ +tg://proxy?port=37443&secret=ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d&server=203.0.113.10 \ No newline at end of file diff --git a/internal/singbox/testdata/golden/mtproto/expected-singbox.json b/internal/singbox/testdata/golden/mtproto/expected-singbox.json new file mode 100644 index 0000000..3e6ffc8 --- /dev/null +++ b/internal/singbox/testdata/golden/mtproto/expected-singbox.json @@ -0,0 +1,7 @@ +{ + "inbounds": [], + "_b0": { + "disposition": "external-runtime", + "notes": "不进核心(现状即被生成器跳过,internal/mtproto 独立 manager 承载)。新模型义务仅为:settings{fakeTlsDomain, clients[{secret,...}]} 原样透传,订阅 tg:// 链接不变(见 expected-link.txt)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/mtproto/input.json b/internal/singbox/testdata/golden/mtproto/input.json new file mode 100644 index 0000000..3317ee5 --- /dev/null +++ b/internal/singbox/testdata/golden/mtproto/input.json @@ -0,0 +1,50 @@ +{ + "case": "mtproto", + "title": "MTProto(外部进程承载,不进核心)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts", + "internal/sub/service_mtproto_test.go" + ], + "disposition": "external", + "risk": null, + "notes": "现状即被生成器跳过;新模型只要求 settings/clients 数据原样透传(不变量 §2.4-5 secret)。expected-singbox 为空入站 + 透传标记。", + "inbound": { + "id": 125, + "listen": "203.0.113.10", + "port": 37443, + "protocol": "mtproto", + "remark": "fx-mtproto", + "tag": "inbound-37443", + "enable": true, + "settings": { + "fakeTlsDomain": "www.cloudflare.com", + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "secret": "ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d" + } + ] + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "secret": "ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-2022/expected-link.txt b/internal/singbox/testdata/golden/shadowsocks-2022/expected-link.txt new file mode 100644 index 0000000..3627542 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-2022/expected-link.txt @@ -0,0 +1 @@ +ss://2022-blake3-aes-128-gcm:MDEyMzQ1Njc4OWFiY2RlZg%3D%3D:Y2xpZW50LXBhc3MtMDAwMQ%3D%3D@203.0.113.10:28443?type=tcp#fx-ss2022-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-2022/expected-singbox.json b/internal/singbox/testdata/golden/shadowsocks-2022/expected-singbox.json new file mode 100644 index 0000000..016735d --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-2022/expected-singbox.json @@ -0,0 +1,26 @@ +{ + "inbounds": [ + { + "type": "shadowsocks", + "tag": "inbound-28443", + "listen": "203.0.113.10", + "listen_port": 28443, + "method": "2022-blake3-aes-128-gcm", + "password": "MDEyMzQ1Njc4OWFiY2RlZg==", + "users": [ + { + "name": "user@example.com", + "password": "Y2xpZW50LXBhc3MtMDAwMQ==" + }, + { + "name": "user2@example.com", + "password": "Y2xpZW50LXBhc3MtMDAwMg==" + } + ] + } + ], + "_b0": { + "disposition": "direct", + "notes": "network=tcp,udp 为 sing-box ss 默认监听面,省略不写。ivCheck 无对应(sing-box 恒校验),本样本 false→恒校验属行为收紧,规范 §3.4 标注、处置随 Q1 一并确认。NexusGate 已实证 2022 组合密码语义两侧一致。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-2022/input.json b/internal/singbox/testdata/golden/shadowsocks-2022/input.json new file mode 100644 index 0000000..c77f6cb --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-2022/input.json @@ -0,0 +1,88 @@ +{ + "case": "shadowsocks-2022", + "title": "Shadowsocks 2022-blake3-aes-128-gcm(多用户)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts", + "~/workspace/NexusGate 生产实证(组合密码语义)" + ], + "disposition": "direct", + "risk": null, + "notes": "服务端密码:客户密码组合式两侧语义一致(不变量 §2.4-4)。主客户为 user@example.com,user2 仅出现在 settings.clients 与 expected sing-box users。", + "inbound": { + "id": 114, + "listen": "203.0.113.10", + "port": 28443, + "protocol": "shadowsocks", + "remark": "fx-ss2022", + "tag": "inbound-28443", + "enable": true, + "settings": { + "method": "2022-blake3-aes-128-gcm", + "password": "MDEyMzQ1Njc4OWFiY2RlZg==", + "network": "tcp,udp", + "ivCheck": false, + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "Y2xpZW50LXBhc3MtMDAwMQ==" + }, + { + "email": "user2@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "Y2xpZW50LXBhc3MtMDAwMg==" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "none", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "Y2xpZW50LXBhc3MtMDAwMQ==" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-legacy/expected-link.txt b/internal/singbox/testdata/golden/shadowsocks-legacy/expected-link.txt new file mode 100644 index 0000000..3086664 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-legacy/expected-link.txt @@ -0,0 +1 @@ +ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpmeC1zcy1sZWdhY3ktY2xpZW50@203.0.113.10:29443?type=tcp#fx-ss-legacy-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-legacy/expected-singbox.json b/internal/singbox/testdata/golden/shadowsocks-legacy/expected-singbox.json new file mode 100644 index 0000000..ba79518 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-legacy/expected-singbox.json @@ -0,0 +1,22 @@ +{ + "inbounds": [ + { + "type": "shadowsocks", + "tag": "inbound-29443", + "listen": "203.0.113.10", + "listen_port": 29443, + "method": "chacha20-ietf-poly1305", + "password": "fx-ss-inbound-pass", + "users": [ + { + "name": "user@example.com", + "password": "fx-ss-legacy-client" + } + ] + } + ], + "_b0": { + "disposition": "direct", + "notes": "legacy 方法 sing-box 仍支持;user 密码语义为逐用户独立密码(不做 2022 式组合),与 xray 客户行为一致,B3 以真客户端复核。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-legacy/input.json b/internal/singbox/testdata/golden/shadowsocks-legacy/input.json new file mode 100644 index 0000000..4a1d4a7 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-legacy/input.json @@ -0,0 +1,74 @@ +{ + "case": "shadowsocks-legacy", + "title": "Shadowsocks legacy chacha20-ietf-poly1305", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "legacy 单密码语义:客户密码即连接密码。sing-box 仍支持该 cipher。", + "inbound": { + "id": 115, + "listen": "203.0.113.10", + "port": 29443, + "protocol": "shadowsocks", + "remark": "fx-ss-legacy", + "tag": "inbound-29443", + "enable": true, + "settings": { + "method": "chacha20-ietf-poly1305", + "password": "fx-ss-inbound-pass", + "network": "tcp,udp", + "ivCheck": false, + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "fx-ss-legacy-client" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "none", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "fx-ss-legacy-client" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-link.txt b/internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-link.txt new file mode 100644 index 0000000..2bb6622 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-link.txt @@ -0,0 +1 @@ +ss://YWVzLTI1Ni1jZmI6Zngtc3MtY2ZiLWNsaWVudA@203.0.113.10:30443?type=tcp#fx-ss-cfb-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-singbox.json b/internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-singbox.json new file mode 100644 index 0000000..b942a40 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-removed-cipher/expected-singbox.json @@ -0,0 +1,17 @@ +{ + "inbounds": [], + "_b0": { + "disposition": "blocked-pending-q1", + "risk": "N6", + "unmapped": [ + { + "concept": "shadowsocks method=aes-256-cfb", + "input": { + "method": "aes-256-cfb" + }, + "reason": "sing-box 已移除 cfb/ctr 类 cipher,无承载;存量按 method 分组计数(census Q06)后随 Q1 报用户。", + "pending": "Q1" + } + ] + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/shadowsocks-removed-cipher/input.json b/internal/singbox/testdata/golden/shadowsocks-removed-cipher/input.json new file mode 100644 index 0000000..a13f483 --- /dev/null +++ b/internal/singbox/testdata/golden/shadowsocks-removed-cipher/input.json @@ -0,0 +1,74 @@ +{ + "case": "shadowsocks-removed-cipher", + "title": "Shadowsocks aes-256-cfb(sing-box 已移除)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "blocked-pending-q1", + "risk": "N6", + "notes": "cfb/ctr 类 cipher 在 sing-box 已移除,无承载。只登记,等 Q1(与 N6 按 method 分组计数一起报)。", + "inbound": { + "id": 116, + "listen": "203.0.113.10", + "port": 30443, + "protocol": "shadowsocks", + "remark": "fx-ss-cfb", + "tag": "inbound-30443", + "enable": true, + "settings": { + "method": "aes-256-cfb", + "password": "fx-ss-cfb-pass", + "network": "tcp,udp", + "ivCheck": false, + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "fx-ss-cfb-client" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "none", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "method": "", + "password": "fx-ss-cfb-client" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/socks-auth/expected-link.txt b/internal/singbox/testdata/golden/socks-auth/expected-link.txt new file mode 100644 index 0000000..9a2ddd3 --- /dev/null +++ b/internal/singbox/testdata/golden/socks-auth/expected-link.txt @@ -0,0 +1 @@ +socks://user%40example.com:fx-socks-pass@203.0.113.10:32443#fx-socks-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/socks-auth/expected-singbox.json b/internal/singbox/testdata/golden/socks-auth/expected-singbox.json new file mode 100644 index 0000000..794b7a3 --- /dev/null +++ b/internal/singbox/testdata/golden/socks-auth/expected-singbox.json @@ -0,0 +1,24 @@ +{ + "inbounds": [ + { + "type": "socks", + "tag": "inbound-32443", + "listen": "203.0.113.10", + "listen_port": 32443, + "users": [ + { + "username": "fxsacct", + "password": "fxsacctpass" + }, + { + "username": "user@example.com", + "password": "fx-socks-pass" + } + ] + } + ], + "_b0": { + "disposition": "direct", + "notes": "socks 与 mixed 在新模型分开成两个 type(§4.1);users 字段为 username/password(NexusGate 服务端实证)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/socks-auth/input.json b/internal/singbox/testdata/golden/socks-auth/input.json new file mode 100644 index 0000000..42bc79c --- /dev/null +++ b/internal/singbox/testdata/golden/socks-auth/input.json @@ -0,0 +1,57 @@ +{ + "case": "socks-auth", + "title": "SOCKS(传统节点同步流入形状,无原生表单)", + "source": [ + "internal/sub/socks_link_test.go" + ], + "disposition": "direct", + "risk": null, + "notes": "sing-box socks 入站 users 字段为 username/password(NexusGate 服务端实证),与 mixed 在新模型分开成两个 type。", + "inbound": { + "id": 118, + "listen": "203.0.113.10", + "port": 32443, + "protocol": "socks", + "remark": "fx-socks", + "tag": "inbound-32443", + "enable": true, + "settings": { + "auth": "password", + "accounts": [ + { + "user": "fxsacct", + "pass": "fxsacctpass" + } + ], + "udp": true, + "ip": "127.0.0.1", + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-socks-pass" + } + ] + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-socks-pass" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-link.txt b/internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-link.txt new file mode 100644 index 0000000..35d2816 --- /dev/null +++ b/internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-link.txt @@ -0,0 +1 @@ +trojan://fx-trojan-pass@203.0.113.10:27443?alpn=h2%2Chttp%2F1.1&fp=chrome&security=tls&sni=tjf.example.com&type=tcp#fx-trojan-flow-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-singbox.json b/internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-singbox.json new file mode 100644 index 0000000..932ed40 --- /dev/null +++ b/internal/singbox/testdata/golden/trojan-tcp-tls-flow/expected-singbox.json @@ -0,0 +1,39 @@ +{ + "inbounds": [ + { + "type": "trojan", + "tag": "inbound-27443", + "listen": "203.0.113.10", + "listen_port": 27443, + "users": [ + { + "name": "user@example.com", + "password": "fx-trojan-pass" + } + ], + "tls": { + "enabled": true, + "server_name": "tjf.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "partial-pending-q1", + "risk": "N5", + "unmapped": [ + { + "concept": "trojan client flow", + "input": "xtls-rprx-vision", + "reason": "sing-box v1.13.21 trojan 入站对 flow 的支持度未实证;B3 真机验前不写入 users(写了可能被核心拒绝或静默忽略),验明后回改本基线。", + "pending": "Q1/B3" + } + ] + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/trojan-tcp-tls-flow/input.json b/internal/singbox/testdata/golden/trojan-tcp-tls-flow/input.json new file mode 100644 index 0000000..67d3d31 --- /dev/null +++ b/internal/singbox/testdata/golden/trojan-tcp-tls-flow/input.json @@ -0,0 +1,87 @@ +{ + "case": "trojan-tcp-tls-flow", + "title": "Trojan / TCP / TLS + xtls flow(支持度待验)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "partial-pending-q1", + "risk": "N5", + "notes": "密码直映;flow 在 sing-box trojan 入站的支持度未验(v1.13.21),登记 _b0.unmapped 待 B3 真机验,不预删。", + "inbound": { + "id": 113, + "listen": "203.0.113.10", + "port": 27443, + "protocol": "trojan", + "remark": "fx-trojan-flow", + "tag": "inbound-27443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-trojan-pass", + "flow": "xtls-rprx-vision" + } + ], + "fallbacks": [] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + }, + "tlsSettings": { + "serverName": "tjf.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-trojan-pass", + "flow": "xtls-rprx-vision" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/trojan-tcp-tls/expected-link.txt b/internal/singbox/testdata/golden/trojan-tcp-tls/expected-link.txt new file mode 100644 index 0000000..373ab51 --- /dev/null +++ b/internal/singbox/testdata/golden/trojan-tcp-tls/expected-link.txt @@ -0,0 +1 @@ +trojan://fx-trojan-pass@203.0.113.10:26443?alpn=h2%2Chttp%2F1.1&fp=chrome&security=tls&sni=tj.example.com&type=tcp#fx-trojan-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/trojan-tcp-tls/expected-singbox.json b/internal/singbox/testdata/golden/trojan-tcp-tls/expected-singbox.json new file mode 100644 index 0000000..9037769 --- /dev/null +++ b/internal/singbox/testdata/golden/trojan-tcp-tls/expected-singbox.json @@ -0,0 +1,30 @@ +{ + "inbounds": [ + { + "type": "trojan", + "tag": "inbound-26443", + "listen": "203.0.113.10", + "listen_port": 26443, + "users": [ + { + "name": "user@example.com", + "password": "fx-trojan-pass" + } + ], + "tls": { + "enabled": true, + "server_name": "tj.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/trojan-tcp-tls/input.json b/internal/singbox/testdata/golden/trojan-tcp-tls/input.json new file mode 100644 index 0000000..320dca8 --- /dev/null +++ b/internal/singbox/testdata/golden/trojan-tcp-tls/input.json @@ -0,0 +1,85 @@ +{ + "case": "trojan-tcp-tls", + "title": "Trojan / TCP / TLS", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "", + "inbound": { + "id": 112, + "listen": "203.0.113.10", + "port": 26443, + "protocol": "trojan", + "remark": "fx-trojan", + "tag": "inbound-26443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-trojan-pass" + } + ], + "fallbacks": [] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + }, + "tlsSettings": { + "serverName": "tj.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "password": "fx-trojan-pass" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/tun/expected-link.txt b/internal/singbox/testdata/golden/tun/expected-link.txt new file mode 100644 index 0000000..e69de29 diff --git a/internal/singbox/testdata/golden/tun/expected-singbox.json b/internal/singbox/testdata/golden/tun/expected-singbox.json new file mode 100644 index 0000000..56d7ca5 --- /dev/null +++ b/internal/singbox/testdata/golden/tun/expected-singbox.json @@ -0,0 +1,39 @@ +{ + "inbounds": [ + { + "type": "tun", + "tag": "inbound-0", + "interface_name": "xray0", + "address": [ + "172.19.0.1/28" + ], + "mtu": 1500, + "auto_route": true, + "route_address": [ + "0.0.0.0/0" + ], + "stack": "system" + } + ], + "_b0": { + "disposition": "direct", + "risk": "Q5", + "unmapped": [ + { + "concept": "settings.dns", + "input": [ + "1.1.1.1" + ], + "reason": "tun 入站本体无 dns 字段,由 sing-box 独立 dns 配置承载——属基础配置面而非入站面,B2 定落点,此处登记防丢。", + "pending": "B2" + }, + { + "concept": "settings.userLevel", + "input": 0, + "reason": "xray policy level 概念,sing-box 无对应;本样本 0 为默认,实际无损。", + "pending": "Q1" + } + ], + "notes": "gateway[] → address[];autoSystemRoutingTable → route_address + auto_route;autoOutboundsInterface=auto 省略(sing-box auto_redirect/出口自动选择为近似,字段落位 B1 定)。是否挂 agent 承载属 Q5。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/tun/input.json b/internal/singbox/testdata/golden/tun/input.json new file mode 100644 index 0000000..ea11483 --- /dev/null +++ b/internal/singbox/testdata/golden/tun/input.json @@ -0,0 +1,46 @@ +{ + "case": "tun", + "title": "TUN(面板本地)", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": "Q5", + "notes": "sing-box 原生 tun 入站且字段更完整;本样本按现字段名映射(gateway→address 等改名在规范中逐项列)。是否挂 agent 承载属 Q5,不影响形状映射。", + "inbound": { + "id": 124, + "listen": "203.0.113.10", + "port": 0, + "protocol": "tun", + "remark": "fx-tun", + "tag": "inbound-0", + "enable": true, + "settings": { + "name": "xray0", + "mtu": 1500, + "gateway": [ + "172.19.0.1/28" + ], + "dns": [ + "1.1.1.1" + ], + "userLevel": 0, + "autoSystemRoutingTable": [ + "0.0.0.0/0" + ], + "autoOutboundsInterface": "auto" + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0 + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/tunnel-dokodemo/expected-link.txt b/internal/singbox/testdata/golden/tunnel-dokodemo/expected-link.txt new file mode 100644 index 0000000..e69de29 diff --git a/internal/singbox/testdata/golden/tunnel-dokodemo/expected-singbox.json b/internal/singbox/testdata/golden/tunnel-dokodemo/expected-singbox.json new file mode 100644 index 0000000..21df8a0 --- /dev/null +++ b/internal/singbox/testdata/golden/tunnel-dokodemo/expected-singbox.json @@ -0,0 +1,22 @@ +{ + "inbounds": [], + "_b0": { + "disposition": "blocked-pending-q1", + "risk": "N2", + "unmapped": [ + { + "concept": "tunnel(dokodemo-door) settings", + "input": { + "rewriteAddress": "192.0.2.50", + "rewritePort": 8080, + "portMap": {}, + "allowedNetwork": "tcp,udp", + "followRedirect": false + }, + "reason": "sing-box 无 dokodemo 等价物(direct 入站已弃用并在新版移除)。现为面板本地协议、不下发 agent;承载(自维护极小转发组件 / route 近似)待 Q1+Q5。", + "pending": "Q1" + } + ], + "mappableRemainder": "无。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/tunnel-dokodemo/input.json b/internal/singbox/testdata/golden/tunnel-dokodemo/input.json new file mode 100644 index 0000000..8365799 --- /dev/null +++ b/internal/singbox/testdata/golden/tunnel-dokodemo/input.json @@ -0,0 +1,38 @@ +{ + "case": "tunnel-dokodemo", + "title": "Tunnel(dokodemo-door 面板本地)", + "source": [ + "frontend/src/schemas/protocols/inbound/tunnel.ts" + ], + "disposition": "blocked-pending-q1", + "risk": "N2", + "notes": "sing-box 无稳定等价物(direct 入站已弃用并在新版移除)。现为面板本地协议、不下发 agent。承载方式(极小转发组件/route 近似)等 Q1/Q5,先登记形状。", + "inbound": { + "id": 123, + "listen": "203.0.113.10", + "port": 36443, + "protocol": "tunnel", + "remark": "fx-tunnel", + "tag": "inbound-36443", + "enable": true, + "settings": { + "rewriteAddress": "192.0.2.50", + "rewritePort": 8080, + "portMap": {}, + "allowedNetwork": "tcp,udp", + "followRedirect": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0 + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-grpc-tls/expected-link.txt b/internal/singbox/testdata/golden/vless-grpc-tls/expected-link.txt new file mode 100644 index 0000000..5d44ed5 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-grpc-tls/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:10443?alpn=h2%2Chttp%2F1.1&authority=&encryption=none&fp=chrome&security=tls&serviceName=fx-grpc&sni=grpc.example.com&type=grpc#fx-vless-grpc-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-grpc-tls/expected-singbox.json b/internal/singbox/testdata/golden/vless-grpc-tls/expected-singbox.json new file mode 100644 index 0000000..444cdc4 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-grpc-tls/expected-singbox.json @@ -0,0 +1,35 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-10443", + "listen": "203.0.113.10", + "listen_port": 10443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555" + } + ], + "transport": { + "type": "grpc", + "service_name": "fx-grpc" + }, + "tls": { + "enabled": true, + "server_name": "grpc.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct", + "notes": "grpcSettings.multiMode 在 sing-box grpc transport 无对应字段;本样本 false 与 sing-box 默认行为一致,无损。multiMode=true 的存量处置见规范(近似/无对应,待 Q1)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-grpc-tls/input.json b/internal/singbox/testdata/golden/vless-grpc-tls/input.json new file mode 100644 index 0000000..99c6272 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-grpc-tls/input.json @@ -0,0 +1,87 @@ +{ + "case": "vless-grpc-tls", + "title": "VLESS / gRPC / TLS", + "source": [ + "frontend/src/schemas/protocols/stream/grpc.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "grpc serviceName 两侧改名 service_name,multiMode 在 sing-box 无对应字段(近似:默认关)——本样本 multiMode=false,无损。", + "inbound": { + "id": 103, + "listen": "203.0.113.10", + "port": 10443, + "protocol": "vless", + "remark": "fx-vless-grpc", + "tag": "inbound-10443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "grpc", + "security": "tls", + "grpcSettings": { + "serviceName": "fx-grpc", + "multiMode": false + }, + "tlsSettings": { + "serverName": "grpc.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-link.txt b/internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-link.txt new file mode 100644 index 0000000..fdb222c --- /dev/null +++ b/internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:11443?alpn=h2%2Chttp%2F1.1&encryption=none&fp=chrome&host=hu.example.com&path=%2Ffxhu&security=tls&sni=hu.example.com&type=httpupgrade#fx-vless-httpupgrade-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-singbox.json b/internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-singbox.json new file mode 100644 index 0000000..c141c12 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-httpupgrade-tls/expected-singbox.json @@ -0,0 +1,35 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-11443", + "listen": "203.0.113.10", + "listen_port": 11443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555" + } + ], + "transport": { + "type": "httpupgrade", + "host": "hu.example.com", + "path": "/fxhu" + }, + "tls": { + "enabled": true, + "server_name": "hu.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-httpupgrade-tls/input.json b/internal/singbox/testdata/golden/vless-httpupgrade-tls/input.json new file mode 100644 index 0000000..58ffeef --- /dev/null +++ b/internal/singbox/testdata/golden/vless-httpupgrade-tls/input.json @@ -0,0 +1,87 @@ +{ + "case": "vless-httpupgrade-tls", + "title": "VLESS / HTTPUpgrade / TLS", + "source": [ + "frontend/src/schemas/protocols/stream/httpupgrade.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "", + "inbound": { + "id": 104, + "listen": "203.0.113.10", + "port": 11443, + "protocol": "vless", + "remark": "fx-vless-httpupgrade", + "tag": "inbound-11443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "httpupgrade", + "security": "tls", + "httpupgradeSettings": { + "path": "/fxhu", + "host": "hu.example.com" + }, + "tlsSettings": { + "serverName": "hu.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-finalmask/expected-link.txt b/internal/singbox/testdata/golden/vless-tcp-finalmask/expected-link.txt new file mode 100644 index 0000000..22bae4b --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-finalmask/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:21443?encryption=none&flow=xtls-rprx-vision&fm=%7B%22tcp%22%3A%5B%7B%22settings%22%3A%7B%22interval%22%3A%2210-20%22%2C%22length%22%3A%22100-200%22%2C%22packets%22%3A%22tlshello%22%7D%2C%22type%22%3A%22fragment%22%7D%5D%7D&fp=chrome&pbk=B6N8vBQgk8i3VdwbEOhstCY3StFqqFPtC9%2FAsrhtHHw%3D&security=reality&sid=ab12cd34&sni=www.amd.com&spx=%2Fd0cf3c252cd6df6&type=tcp#fx-vless-finalmask-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-finalmask/expected-singbox.json b/internal/singbox/testdata/golden/vless-tcp-finalmask/expected-singbox.json new file mode 100644 index 0000000..d5900ee --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-finalmask/expected-singbox.json @@ -0,0 +1,52 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-21443", + "listen": "203.0.113.10", + "listen_port": 21443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } + ], + "tls": { + "enabled": true, + "server_name": "www.amd.com", + "reality": { + "enabled": true, + "handshake": { + "server": "www.amd.com", + "server_port": 443 + }, + "private_key": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "short_id": [ + "ab12cd34" + ] + } + }, + "sniff": true + } + ], + "_b0": { + "disposition": "partial-pending-q1", + "risk": "N9", + "unmapped": [ + { + "concept": "streamSettings.finalmask.tcp[fragment]", + "input": { + "type": "fragment", + "settings": { + "packets": "tlshello", + "length": "100-200", + "interval": "10-20" + } + }, + "reason": "sing-box 无 finalmask 机制。且现行分享链接携带 fm= 参数(见 expected-link.txt),删服务端 mask 会使存量客户端链接失效——属用户可见变化,待 Q1。", + "pending": "Q1" + } + ] + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-finalmask/input.json b/internal/singbox/testdata/golden/vless-tcp-finalmask/input.json new file mode 100644 index 0000000..068e8d2 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-finalmask/input.json @@ -0,0 +1,108 @@ +{ + "case": "vless-tcp-finalmask", + "title": "VLESS / TCP / Reality + finalmask(fragment)", + "source": [ + "frontend/src/schemas/protocols/stream/finalmask.ts" + ], + "disposition": "partial-pending-q1", + "risk": "N9", + "notes": "finalmask 无 sing-box 对应。主入站本体可直映;mask 登记 _b0.unmapped。注意例外:hysteria2 的 udp salamander mask 有直映路径,见 hysteria2-salamander。", + "inbound": { + "id": 107, + "listen": "203.0.113.10", + "port": 21443, + "protocol": "vless", + "remark": "fx-vless-finalmask", + "tag": "inbound-21443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "tcp", + "security": "reality", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + }, + "realitySettings": { + "show": false, + "xver": 0, + "target": "www.amd.com:443", + "serverNames": [ + "www.amd.com" + ], + "privateKey": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "minClientVer": "", + "maxClientVer": "", + "maxTimediff": 0, + "shortIds": [ + "ab12cd34" + ], + "mldsa65Seed": "", + "settings": { + "publicKey": "B6N8vBQgk8i3VdwbEOhstCY3StFqqFPtC9/AsrhtHHw=", + "fingerprint": "chrome", + "serverName": "", + "spiderX": "/", + "mldsa65Verify": "" + } + }, + "finalmask": { + "tcp": [ + { + "type": "fragment", + "settings": { + "packets": "tlshello", + "length": "100-200", + "interval": "10-20" + } + } + ] + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-link.txt b/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-link.txt new file mode 100644 index 0000000..6596039 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:20443?encryption=none&flow=xtls-rprx-vision&fp=chrome&pbk=WGmv9FBUlzLLqu1eXfmzCm2jHLDldCutWtShp2jxpns%3D&security=reality&sid=77aa10f0&sni=www.microsoft.com&spx=%2Fd0cf3c252cd6df6&type=tcp#fx-vless-fallback-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-singbox.json b/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-singbox.json new file mode 100644 index 0000000..f86ff85 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/expected-singbox.json @@ -0,0 +1,52 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-20443", + "listen": "203.0.113.10", + "listen_port": 20443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } + ], + "tls": { + "enabled": true, + "server_name": "www.microsoft.com", + "reality": { + "enabled": true, + "handshake": { + "server": "www.microsoft.com", + "server_port": 443 + }, + "private_key": "ISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0A=", + "short_id": [ + "77aa10f0" + ] + } + }, + "sniff": true + } + ], + "_b0": { + "disposition": "partial-pending-q1", + "risk": "N1", + "unmapped": [ + { + "concept": "fallbacks(inbound_fallbacks 表行 → settings.fallbacks 注入形)", + "input": { + "name": "", + "alpn": "", + "path": "", + "dest": 8080, + "xver": 0 + }, + "reason": "sing-box 无 fallback 概念;主入站本体已直映,回落关系(按 alpn/path 分流到 dest)无承载。候选承载(route 条件分流近似 / 专用小组件)待 Q1 拍板。", + "pending": "Q1" + } + ], + "notes": "迁移器须同时读 inbound_fallbacks 表与 settings 内嵌 fallbacks 两种存量形态(见规范 §4.1)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/input.json b/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/input.json new file mode 100644 index 0000000..6fa6b28 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-reality-fallbacks/input.json @@ -0,0 +1,105 @@ +{ + "case": "vless-tcp-reality-fallbacks", + "title": "VLESS / TCP / Reality + fallbacks(子入站回落)", + "source": [ + "internal/database/model/model.go InboundFallback", + "internal/web/service/xray.go BuildFallbacksJSON 注入" + ], + "disposition": "partial-pending-q1", + "risk": "N1", + "notes": "fallbacks 在本 fork 的真源是 inbound_fallbacks 表(master_id/child_id/dest/xver),生成时注入 settings.fallbacks。本样本以 settings 注入形表示一条 dest=8080 的网页回落。主入站本体可直映;回落关系无 sing-box 对应,登记在 expected 的 _b0.unmapped。", + "inbound": { + "id": 106, + "listen": "203.0.113.10", + "port": 20443, + "protocol": "vless", + "remark": "fx-vless-fallback", + "tag": "inbound-20443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [ + { + "name": "", + "alpn": "", + "path": "", + "dest": 8080, + "xver": 0 + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "reality", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + }, + "realitySettings": { + "show": false, + "xver": 0, + "target": "www.microsoft.com:443", + "serverNames": [ + "www.microsoft.com" + ], + "privateKey": "ISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0A=", + "minClientVer": "", + "maxClientVer": "", + "maxTimediff": 0, + "shortIds": [ + "77aa10f0" + ], + "mldsa65Seed": "", + "settings": { + "publicKey": "WGmv9FBUlzLLqu1eXfmzCm2jHLDldCutWtShp2jxpns=", + "fingerprint": "chrome", + "serverName": "", + "spiderX": "/", + "mldsa65Verify": "" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-reality/expected-link.txt b/internal/singbox/testdata/golden/vless-tcp-reality/expected-link.txt new file mode 100644 index 0000000..1082901 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-reality/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:443?encryption=none&flow=xtls-rprx-vision&fp=chrome&pbk=B6N8vBQgk8i3VdwbEOhstCY3StFqqFPtC9%2FAsrhtHHw%3D&security=reality&sid=ab12cd34&sni=www.amd.com&spx=%2Fd0cf3c252cd6df6&type=tcp#fx-vless-reality-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-reality/expected-singbox.json b/internal/singbox/testdata/golden/vless-tcp-reality/expected-singbox.json new file mode 100644 index 0000000..18c12d7 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-reality/expected-singbox.json @@ -0,0 +1,37 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-443", + "listen": "203.0.113.10", + "listen_port": 443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } + ], + "tls": { + "enabled": true, + "server_name": "www.amd.com", + "reality": { + "enabled": true, + "handshake": { + "server": "www.amd.com", + "server_port": 443 + }, + "private_key": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "short_id": [ + "ab12cd34" + ] + } + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct", + "notes": "sniff destOverride[http,tls,quic] 由 route 层 sniff 动作近似承载(见规范 §5),入站侧只落 sniff:true。reality.maxTimediff=0 省略(sing-box max_time_difference 默认)。nested settings{publicKey,fingerprint,spiderX} 为客户端侧参数、不进服务端 tls,归订阅元数据。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-tcp-reality/input.json b/internal/singbox/testdata/golden/vless-tcp-reality/input.json new file mode 100644 index 0000000..6b7ef10 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-tcp-reality/input.json @@ -0,0 +1,97 @@ +{ + "case": "vless-tcp-reality", + "title": "VLESS / TCP / Reality / Vision flow", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts", + "internal/sub/characterization_test.go (C4 shape)" + ], + "disposition": "direct", + "risk": null, + "notes": "现网主力形状(备份库 12 条 vless 全部 tcp+reality)。flow 归一规则:xtls-rprx-vision-udp443 在生成侧归一为 xtls-rprx-vision,sing-box 同名保留。", + "inbound": { + "id": 101, + "listen": "203.0.113.10", + "port": 443, + "protocol": "vless", + "remark": "fx-vless-reality", + "tag": "inbound-443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "tcp", + "security": "reality", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + }, + "realitySettings": { + "show": false, + "xver": 0, + "target": "www.amd.com:443", + "serverNames": [ + "www.amd.com" + ], + "privateKey": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "minClientVer": "", + "maxClientVer": "", + "maxTimediff": 0, + "shortIds": [ + "ab12cd34" + ], + "mldsa65Seed": "", + "settings": { + "publicKey": "B6N8vBQgk8i3VdwbEOhstCY3StFqqFPtC9/AsrhtHHw=", + "fingerprint": "chrome", + "serverName": "", + "spiderX": "/", + "mldsa65Verify": "" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "xtls-rprx-vision" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-vlessenc/expected-link.txt b/internal/singbox/testdata/golden/vless-vlessenc/expected-link.txt new file mode 100644 index 0000000..0732d5c --- /dev/null +++ b/internal/singbox/testdata/golden/vless-vlessenc/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:22443?encryption=none&security=none&type=tcp#fx-vless-vlessenc-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-vlessenc/expected-singbox.json b/internal/singbox/testdata/golden/vless-vlessenc/expected-singbox.json new file mode 100644 index 0000000..3b4ae4d --- /dev/null +++ b/internal/singbox/testdata/golden/vless-vlessenc/expected-singbox.json @@ -0,0 +1,30 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-22443", + "listen": "203.0.113.10", + "listen_port": 22443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555" + } + ], + "sniff": true + } + ], + "_b0": { + "disposition": "partial-pending-q1", + "risk": "N3", + "unmapped": [ + { + "concept": "settings.decryption(vlessenc / ML-KEM 加密串)", + "input": "mlkem768x25519plus.native.0rtt.*(假串)", + "reason": "sing-box vless 无对应加密层(decryption 恒 none 语义);带 vlessenc 的入站迁过去即明文 vless,行为与安全性变化,待 Q1。", + "pending": "Q1" + } + ], + "notes": "users 本体已映;阻塞点仅 decryption。client 侧 reverse 字段同属 N4,见规范 §3.1。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-vlessenc/input.json b/internal/singbox/testdata/golden/vless-vlessenc/input.json new file mode 100644 index 0000000..19462a7 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-vlessenc/input.json @@ -0,0 +1,73 @@ +{ + "case": "vless-vlessenc", + "title": "VLESS / TCP / vlessenc(decryption=ML-KEM 串)", + "source": [ + "frontend/src/lib/xray/vless-encryption.ts" + ], + "disposition": "partial-pending-q1", + "risk": "N3", + "notes": "decryption 为 vlessenc 串(此处假串)时 sing-box 无对应加密层。users 本体可映;decryption 登记 _b0.unmapped,等 Q1。", + "inbound": { + "id": 108, + "listen": "203.0.113.10", + "port": 22443, + "protocol": "vless", + "remark": "fx-vless-vlessenc", + "tag": "inbound-22443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } + ], + "decryption": "mlkem768x25519plus.native.0rtt.FAKEFAKEFAKEFAKE", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "tcp", + "security": "none", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "none" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-ws-tls/expected-link.txt b/internal/singbox/testdata/golden/vless-ws-tls/expected-link.txt new file mode 100644 index 0000000..d3ad094 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-ws-tls/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:8443?alpn=h2%2Chttp%2F1.1&encryption=none&fp=chrome&host=cdn.example.com&path=%2Ffxws&security=tls&sni=cdn.example.com&type=ws#fx-vless-ws-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-ws-tls/expected-singbox.json b/internal/singbox/testdata/golden/vless-ws-tls/expected-singbox.json new file mode 100644 index 0000000..adf5901 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-ws-tls/expected-singbox.json @@ -0,0 +1,38 @@ +{ + "inbounds": [ + { + "type": "vless", + "tag": "inbound-8443", + "listen": "203.0.113.10", + "listen_port": 8443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555" + } + ], + "transport": { + "type": "ws", + "path": "/fxws", + "headers": { + "Host": "cdn.example.com" + } + }, + "tls": { + "enabled": true, + "server_name": "cdn.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct", + "notes": "tlsSettings.settings.fingerprint 为客户端侧参数,不进服务端 tls。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-ws-tls/input.json b/internal/singbox/testdata/golden/vless-ws-tls/input.json new file mode 100644 index 0000000..8992412 --- /dev/null +++ b/internal/singbox/testdata/golden/vless-ws-tls/input.json @@ -0,0 +1,91 @@ +{ + "case": "vless-ws-tls", + "title": "VLESS / WS / TLS", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts", + "frontend/src/schemas/protocols/stream/ws.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "ws headers.Host 是订阅与服务端共同参数,逐项对齐不变量 §2.4-3。", + "inbound": { + "id": 102, + "listen": "203.0.113.10", + "port": 8443, + "protocol": "vless", + "remark": "fx-vless-ws", + "tag": "inbound-8443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "wsSettings": { + "acceptProxyProtocol": false, + "path": "/fxws", + "headers": { + "Host": "cdn.example.com" + } + }, + "tlsSettings": { + "serverName": "cdn.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-xhttp-tls/expected-link.txt b/internal/singbox/testdata/golden/vless-xhttp-tls/expected-link.txt new file mode 100644 index 0000000..ff5956d --- /dev/null +++ b/internal/singbox/testdata/golden/vless-xhttp-tls/expected-link.txt @@ -0,0 +1 @@ +vless://11111111-2222-4333-8444-555555555555@203.0.113.10:12443?alpn=h2%2Chttp%2F1.1&encryption=none&extra=%7B%22mode%22%3A%22auto%22%2C%22xmux%22%3A%7B%22cMaxReuseTimes%22%3A0%2C%22hKeepAlivePeriod%22%3A0%2C%22hMaxRequestTimes%22%3A%22600-900%22%2C%22hMaxReusableSecs%22%3A%221800-3000%22%2C%22maxConcurrency%22%3A%2216-32%22%2C%22maxConnections%22%3A0%7D%7D&fp=chrome&host=xh.example.com&mode=auto&path=%2Ffxxh&security=tls&sni=xh.example.com&type=xhttp#fx-vless-xhttp-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-xhttp-tls/expected-singbox.json b/internal/singbox/testdata/golden/vless-xhttp-tls/expected-singbox.json new file mode 100644 index 0000000..314e38e --- /dev/null +++ b/internal/singbox/testdata/golden/vless-xhttp-tls/expected-singbox.json @@ -0,0 +1,23 @@ +{ + "inbounds": [], + "_b0": { + "disposition": "blocked-pending-q1", + "risk": "N8", + "unmapped": [ + { + "concept": "streamSettings.network=xhttp + xhttpSettings", + "input": { + "path": "/fxxh", + "host": "xh.example.com", + "mode": "auto", + "xmux": { + "maxConcurrency": "16-32" + } + }, + "reason": "sing-box v1.13.21 无 XHTTP 传输,无任何等价承载;强行落 ws/httpupgrade 会改变订阅链接(违背无感知不变量)。处置(近似映射/专用承载)待 Q1 拍板后补基线。", + "pending": "Q1" + } + ], + "mappableRemainder": "users、tls、sniff 与 vless-ws-tls 同构;阻塞点仅在 transport。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vless-xhttp-tls/input.json b/internal/singbox/testdata/golden/vless-xhttp-tls/input.json new file mode 100644 index 0000000..18c381b --- /dev/null +++ b/internal/singbox/testdata/golden/vless-xhttp-tls/input.json @@ -0,0 +1,96 @@ +{ + "case": "vless-xhttp-tls", + "title": "VLESS / XHTTP / TLS(无对应)", + "source": [ + "frontend/src/schemas/protocols/stream/xhttp.ts" + ], + "disposition": "blocked-pending-q1", + "risk": "N8", + "notes": "sing-box 无 XHTTP。expected-singbox.json 不给伪造输出,只登记未映射事实,等 Q1 拍板近似承载(ws/httpupgrade)后再定基线。", + "inbound": { + "id": 105, + "listen": "203.0.113.10", + "port": 12443, + "protocol": "vless", + "remark": "fx-vless-xhttp", + "tag": "inbound-12443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } + ], + "decryption": "none", + "encryption": "none", + "fallbacks": [] + }, + "streamSettings": { + "network": "xhttp", + "security": "tls", + "xhttpSettings": { + "path": "/fxxh", + "host": "xh.example.com", + "mode": "auto", + "xmux": { + "maxConcurrency": "16-32", + "maxConnections": 0, + "cMaxReuseTimes": 0, + "hMaxRequestTimes": "600-900", + "hMaxReusableSecs": "1800-3000", + "hKeepAlivePeriod": 0 + } + }, + "tlsSettings": { + "serverName": "xh.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "flow": "" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-kcp/expected-link.txt b/internal/singbox/testdata/golden/vmess-kcp/expected-link.txt new file mode 100644 index 0000000..7826696 --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-kcp/expected-link.txt @@ -0,0 +1 @@ +vmess://ewogICJhZGQiOiAiMjAzLjAuMTEzLjEwIiwKICAiaWQiOiAiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwKICAibXR1IjogMTM1MCwKICAibmV0IjogImtjcCIsCiAgInBhdGgiOiAiZngta2NwLXNlZWQiLAogICJwb3J0IjogMjU0NDMsCiAgInBzIjogImZ4LXZtZXNzLWtjcC11c2VyQGV4YW1wbGUuY29tIiwKICAic2N5IjogImF1dG8iLAogICJ0bHMiOiAibm9uZSIsCiAgInR0aSI6IDIwLAogICJ0eXBlIjogInNydHAiLAogICJ2IjogIjIiCn0= \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-kcp/expected-singbox.json b/internal/singbox/testdata/golden/vmess-kcp/expected-singbox.json new file mode 100644 index 0000000..6084aaa --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-kcp/expected-singbox.json @@ -0,0 +1,25 @@ +{ + "inbounds": [], + "_b0": { + "disposition": "blocked-pending-q1", + "risk": "N8", + "unmapped": [ + { + "concept": "streamSettings.network=kcp + kcpSettings", + "input": { + "mtu": 1350, + "tti": 20, + "uplinkCapacity": 5, + "downlinkCapacity": 20, + "header": { + "type": "srtp" + }, + "seed": "*" + }, + "reason": "sing-box 无 mKCP 传输,无等价承载;处置同 xhttp,待 Q1。", + "pending": "Q1" + } + ], + "mappableRemainder": "users/sniff 与 vmess-ws-tls 同构;阻塞点仅在 transport。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-kcp/input.json b/internal/singbox/testdata/golden/vmess-kcp/input.json new file mode 100644 index 0000000..07ade2b --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-kcp/input.json @@ -0,0 +1,76 @@ +{ + "case": "vmess-kcp", + "title": "VMess / mKCP(无对应)", + "source": [ + "frontend/src/schemas/protocols/stream/kcp.ts" + ], + "disposition": "blocked-pending-q1", + "risk": "N8", + "notes": "sing-box 无 mKCP,同 xhttp 口径:只登记事实,等 Q1。", + "inbound": { + "id": 111, + "listen": "203.0.113.10", + "port": 25443, + "protocol": "vmess", + "remark": "fx-vmess-kcp", + "tag": "inbound-25443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "security": "auto" + } + ] + }, + "streamSettings": { + "network": "kcp", + "security": "none", + "kcpSettings": { + "mtu": 1350, + "tti": 20, + "uplinkCapacity": 5, + "downlinkCapacity": 20, + "cwndMultiplier": 1, + "maxSendingWindow": 2097152, + "header": { + "type": "srtp" + }, + "seed": "fx-kcp-seed" + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "security": "auto" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-link.txt b/internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-link.txt new file mode 100644 index 0000000..b10ebe3 --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-link.txt @@ -0,0 +1 @@ +vmess://ewogICJhZGQiOiAiMjAzLjAuMTEzLjEwIiwKICAiaG9zdCI6ICJ3d3cuZXhhbXBsZS5jb20iLAogICJpZCI6ICIxMTExMTExMS0yMjIyLTQzMzMtODQ0NC01NTU1NTU1NTU1NTUiLAogICJuZXQiOiAidGNwIiwKICAicGF0aCI6ICIvIiwKICAicG9ydCI6IDI0NDQzLAogICJwcyI6ICJmeC12bWVzcy1odHRwY2Ftby11c2VyQGV4YW1wbGUuY29tIiwKICAic2N5IjogImFlcy0xMjgtZ2NtIiwKICAidGxzIjogIm5vbmUiLAogICJ0eXBlIjogImh0dHAiLAogICJ2IjogIjIiCn0= \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-singbox.json b/internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-singbox.json new file mode 100644 index 0000000..3797e4d --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-tcp-httpheader/expected-singbox.json @@ -0,0 +1,33 @@ +{ + "inbounds": [ + { + "type": "vmess", + "tag": "inbound-24443", + "listen": "203.0.113.10", + "listen_port": 24443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555", + "security": "aes-128-gcm", + "alter_id": 0 + } + ], + "transport": { + "type": "http", + "host": [ + "www.example.com" + ], + "path": [ + "/" + ], + "method": "GET" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct", + "notes": "tcp header type=http → transport{type:http};request.headers.Host 提升为 host 数组,其余 headers 进 transport.headers(本样本无其余项)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-tcp-httpheader/input.json b/internal/singbox/testdata/golden/vmess-tcp-httpheader/input.json new file mode 100644 index 0000000..6e5be9b --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-tcp-httpheader/input.json @@ -0,0 +1,82 @@ +{ + "case": "vmess-tcp-httpheader", + "title": "VMess / TCP / HTTP 头伪装", + "source": [ + "frontend/src/schemas/protocols/stream/tcp.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "tcp header type=http → sing-box transport{type:http}(仅请求侧伪装字段改名)。", + "inbound": { + "id": 110, + "listen": "203.0.113.10", + "port": 24443, + "protocol": "vmess", + "remark": "fx-vmess-httpcamo", + "tag": "inbound-24443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "security": "aes-128-gcm" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "none", + "tcpSettings": { + "acceptProxyProtocol": false, + "header": { + "type": "http", + "request": { + "version": "1.1", + "method": "GET", + "path": [ + "/" + ], + "headers": { + "Host": [ + "www.example.com" + ] + } + } + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "security": "aes-128-gcm" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-ws-tls/expected-link.txt b/internal/singbox/testdata/golden/vmess-ws-tls/expected-link.txt new file mode 100644 index 0000000..cf08e72 --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-ws-tls/expected-link.txt @@ -0,0 +1 @@ +vmess://ewogICJhZGQiOiAiMjAzLjAuMTEzLjEwIiwKICAiYWxwbiI6ICJoMixodHRwLzEuMSIsCiAgImZwIjogImNocm9tZSIsCiAgImhvc3QiOiAidm0uZXhhbXBsZS5jb20iLAogICJpZCI6ICIxMTExMTExMS0yMjIyLTQzMzMtODQ0NC01NTU1NTU1NTU1NTUiLAogICJuZXQiOiAid3MiLAogICJwYXRoIjogIi9meHZtIiwKICAicG9ydCI6IDIzNDQzLAogICJwcyI6ICJmeC12bWVzcy13cy11c2VyQGV4YW1wbGUuY29tIiwKICAic2N5IjogImF1dG8iLAogICJzbmkiOiAidm0uZXhhbXBsZS5jb20iLAogICJ0bHMiOiAidGxzIiwKICAidHlwZSI6ICJub25lIiwKICAidiI6ICIyIgp9 \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-ws-tls/expected-singbox.json b/internal/singbox/testdata/golden/vmess-ws-tls/expected-singbox.json new file mode 100644 index 0000000..0a170a8 --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-ws-tls/expected-singbox.json @@ -0,0 +1,40 @@ +{ + "inbounds": [ + { + "type": "vmess", + "tag": "inbound-23443", + "listen": "203.0.113.10", + "listen_port": 23443, + "users": [ + { + "name": "user@example.com", + "uuid": "11111111-2222-4333-8444-555555555555", + "security": "auto", + "alter_id": 0 + } + ], + "transport": { + "type": "ws", + "path": "/fxvm", + "headers": { + "Host": "vm.example.com" + } + }, + "tls": { + "enabled": true, + "server_name": "vm.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificate_path": "/etc/x-ui/cert/fx.pem", + "key_path": "/etc/x-ui/cert/fx.key" + }, + "sniff": true + } + ], + "_b0": { + "disposition": "direct", + "notes": "alterId 在 wire 上为 alterId、sing-box 为 alter_id;security 逐字保留(不变量 §2.4-5)。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/vmess-ws-tls/input.json b/internal/singbox/testdata/golden/vmess-ws-tls/input.json new file mode 100644 index 0000000..14307f3 --- /dev/null +++ b/internal/singbox/testdata/golden/vmess-ws-tls/input.json @@ -0,0 +1,87 @@ +{ + "case": "vmess-ws-tls", + "title": "VMess / WS / TLS", + "source": [ + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": null, + "notes": "", + "inbound": { + "id": 109, + "listen": "203.0.113.10", + "port": 23443, + "protocol": "vmess", + "remark": "fx-vmess-ws", + "tag": "inbound-23443", + "enable": true, + "settings": { + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "security": "auto" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "wsSettings": { + "acceptProxyProtocol": false, + "path": "/fxvm", + "headers": { + "Host": "vm.example.com" + } + }, + "tlsSettings": { + "serverName": "vm.example.com", + "alpn": [ + "h2", + "http/1.1" + ], + "certificates": [ + { + "certificateFile": "/etc/x-ui/cert/fx.pem", + "keyFile": "/etc/x-ui/cert/fx.key" + } + ], + "settings": { + "fingerprint": "chrome" + } + } + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls", + "quic" + ], + "metadataOnly": false, + "routeOnly": false + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "id": "11111111-2222-4333-8444-555555555555", + "security": "auto" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/wireguard/expected-link.txt b/internal/singbox/testdata/golden/wireguard/expected-link.txt new file mode 100644 index 0000000..891f846 --- /dev/null +++ b/internal/singbox/testdata/golden/wireguard/expected-link.txt @@ -0,0 +1 @@ +wireguard://l5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tbY%3D@203.0.113.10:51820?address=10.0.0.2%2F32&keepalive=25&mtu=1420&publickey=VxR2nRFr92Q2rnS8eT0sMK0ZA8WaxSc4BcfiaYtBDDY%3D#fx-wireguard-user%40example.com \ No newline at end of file diff --git a/internal/singbox/testdata/golden/wireguard/expected-singbox.json b/internal/singbox/testdata/golden/wireguard/expected-singbox.json new file mode 100644 index 0000000..74891b9 --- /dev/null +++ b/internal/singbox/testdata/golden/wireguard/expected-singbox.json @@ -0,0 +1,29 @@ +{ + "inbounds": [], + "endpoints": [ + { + "type": "wireguard", + "tag": "inbound-51820", + "listen_port": 51820, + "address": [ + "10.0.0.1/24" + ], + "private_key": "ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gIGCg4Q=", + "mtu": 1420, + "peers": [ + { + "public_key": "FThkYS9cXUf2hNZomaneT3wwtfdKjr1plkIhK84A2Tg=", + "allowed_ips": [ + "10.0.0.2/32" + ], + "persistent_keepalive_interval": 25 + } + ] + } + ], + "_b0": { + "disposition": "direct", + "risk": "N7", + "notes": "挂载位置变化:inbounds[] → endpoints[],且须 route 规则把流量引入(B2 基础配置处理)。服务端 address 按现后端推导口径取 subnet 首地址(10.0.0.0/24 → 10.0.0.1/24),B1 与现生成链(WireguardPeerFromClient/服务端地址推导)逐项对。noKernelTun=false → 系统 tun 模式;内核/系统模式真机差异待 B3(N7)。peers 的 preshared_key 本样本为空省略。" + } +} \ No newline at end of file diff --git a/internal/singbox/testdata/golden/wireguard/input.json b/internal/singbox/testdata/golden/wireguard/input.json new file mode 100644 index 0000000..4347f5c --- /dev/null +++ b/internal/singbox/testdata/golden/wireguard/input.json @@ -0,0 +1,65 @@ +{ + "case": "wireguard", + "title": "WireGuard(endpoint 化)", + "source": [ + "internal/sub/service_wireguard_test.go", + "frontend/src/lib/xray/inbound-defaults.ts" + ], + "disposition": "direct", + "risk": "N7", + "notes": "新模型不是 inbound 而是 endpoints[] + route 引用。客户端 .conf 与 wireguard:// 链均属不变量 §2.4-5。noKernelTun/系统模式差异待 B3 真机验(N7)。", + "inbound": { + "id": 122, + "listen": "203.0.113.10", + "port": 51820, + "protocol": "wireguard", + "remark": "fx-wireguard", + "tag": "inbound-51820", + "enable": true, + "settings": { + "mtu": 1420, + "secretKey": "ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gIGCg4Q=", + "peers": [], + "clients": [ + { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "privateKey": "l5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tbY=", + "publicKey": "FThkYS9cXUf2hNZomaneT3wwtfdKjr1plkIhK84A2Tg=", + "allowedIPs": [ + "10.0.0.2/32" + ], + "keepAlive": 25 + } + ], + "noKernelTun": false, + "subnetIp": "10.0.0.0", + "subnetCidr": 24 + } + }, + "clientEmail": "user@example.com", + "client": { + "email": "user@example.com", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": true, + "tgId": 0, + "subId": "fxsubid000000001", + "comment": "", + "reset": 0, + "privateKey": "l5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tbY=", + "publicKey": "FThkYS9cXUf2hNZomaneT3wwtfdKjr1plkIhK84A2Tg=", + "allowedIPs": [ + "10.0.0.2/32" + ], + "keepAlive": 25 + } +} \ No newline at end of file