diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index bdfba3c60..495a4f086 100644 --- a/internal/web/service/inbound.go +++ b/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). diff --git a/internal/web/service/inbound_update_tag_test.go b/internal/web/service/inbound_update_tag_test.go index 38d61b230..d55835438 100644 --- a/internal/web/service/inbound_update_tag_test.go +++ b/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) {