diff --git a/internal/sub/json_flow_gate_test.go b/internal/sub/json_flow_gate_test.go new file mode 100644 index 000000000..b820573b5 --- /dev/null +++ b/internal/sub/json_flow_gate_test.go @@ -0,0 +1,87 @@ +package sub + +import ( + "fmt" + "strings" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +func seedFlowInbound(t *testing.T, subId, tag string, port int, stream string) *model.Inbound { + t.Helper() + db := database.GetDB() + uuid := "11111111-2222-4333-8444-" + fmt.Sprintf("%012d", port) + email := tag + "@e" + settings := fmt.Sprintf( + `{"clients":[{"id":%q,"email":%q,"subId":%q,"enable":true,"flow":"xtls-rprx-vision"}],"decryption":"none"}`, + uuid, email, subId) + ib := &model.Inbound{ + UserId: 1, Tag: tag, Enable: true, Listen: "203.0.113.5", Port: port, + Protocol: model.VLESS, Remark: tag, Settings: settings, StreamSettings: stream, + SubSortIndex: 1, + } + if err := db.Create(ib).Error; err != nil { + t.Fatalf("seed inbound %s: %v", tag, err) + } + client := &model.ClientRecord{Email: email, SubID: subId, UUID: uuid, Enable: true} + if err := db.Create(client).Error; err != nil { + t.Fatalf("seed client %s: %v", email, err) + } + link := &model.ClientInbound{ClientId: client.Id, InboundId: ib.Id, FlowOverride: "xtls-rprx-vision"} + if err := db.Create(link).Error; err != nil { + t.Fatalf("seed client_inbound %s: %v", email, err) + } + return ib +} + +// A vision flow left on a client after its inbound moved to a transport Vision +// cannot use is stripped from the raw link and the Clash proxy; the JSON +// subscription must agree instead of emitting an outbound xray rejects. +func TestSub_JSONStripsFlowOnUnsupportedTransport(t *testing.T) { + seedSubDB(t) + seedFlowInbound(t, "s1", "wsflow", 4601, wsTLSStream) + + links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com") + if err != nil { + t.Fatalf("GetSubs: %v", err) + } + if joined := strings.Join(links, "\n"); strings.Contains(joined, "flow=") { + t.Fatalf("raw link must not carry a flow on ws+tls: %s", joined) + } + + clash := NewSubClashService(false, "", NewSubService("")) + yaml, _, err := clash.GetClash("s1", "req.example.com") + if err != nil { + t.Fatalf("GetClash: %v", err) + } + if strings.Contains(yaml, "flow:") { + t.Fatalf("clash proxy must not carry a flow on ws+tls:\n%s", yaml) + } + + js := NewSubJsonService("", "", "", NewSubService("")) + out, _, err := js.GetJson("s1", "req.example.com", false) + if err != nil { + t.Fatalf("GetJson: %v", err) + } + if strings.Contains(out, `"flow"`) { + t.Fatalf("json outbound must not carry a flow on ws+tls:\n%s", out) + } +} + +// The gate must not strip a flow the transport does support. +func TestSub_JSONKeepsFlowOnTcpTLS(t *testing.T) { + seedSubDB(t) + seedFlowInbound(t, "s1", "tcpflow", 4602, + `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"}}`) + + js := NewSubJsonService("", "", "", NewSubService("")) + out, _, err := js.GetJson("s1", "req.example.com", false) + if err != nil { + t.Fatalf("GetJson: %v", err) + } + if !strings.Contains(out, `"flow": "xtls-rprx-vision"`) { + t.Fatalf("json outbound must keep the vision flow on tcp+tls:\n%s", out) + } +} diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 9ac60c2dc..cbd8ddb90 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -218,6 +218,13 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c case "vless": vc := client vc.ID = applyVlessRoute(client.ID, hostVlessRoute(extPrxy)) + // Same gate the raw link and the Clash proxy apply: a flow left + // over from a transport Vision supported produces an outbound + // xray refuses to start. + newNetwork, _ := newStream["network"].(string) + if vc.Flow != "" && !vlessFlowAllowed(newNetwork, security, subReq.linkSettings(inbound)) { + vc.Flow = "" + } newOutbounds = append(newOutbounds, s.genVless(subReq, inbound, streamSettings, vc, jsonMux(mux, hostMux))) case "trojan", "shadowsocks": newOutbounds = append(newOutbounds, s.genServer(subReq, inbound, streamSettings, client, jsonMux(mux, hostMux)))