Просмотр исходного кода

fix(sub): gate the VLESS flow in JSON subscriptions like raw and Clash links

genVless emitted client.Flow unconditionally, while the raw link
(service.go:806) and the Clash proxy (clash_service.go:251) both gate it
behind vlessFlowAllowed. A flow_override left on client_inbounds after its
inbound moved to a transport Vision cannot use -- ws, grpc, httpupgrade --
therefore survived only into the JSON subscription, handing that client an
outbound xray-core rejects while its other two formats were correct.

Apply the same gate at the call site, reading the network from the
per-host stream so a host that rewrites the transport is judged on what it
actually emits. Verified by seeding a flow_override on a ws+tls inbound:
before, raw and Clash dropped the flow and JSON kept it.
Sanaei 13 часов назад
Родитель
Сommit
29557e2153
2 измененных файлов с 94 добавлено и 0 удалено
  1. 87 0
      internal/sub/json_flow_gate_test.go
  2. 7 0
      internal/sub/json_service.go

+ 87 - 0
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)
+	}
+}

+ 7 - 0
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)))