Scope B (Windows agent): - probe.go split into shared core + probe_linux.go (verbatim Linux sampler, build-tagged) + probe_windows.go (Win32 sampler: GetSystemTimes CPU diff, GlobalMemoryStatusEx, fixed-drive df totals, GetIfTable2 counted set, MachineGuid boot identity, registry facts; load and TCP/UDP counts stay zero - x/sys v0.47.0 exports no wrapper) - update intents are acknowledged and skipped on Windows (the release assets are Linux binaries; Windows nodes update via install-agent.ps1) - cmd/xui-agent-windows: SCM service host (svc.Run + mgr install/ uninstall/start/stop) mirroring main.go's agent lifecycle wiring - install-agent.ps1: downloads the exe, writes probeOnly agent.json, registers and starts the service, rerun-safe Scope C (version display): - config.GetAgentDisplayVersion (and the master getter) compose from the bare number so a V-form injection no longer doubles the letter (AV2.1.0 -> A2.1.0) - node list view repairs stored AV-prefixed reports at assembly so deployed agents read uniformly without an update Scope D (production fix): - share-link reality sni: when the inbound has a sing-box fragment, sni is the fragment's tls.server_name - the only name the sing-box REALITY server serves. The previous rule picked a uniformly random serverNames entry per generation, which xray tolerated but sing-box refuses as an invalid connection. sid keeps its random pick: the converter carries the whole shortIds list into the fragment, so every listed sid is served.
186 lines
6.1 KiB
Go
186 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|