diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index a1b1dde35..0cb943ac5 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -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 diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index f1414c72c..e7daf34d1 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -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) } }