fix(sub): wrap JSON-subscription SS/Trojan outbound in servers[] array

The flat top-level address/method/password form only parses on recent
xray-core; older bundled cores (e.g. in v2rayN) reject it. Restore the standard
"servers" array used through 2.9.x so the JSON subscription connects across all
xray-core versions. VMess/VLESS keep the flat vnext fallback, which is long
established in xray-core.
This commit is contained in:
MHSanaei
2026-06-17 14:11:44 +02:00
parent 982595968d
commit 340d0df9fc
2 changed files with 27 additions and 10 deletions
+9 -3
View File
@@ -425,16 +425,22 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
}
outbound.StreamSettings = streamSettings
settings := map[string]any{
// Wrap the endpoint in a "servers" array (the standard Xray schema for
// Shadowsocks/Trojan outbounds). The flat top-level form only parses on very
// recent xray-core; older bundled cores (e.g. in v2rayN) reject it, so SS
// links fail to connect. See genVnext/genVless for the VMess/VLESS shape.
server := map[string]any{
"address": serverData[0].Address,
"port": serverData[0].Port,
"password": serverData[0].Password,
"level": 8,
}
if inbound.Protocol == model.Shadowsocks {
settings["method"] = serverData[0].Method
server["method"] = serverData[0].Method
}
outbound.Settings = map[string]any{
"servers": []any{server},
}
outbound.Settings = settings
result, _ := json.MarshalIndent(outbound, "", " ")
return result
+18 -7
View File
@@ -128,21 +128,32 @@ func TestSubJsonServiceVmessFlattened(t *testing.T) {
}
}
func TestSubJsonServiceServerFlattened(t *testing.T) {
// Shadowsocks/Trojan outbounds must use the standard "servers" array so older
// bundled xray-cores (e.g. v2rayN) parse them; the flat top-level form only
// works on very recent xray-core.
func TestSubJsonServiceServerUsesServersArray(t *testing.T) {
trojan := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Trojan, Settings: `{}`}
client := model.Client{Password: "p4ss"}
settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(trojan, nil, client, ""))
if _, ok := settings["servers"]; ok {
t.Fatal("trojan outbound must not use servers array")
server := firstServer(settings)
if server == nil {
t.Fatalf("trojan outbound must use a servers array, got: %#v", settings)
}
if settings["password"] != "p4ss" || settings["address"] != "1.2.3.4" {
t.Fatalf("flat trojan settings wrong: %#v", settings)
if server["password"] != "p4ss" || server["address"] != "1.2.3.4" {
t.Fatalf("trojan server entry wrong: %#v", server)
}
if _, ok := server["method"]; ok {
t.Fatalf("trojan must not carry method: %#v", server)
}
ss := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Shadowsocks, Settings: `{"method":"aes-256-gcm"}`}
ssSettings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(ss, nil, client, ""))
if ssSettings["method"] != "aes-256-gcm" {
t.Fatalf("flat shadowsocks must carry method: %#v", ssSettings)
ssServer := firstServer(ssSettings)
if ssServer == nil {
t.Fatalf("shadowsocks outbound must use a servers array, got: %#v", ssSettings)
}
if ssServer["method"] != "aes-256-gcm" {
t.Fatalf("shadowsocks server entry must carry method: %#v", ssServer)
}
}