Files
3x-ui-agent/internal/web/service/inbound_finalmask_xmc_test.go
T

195 lines
7.7 KiB
Go

package service
import (
"encoding/json"
"testing"
)
const completeXmcProfile = `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`
func TestValidateFinalMaskXmcProfiles(t *testing.T) {
tests := []struct {
name string
streamSettings string
wantErr bool
}{
{
name: "empty streamSettings",
streamSettings: "",
wantErr: false,
},
{
name: "no finalmask",
streamSettings: `{"network":"tcp","security":"none"}`,
wantErr: false,
},
{
name: "non-xmc mask is untouched",
streamSettings: `{"finalmask":{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}}`,
wantErr: false,
},
{
name: "xmc with a complete profile",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"hostname":"mc.example.com","password":"pw","profiles":[` + completeXmcProfile + `]}}]}}`,
wantErr: false,
},
{
name: "legacy usernames shape without profiles",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"hostname":"mc.example.com","password":"pw","usernames":["Dream"]}}]}}`,
wantErr: true,
},
{
name: "xmc with an empty profiles array",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[]}}]}}`,
wantErr: true,
},
{
name: "profile missing the textures signature",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":""}]}}]}}`,
wantErr: true,
},
{
name: "profile with an unparseable uuid",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[{"username":"Notch","uuid":"not-a-uuid","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}]}}]}}`,
wantErr: true,
},
{
name: "profile with an out-of-range username",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[{"username":"ab","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}]}}]}}`,
wantErr: true,
},
{
name: "one complete and one incomplete profile",
streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[` + completeXmcProfile + `,{"username":"Herobrine","uuid":"","texturesValue":"","texturesSignature":""}]}}]}}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateFinalMaskXmcProfiles(tt.streamSettings)
if (err != nil) != tt.wantErr {
t.Errorf("validateFinalMaskXmcProfiles(%q) error = %v, wantErr %v", tt.streamSettings, err, tt.wantErr)
}
})
}
}
// TestXmcMaskProfilesCompleteExpectations pins the panel predicate's own
// contract. Until the lite peel it was cross-checked against xray-core's
// loader (every accepted profile had to build in the core and vice versa);
// with the library gone the predicate stands on its own rules — username
// ^[A-Za-z0-9_]{3,16}$, a parseable uuid, non-empty textures value and
// signature, at least one profile — and this table records them. A core
// release that tightens or relaxes its side is no longer detected here;
// the strip/validate pair errs on the panel's documented rules instead.
func TestXmcMaskProfilesCompleteExpectations(t *testing.T) {
profiles := []struct {
name string
raw string
want bool
}{
{name: "complete", raw: completeXmcProfile, want: true},
{name: "undashed uuid", raw: `{"username":"Notch","uuid":"069a79f444e94726a5befca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: true},
{name: "username at the 16 char limit", raw: `{"username":"Abcdefghijklmnop","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: true},
{name: "username over the limit", raw: `{"username":"Abcdefghijklmnopq","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: false},
{name: "username with a hyphen", raw: `{"username":"No-tch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: false},
{name: "empty uuid", raw: `{"username":"Notch","uuid":"","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: false},
{name: "missing textures value", raw: `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"","texturesSignature":"c2ln"}`, want: false},
{name: "no profiles at all", raw: ``, want: false},
}
for _, tt := range profiles {
t.Run(tt.name, func(t *testing.T) {
mask := map[string]any{"type": "xmc"}
var settings map[string]any
raw := tt.raw
if raw == "" {
raw = `{}` // a mask whose settings carry no profiles key at all
if err := json.Unmarshal([]byte(raw), &settings); err != nil {
t.Fatalf("unmarshal settings: %v", err)
}
} else {
if err := json.Unmarshal([]byte(`{"profiles":[`+raw+`]}`), &settings); err != nil {
t.Fatalf("unmarshal settings: %v", err)
}
}
mask["settings"] = settings
if got := xmcMaskProfilesComplete(mask); got != tt.want {
t.Errorf("xmcMaskProfilesComplete = %v, want %v", got, tt.want)
}
})
}
}
func TestStripIncompleteXmcMasks(t *testing.T) {
tests := []struct {
name string
stream string
wantDropped int
wantStream string
}{
{
name: "legacy usernames mask is dropped and finalmask removed",
stream: `{"network":"tcp","finalmask":{"tcp":[{"type":"xmc","settings":{"usernames":["Dream"],"password":"pw"}}]}}`,
wantDropped: 1,
wantStream: `{"network":"tcp"}`,
},
{
name: "complete mask is kept",
stream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[` + completeXmcProfile + `]}}]}}`,
wantDropped: 0,
wantStream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[` + completeXmcProfile + `]}}]}}`,
},
{
name: "sibling masks survive the drop",
stream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"usernames":["Dream"]}},{"type":"fragment","settings":{"packets":"tlshello"}}]}}`,
wantDropped: 1,
wantStream: `{"finalmask":{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}}`,
},
{
name: "udp masks are preserved when tcp empties out",
stream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{}}],"udp":[{"type":"salamander"}]}}`,
wantDropped: 1,
wantStream: `{"finalmask":{"udp":[{"type":"salamander"}]}}`,
},
{
name: "stream without finalmask is untouched",
stream: `{"network":"tcp","security":"tls"}`,
wantDropped: 0,
wantStream: `{"network":"tcp","security":"tls"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var stream map[string]any
if err := json.Unmarshal([]byte(tt.stream), &stream); err != nil {
t.Fatalf("unmarshal stream: %v", err)
}
dropped := stripIncompleteXmcMasks(stream)
if dropped != tt.wantDropped {
t.Errorf("stripIncompleteXmcMasks dropped = %d, want %d", dropped, tt.wantDropped)
}
var want map[string]any
if err := json.Unmarshal([]byte(tt.wantStream), &want); err != nil {
t.Fatalf("unmarshal wantStream: %v", err)
}
got, err := json.Marshal(stream)
if err != nil {
t.Fatalf("marshal stream: %v", err)
}
wantJSON, err := json.Marshal(want)
if err != nil {
t.Fatalf("marshal want: %v", err)
}
if string(got) != string(wantJSON) {
t.Errorf("stream after strip = %s, want %s", got, wantJSON)
}
})
}
}