Files
3x-ui/internal/sub/service_flow_test.go
T
Farhan Zare 930a0ed59d feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698)
* feat(inbound): add DisableFlow to opt an inbound out of auto XTLS Vision

Adds an inbound-level DisableFlow flag so operators can suppress automatic
xtls-rprx-vision injection on a specific inbound even when its transport is
flow-capable — e.g. a tunneled/CDN-fronted XHTTP+vlessenc inbound where Vision
is not wanted, while keeping it on the same client's Reality inbounds.

When set, the inbound reports tlsFlowCapable=false, the write path clamps each
attached client's flow to empty (so flow_override stores ""), and share
links/subscriptions never carry the flow for it. The flag is panel-only
metadata and is never sent to xray.

Closes part of #5689.

* feat(inbound): DisableFlow toggle in the inbound form (frontend)

Wire the DisableFlow field through the form schema + adapters and add a
VLESS-gated switch in the inbound form, plus en-US strings. tsc --noEmit and
eslint pass.

* fix(inbound): honor DisableFlow in all emitters + on toggle; regen OpenAPI

Addresses review on #5690:
- Clash (clash_service.go) and JSON (json_service.go) subscription emitters now
  also skip the flow for a DisableFlow inbound — previously only the raw
  share-link path was gated, so those two still advertised it (blocking 1).
- UpdateInbound now strips any flow already stored on a DisableFlow inbound's
  clients (settings.clients[].flow + client_inbounds.flow_override) so xray and
  the subscription agree; otherwise toggling DisableFlow on an existing Vision
  client left xray expecting a flow the client no longer sends.
- Regenerated the OpenAPI + zod/types/examples artifacts for the new field and
  added an example tag (blocking 2; make gen-check is clean).
- Added Clash + JSON DisableFlow suppression tests alongside the raw-link one.

* fix(inbound): make DisableFlow durable, clamp on create, guard live config

Addresses the review + completeness audit on #5690:
- UpdateInbound now persists inbound.DisableFlow onto the saved row. It was
  only read to branch strip-vs-restore, so toggling the flag on an existing
  inbound never stuck and MigrationRestoreVisionFlow re-injected the flow — the
  exact #5689 path (editing a multi-inbound client's inbound) self-reverted.
- DBInbound (frontend) declares + initializes disableFlow so ObjectUtil
  .cloneProps carries the API value through; the edit Switch previously always
  read false and re-saving silently reverted the opt-out.
- AddInbound strips client flow (settings + parsed clients) when DisableFlow is
  set, so a created-disabled inbound never persists a flow xray would expect.
- GetXrayConfig forces flow="" for DisableFlow inbounds (VLESS + Trojan) as
  defense-in-depth, keeping the live config and the subscription in agreement.
- genTrojanLink share link honors DisableFlow too.
- Drop the dead explicit flow_override clear in UpdateInbound (SyncInbound
  rebuilds it from the stripped settings).
- Clear disableFlow in the inbound form when switching to a non-VLESS protocol.
- Add disableFlow/disableFlowHelp to the remaining 12 locales.

Tests: stripClientFlows unit cases; DB-backed AddInbound clamp; UpdateInbound
persist+strip+resist-restore regression (fails without the persist fix);
frontend DBInbound + adapter round-trip (fails without the model field).

* style(inbound): drop // line comments per repo CLAUDE.md

The DisableFlow work followed the surrounding code's commenting style; the repo
CLAUDE.md forbids // line comments in committed Go/TS. Remove the comments I
added (Go + frontend + tests) and regenerate OpenAPI/schemas, which drops the
generated field descriptions sourced from the Go doc comments. No behavior
change; full go test (service+sub, CGO) + frontend typecheck/vitest green;
golangci-lint clean on the changed files.

* fix(runtime): propagate disableFlow to nodes

Preserve the inbound DisableFlow flag when syncing inbounds across nodes and when recreating central records from remote traffic snapshots. This keeps multi-node deployments from reintroducing VLESS Vision flow in node configs and share links, and updates the related tests to cover the wired field and VLESS JSON generation.
2026-08-15 23:09:16 +02:00

111 lines
3.5 KiB
Go

package sub
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// Issue #5232: a vision flow set on a VLESS+XHTTP+REALITY (vlessenc) client
// must survive into subscription output, not just the inbound JSON.
const testMlkemEncryption = "mlkem768x25519plus.native.0rtt.dGVzdC1rZXk"
func TestVlessFlowAllowed(t *testing.T) {
enc := map[string]any{"encryption": testMlkemEncryption}
noEnc := map[string]any{"encryption": "none"}
tests := []struct {
name string
network string
security string
settings map[string]any
want bool
}{
{"tcp tls", "tcp", "tls", noEnc, true},
{"tcp reality", "tcp", "reality", noEnc, true},
{"tcp none", "tcp", "none", noEnc, false},
{"tcp none vlessenc", "tcp", "none", enc, false},
{"xhttp none vlessenc", "xhttp", "none", enc, true},
{"xhttp reality vlessenc (#5232)", "xhttp", "reality", enc, true},
{"xhttp tls vlessenc", "xhttp", "tls", enc, true},
{"xhttp reality no vlessenc", "xhttp", "reality", noEnc, false},
{"ws tls", "ws", "tls", noEnc, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := vlessFlowAllowed(tc.network, tc.security, tc.settings); got != tc.want {
t.Fatalf("vlessFlowAllowed(%q, %q, %v) = %v, want %v", tc.network, tc.security, tc.settings, got, tc.want)
}
})
}
}
func flowTestInbound(streamSettings, encryption string) *model.Inbound {
return &model.Inbound{
Listen: "203.0.113.1",
Port: 443,
Protocol: model.VLESS,
Remark: "flowtest",
Settings: `{"clients":[{"id":"11111111-2222-4333-8444-555555555555","email":"user","flow":"xtls-rprx-vision"}],` +
`"decryption":"` + encryption + `","encryption":"` + encryption + `"}`,
StreamSettings: streamSettings,
}
}
const xhttpRealityStream = `{
"network": "xhttp",
"security": "reality",
"xhttpSettings": {"path": "/", "mode": "auto"},
"realitySettings": {
"serverNames": ["example.com"],
"shortIds": ["abcd"],
"settings": {"publicKey": "pub", "fingerprint": "chrome"}
}
}`
func TestGenVlessLink_FlowXhttpRealityVlessenc(t *testing.T) {
s := &SubService{}
link := s.genVlessLink(flowTestInbound(xhttpRealityStream, testMlkemEncryption), "user")
if !strings.Contains(link, "flow=xtls-rprx-vision") {
t.Fatalf("xhttp+reality+vlessenc link must carry the vision flow (#5232), got %q", link)
}
}
func TestGenVlessLink_NoFlowXhttpRealityWithoutVlessenc(t *testing.T) {
s := &SubService{}
link := s.genVlessLink(flowTestInbound(xhttpRealityStream, "none"), "user")
if strings.Contains(link, "flow=") {
t.Fatalf("xhttp+reality without vlessenc must not carry a flow, got %q", link)
}
}
func TestGenVlessLink_DisableFlowSuppressesFlow(t *testing.T) {
s := &SubService{}
ib := flowTestInbound(xhttpRealityStream, testMlkemEncryption)
ib.DisableFlow = true
link := s.genVlessLink(ib, "user")
if strings.Contains(link, "flow=") {
t.Fatalf("DisableFlow inbound must not carry a flow even when the transport is capable, got %q", link)
}
}
func TestGenVlessLink_FlowTcpRealityStillWorks(t *testing.T) {
stream := `{
"network": "tcp",
"security": "reality",
"tcpSettings": {"header": {"type": "none"}},
"realitySettings": {
"serverNames": ["example.com"],
"shortIds": ["abcd"],
"settings": {"publicKey": "pub", "fingerprint": "chrome"}
}
}`
s := &SubService{}
link := s.genVlessLink(flowTestInbound(stream, "none"), "user")
if !strings.Contains(link, "flow=xtls-rprx-vision") {
t.Fatalf("tcp+reality link must keep the vision flow, got %q", link)
}
}