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

fix(inbound): scope port-conflict check to the stored node on update (#5833)

* fix(inbound): scope port-conflict check to the stored node on update

UpdateInbound called checkPortConflict before restoring the inbound's NodeID
from the database, so the check used the NodeID from the request body. That
value is unreliable for edits: clients omit it (nodeId is `json:",omitempty"`)
and the code already treats the stored NodeID as authoritative — an inbound
can't be moved between nodes via edit. With a nil request NodeID a node inbound
was mis-checked as a local/main-panel inbound and falsely collided with an
unrelated inbound that happened to reuse the same port on the central panel (or
another node). Symptom: editing a node inbound's listen address was rejected
with "port <p> (tcp) already used by inbound ... " and silently discarded.

Load the old inbound and restore inbound.NodeID *before* checkPortConflict, so
the check runs against the node the inbound actually lives on. checkPortConflict
already scopes candidates by node (sameNode); it was simply being fed the wrong
NodeID.

Add a regression test that seeds a main-panel and a node inbound on the same
port and asserts the node inbound stays editable (fails before this change with
the exact "already used" rejection).

* style(inbound): trim inline comments from port-conflict scoping

Repo convention forbids // line comments in committed Go; keep the scoping fix self-documenting.
Yuri Khachaturyan 22 часов назад
Родитель
Сommit
2c28fa5f48
2 измененных файлов с 32 добавлено и 5 удалено
  1. 8 5
      internal/web/service/inbound.go
  2. 24 0
      internal/web/service/inbound_update_tag_test.go

+ 8 - 5
internal/web/service/inbound.go

@@ -1104,6 +1104,14 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 	s.normalizeMtprotoSecret(inbound)
 	inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)
 
+	oldInbound, err := s.GetInbound(inbound.Id)
+	if err != nil {
+		return inbound, false, err
+	}
+	// Restore the stored NodeID before the port-conflict check so a node inbound
+	// stays scoped to its own node (the payload's nodeId is unreliable, often absent).
+	inbound.NodeID = oldInbound.NodeID
+
 	conflict, err := s.checkPortConflict(inbound, inbound.Id)
 	if err != nil {
 		return inbound, false, err
@@ -1112,11 +1120,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 		return inbound, false, common.NewError(conflict.String())
 	}
 
-	oldInbound, err := s.GetInbound(inbound.Id)
-	if err != nil {
-		return inbound, false, err
-	}
-	inbound.NodeID = oldInbound.NodeID
 	// Capture the pre-edit protocol and routing state before oldInbound is
 	// overwritten with the new values further down, then ensure a routed
 	// inbound keeps a stable egress port (reusing the one already stored).

+ 24 - 0
internal/web/service/inbound_update_tag_test.go

@@ -1,6 +1,7 @@
 package service
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/mhsanaei/3x-ui/v3/internal/database"
@@ -67,6 +68,29 @@ func TestUpdateInbound_NodeTagKeepsPrefixWhenNodeIdOmitted(t *testing.T) {
 	}
 }
 
+// A node inbound sharing a port with a local inbound must stay editable: the
+// port-conflict check is scoped to the inbound's stored NodeID, not the body's.
+func TestUpdateInbound_NodeInboundNotBlockedByLocalSamePort(t *testing.T) {
+	setupConflictDB(t)
+	seedInboundConflict(t, "in-10000-tcp", "0.0.0.0", 10000, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`)
+	seedInboundConflictNode(t, "n1-in-10000-tcp", "0.0.0.0", 10000, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`, new(1))
+
+	var nodeInbound model.Inbound
+	if err := database.GetDB().Where("tag = ?", "n1-in-10000-tcp").First(&nodeInbound).Error; err != nil {
+		t.Fatalf("read seeded node row: %v", err)
+	}
+
+	svc := &InboundService{}
+	update := nodeInbound
+	update.Listen = "10.0.0.5"
+	update.NodeID = nil
+	_, _, err := svc.UpdateInbound(&update)
+
+	if err != nil && strings.Contains(err.Error(), "already used") {
+		t.Fatalf("node inbound edit wrongly rejected as a port conflict: %v", err)
+	}
+}
+
 // a tag the user set by hand (doesn't match the canonical shape) survives a
 // port change untouched.
 func TestUpdateInbound_KeepsCustomTagOnPortChange(t *testing.T) {