package sub import ( "net/url" "strings" "testing" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) // shareLinkInbound builds a VLESS inbound with one client and the given stream // settings, mirroring flowTestInbound but without forcing a flow. func shareLinkInbound(streamSettings string) *model.Inbound { return &model.Inbound{ Listen: "203.0.113.1", Port: 443, Protocol: model.VLESS, Remark: "sharelink", Settings: `{"clients":[{"id":"11111111-2222-4333-8444-555555555555","email":"user"}],"decryption":"none","encryption":"none"}`, StreamSettings: streamSettings, } } // TestGenVlessLink_TLSParamsMapped locks every field that applyShareTLSParams // (service.go:1029) writes into a TLS share link. Without these assertions a mutant // that drops `sni`, swaps a key, or skips `pcs`/`alpn`/`fp` survives the whole suite — // the existing flow tests only check `flow=`. func TestGenVlessLink_TLSParamsMapped(t *testing.T) { stream := `{ "network":"tcp","security":"tls", "tcpSettings":{"header":{"type":"none"}}, "tlsSettings":{ "serverName":"sni.example.com", "alpn":["h2","http/1.1"], "settings":{"fingerprint":"chrome","pinnedPeerCertSha256":["YWJj"]} } }` s := &SubService{} link := s.genVlessLink(shareLinkInbound(stream), "user") // url.Values.Encode() percent-encodes values: "," -> %2C, "/" -> %2F. wants := []string{ "security=tls", "sni=sni.example.com", "fp=chrome", "alpn=h2%2Chttp%2F1.1", "pcs=YWJj", } for _, w := range wants { if !strings.Contains(link, w) { t.Fatalf("TLS link missing %q\n got: %s", w, link) } } } // Locks the reality field mapping of applyShareRealityParams; distinct pbk/sid // catch a swap mutant. spx must never be emitted (V2.0.1 ⑦). func TestGenVlessLink_RealityParamsMapped(t *testing.T) { stream := `{ "network":"tcp","security":"reality", "tcpSettings":{"header":{"type":"none"}}, "realitySettings":{ "serverNames":["reality.example.com"], "shortIds":["ab12cd"], "settings":{"publicKey":"PBKvalue","fingerprint":"firefox","spiderX":"/mypath"} } }` s := &SubService{} link := s.genVlessLink(shareLinkInbound(stream), "user") wants := []string{ "security=reality", "sni=reality.example.com", "pbk=PBKvalue", "sid=ab12cd", "fp=firefox", } for _, w := range wants { if !strings.Contains(link, w) { t.Fatalf("reality link missing %q\n got: %s", w, link) } } if strings.Contains(link, "spx") { t.Fatalf("reality link must not carry spx (V2.0.1 ⑦): %s", link) } // A pbk<->sid swap must not silently pass: pbk must not carry the shortId. if strings.Contains(link, "pbk=ab12cd") || strings.Contains(link, "sid=PBKvalue") { t.Fatalf("reality pbk/sid mapping crossed: %s", link) } } // Scope D: with several serverNames, the generated link's sni must be // the sing-box fragment's tls.server_name — the only name the sing-box // REALITY server serves (the converter stores serverNames[0] there). // The historical behaviour picked a uniformly random list entry per // generation, which xray tolerated (it serves the whole list) but // sing-box refuses as an invalid connection. Without a fragment the // sni still comes from the list. func TestGenVlessLink_RealitySniPrefersSingboxFragment(t *testing.T) { stream := `{ "network":"tcp","security":"reality", "tcpSettings":{"header":{"type":"none"}}, "realitySettings":{ "serverNames":["amd.example.com","connect-sit.amd.example.com","www.amd.example.com"], "shortIds":["ab12cd"], "settings":{"publicKey":"PBKvalue","fingerprint":"firefox"} } }` s := &SubService{} inbound := shareLinkInbound(stream) inbound.SingboxConfig = `{"type":"vless","tag":"in","listen":"::","listen_port":443,` + `"tls":{"enabled":true,"server_name":"amd.example.com",` + `"reality":{"enabled":true,"short_id":["ab12cd"]}}}` // The losing branch is a random pick: sample enough generations // that a regression to it cannot survive by luck (2/3 wrong per // draw; 30 draws make a false pass ~2e-6). for i := 0; i < 30; i++ { link := s.genVlessLink(inbound, "user") u, err := url.Parse(link) if err != nil { t.Fatalf("parse link %q: %v", link, err) } if got := u.Query().Get("sni"); got != "amd.example.com" { t.Fatalf("draw %d: sni = %q, want the fragment's amd.example.com\nlink: %s", i, got, link) } } // No fragment (or an unusable one): historical behaviour — the // sni is some entry of the serverNames list. for _, frag := range []string{"", "{not json", `{"type":"vless"}`} { inbound.SingboxConfig = frag link := s.genVlessLink(inbound, "user") u, err := url.Parse(link) if err != nil { t.Fatalf("parse link %q: %v", link, err) } switch got := u.Query().Get("sni"); got { case "amd.example.com", "connect-sit.amd.example.com", "www.amd.example.com": default: t.Fatalf("fragment %q: sni = %q, want a serverNames entry", frag, got) } } } // realityTwoClientInbound builds a reality VLESS inbound carrying two clients // with distinct subIds. func realityTwoClientInbound() *model.Inbound { return &model.Inbound{ Listen: "203.0.113.1", Port: 443, Protocol: model.VLESS, Remark: "sharelink", Settings: `{"clients":[ {"id":"11111111-2222-4333-8444-555555555555","email":"alice","subId":"subAlice"}, {"id":"22222222-3333-4444-8555-666666666666","email":"bob","subId":"subBob"} ],"decryption":"none","encryption":"none"}`, StreamSettings: `{ "network":"tcp","security":"reality", "tcpSettings":{"header":{"type":"none"}}, "realitySettings":{ "serverNames":["reality.example.com"], "shortIds":["ab12cd"], "settings":{"publicKey":"PBKvalue","fingerprint":"firefox","spiderX":"/seed"} } }`, } } // V2.0.1 ⑦: generated reality links never carry spx, for any client, // even when the inbound still stores a spiderX seed. func TestGenVlessLink_RealityOmitsSpx(t *testing.T) { s := &SubService{} inbound := realityTwoClientInbound() for _, email := range []string{"alice", "bob"} { link := s.genVlessLink(inbound, email) u, err := url.Parse(link) if err != nil { t.Fatalf("parse link %q: %v", link, err) } if _, ok := u.Query()["spx"]; ok { t.Fatalf("link for %s carries spx: %s", email, link) } } }