package service import ( "strings" "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" ) func setupEnablePortTest(t *testing.T) { t.Helper() setupConflictDB(t) mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }}) mgr.SetLocalRuntimeOverride(&fakeNodeRuntime{}) runtime.SetManager(mgr) t.Cleanup(func() { runtime.SetManager(nil) }) } func disableInboundRow(t *testing.T, id int) { t.Helper() if err := database.GetDB().Model(&model.Inbound{}).Where("id = ?", id).Update("enable", false).Error; err != nil { t.Fatalf("disable row %d: %v", id, err) } } // Saving a row while it is disabled skips every port guard, so enabling it later // was the one path that could still put two inbounds on one socket. func TestSetInboundEnableRefusesAPortAnotherEnabledInboundServes(t *testing.T) { setupEnablePortTest(t) seedInboundConflict(t, "holder", "0.0.0.0", 44431, model.VLESS, `{"network":"tcp"}`, `{}`) seedInboundConflict(t, "sleeper", "0.0.0.0", 44431, model.VLESS, `{"network":"tcp"}`, `{}`) sleeper := loadInboundByTag(t, "sleeper") disableInboundRow(t, sleeper.Id) _, err := (&InboundService{}).SetInboundEnable(sleeper.Id, true) if err == nil { t.Fatal("enabling a row onto a port another enabled inbound serves must be refused") } if !strings.Contains(err.Error(), "holder") { t.Fatalf("the refusal must name the row that owns the port; got %q", err) } if after := loadInboundByTag(t, "sleeper"); after.Enable { t.Fatal("a refused enable must not write the flag") } } // The enable path must keep the tcp/udp coexistence the rest of the guards // allow, or it would refuse half of the working setups out there. func TestSetInboundEnableAllowsTCPUDPCoexistence(t *testing.T) { setupEnablePortTest(t) seedInboundConflict(t, "udp-holder", "0.0.0.0", 44432, model.Hysteria, ``, `{}`) seedInboundConflict(t, "tcp-sleeper", "0.0.0.0", 44432, model.VLESS, `{"network":"tcp"}`, `{}`) sleeper := loadInboundByTag(t, "tcp-sleeper") disableInboundRow(t, sleeper.Id) if _, err := (&InboundService{}).SetInboundEnable(sleeper.Id, true); err != nil { t.Fatalf("a tcp inbound must be able to enable onto a udp-only row's port: %v", err) } if after := loadInboundByTag(t, "tcp-sleeper"); !after.Enable { t.Fatal("the row must be enabled") } } // Nodes run their own Xray, so a node row sharing a local port is legal and must // stay enableable. func TestSetInboundEnableAllowsANodeRowOnALocalPort(t *testing.T) { setupEnablePortTest(t) seedInboundConflict(t, "local-holder", "0.0.0.0", 44433, model.VLESS, `{"network":"tcp"}`, `{}`) node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, Scheme: "https", Enable: true, Status: "online"} if err := database.GetDB().Create(node).Error; err != nil { t.Fatalf("seed node: %v", err) } seedInboundConflictNode(t, "node-row", "0.0.0.0", 44433, model.VLESS, `{"network":"tcp"}`, `{}`, &node.Id) row := loadInboundByTag(t, "node-row") disableInboundRow(t, row.Id) if _, err := (&InboundService{}).SetInboundEnable(row.Id, true); err != nil { t.Fatalf("a node row must be enableable regardless of a local row's port: %v", err) } }