Files
3x-ui/internal/database/outbound_removed_keys_migration_test.go
T
Sanaei 2ec6c73613 feat(xray): update xray-core to v26.9.8 and adapt panel
Bump xtls/xray-core to 37ceb8b4b6 (v26.9.8) and the three binary pins
(DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted
symbols; the impact is entirely on the JSON config surface.

Outbound "proxySettings" is now refused by the config loader (moved to
streamSettings.sockopt.dialerProxy) and a freedom outbound rejects
sockopt.addressPortStrategy. Either key in a stored template would keep
the core from starting after the upgrade, so a new OutboundRemovedKeysFix
seeder rewrites xrayTemplateConfig once: proxySettings.tag becomes
sockopt.dialerProxy (an existing dialerProxy wins) and addressPortStrategy
is dropped from freedom outbounds. Template saves and outbound
subscriptions already run through the vendored loader, so the new
refusals surface there with the core's own message.

REALITY no longer applies a built-in minClientVer (26.3.27) when the
field is empty. The form placeholder and the min/max hints in all 13
locales now say that empty means no minimum.

New upstream keys the Zod schemas would otherwise strip, with form
support where a sibling field already had it:
- blackhole response type "custom" with base64 customResponseData
- realm finalmask ipMode (dual/v4/v6) and portMapping (UPnP / NAT-PMP)
- quicParams brutalDisableLossCompensation, disableChromeParrot,
  disableGSO, disableStatelessReset
- hysteria masquerade proxy xForwarded
- wireguard outbound remoteDNS
- routing rule localOS

freedom.domainStrategy is only deprecated upstream (auto-migrated to
sockopt.domainStrategy with a warning) and is left untouched.
2026-09-08 13:49:32 +02:00

118 lines
3.9 KiB
Go

package database
import (
"encoding/json"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
func TestRewriteRemovedOutboundKeys(t *testing.T) {
tests := []struct {
name string
raw string
wantChanged bool
wantOutbound map[string]any
}{
{
name: "proxySettings.tag becomes sockopt.dialerProxy",
raw: `{"outbounds":[{"protocol":"vless","tag":"chain","settings":{},"proxySettings":{"tag":"hop","transportLayer":true}}]}`,
wantChanged: true,
wantOutbound: map[string]any{
"protocol": "vless", "tag": "chain", "settings": map[string]any{},
"streamSettings": map[string]any{"sockopt": map[string]any{"dialerProxy": "hop"}},
},
},
{
name: "an existing dialerProxy wins over proxySettings",
raw: `{"outbounds":[{"protocol":"vless","tag":"chain","proxySettings":{"tag":"hop"},"streamSettings":{"network":"tcp","sockopt":{"dialerProxy":"keep"}}}]}`,
wantChanged: true,
wantOutbound: map[string]any{
"protocol": "vless", "tag": "chain",
"streamSettings": map[string]any{"network": "tcp", "sockopt": map[string]any{"dialerProxy": "keep"}},
},
},
{
name: "freedom drops sockopt.addressPortStrategy",
raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","streamSettings":{"sockopt":{"addressPortStrategy":"SrvPortOnly","tcpFastOpen":true}}}]}`,
wantChanged: true,
wantOutbound: map[string]any{
"protocol": "freedom", "tag": "direct",
"streamSettings": map[string]any{"sockopt": map[string]any{"tcpFastOpen": true}},
},
},
{
name: "addressPortStrategy stays on other protocols",
raw: `{"outbounds":[{"protocol":"vless","tag":"proxy","streamSettings":{"sockopt":{"addressPortStrategy":"SrvPortOnly"}}}]}`,
wantChanged: false,
wantOutbound: map[string]any{
"protocol": "vless", "tag": "proxy",
"streamSettings": map[string]any{"sockopt": map[string]any{"addressPortStrategy": "SrvPortOnly"}},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
updated, changed, err := rewriteRemovedOutboundKeys(tc.raw)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if changed != tc.wantChanged {
t.Fatalf("changed = %v, want %v", changed, tc.wantChanged)
}
var cfg struct {
Outbounds []map[string]any `json:"outbounds"`
}
if err := json.Unmarshal([]byte(updated), &cfg); err != nil {
t.Fatalf("rewritten template is not JSON: %v", err)
}
if len(cfg.Outbounds) != 1 {
t.Fatalf("got %d outbounds, want 1", len(cfg.Outbounds))
}
got, _ := json.Marshal(cfg.Outbounds[0])
want, _ := json.Marshal(tc.wantOutbound)
if string(got) != string(want) {
t.Fatalf("outbound = %s, want %s", got, want)
}
})
}
}
func TestRewriteRemovedOutboundKeysSatisfiesCore(t *testing.T) {
raw := `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{},"proxySettings":{"tag":"hop"},"streamSettings":{"sockopt":{"addressPortStrategy":"SrvPortOnly"}}}]}`
var before struct {
Outbounds []json.RawMessage `json:"outbounds"`
}
if err := json.Unmarshal([]byte(raw), &before); err != nil {
t.Fatal(err)
}
if err := xray.ValidateOutboundConfig(before.Outbounds[0]); err == nil {
t.Fatal("expected the vendored core to refuse the legacy outbound")
}
updated, changed, err := rewriteRemovedOutboundKeys(raw)
if err != nil || !changed {
t.Fatalf("rewrite: changed=%v err=%v", changed, err)
}
var after struct {
Outbounds []json.RawMessage `json:"outbounds"`
}
if err := json.Unmarshal([]byte(updated), &after); err != nil {
t.Fatal(err)
}
if err := xray.ValidateOutboundConfig(after.Outbounds[0]); err != nil {
t.Fatalf("rewritten outbound still refused by xray-core: %v", err)
}
}
func TestRewriteRemovedOutboundKeysInvalidJSON(t *testing.T) {
_, changed, err := rewriteRemovedOutboundKeys("{not json")
if err == nil {
t.Fatal("expected an error for invalid JSON")
}
if changed {
t.Fatal("invalid JSON must not report a change")
}
}