Ver código fonte

fix(inbounds): let a node-adopted inbound keep its own protocol on edit

UpdateInbound restores the stored NodeID before the node-eligibility check, so
the payload can never introduce an assignment there — the check could only ever
fire on a row that already had one. A node's MTProto inbound arrives on the
master by adoption, which does not go through that check, so every later edit of
it was refused with "mtproto inbounds cannot be assigned to a node". That made
the share address of a node-managed MTProto inbound impossible to change from
the panel that generates its subscription links.

Refuse only a protocol change into an ineligible protocol, which is the one way
an update can still strand a row the master's sidecar loops would never
reconcile.

Closes #6415
Sanaei 8 horas atrás
pai
commit
2004340d1d

+ 3 - 1
internal/web/service/inbound.go

@@ -1598,7 +1598,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 	// 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
-	if inbound.NodeID != nil && !isNodeEligibleProtocol(inbound.Protocol) {
+	// The node assignment is the stored one, so only a protocol change can
+	// introduce one; a row adopted from a node keeps the protocol it arrived with.
+	if inbound.NodeID != nil && inbound.Protocol != oldInbound.Protocol && !isNodeEligibleProtocol(inbound.Protocol) {
 		return inbound, false, common.NewErrorf("%s inbounds cannot be assigned to a node", inbound.Protocol)
 	}
 

+ 67 - 0
internal/web/service/inbound_node_protocol_update_test.go

@@ -0,0 +1,67 @@
+package service
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+// A node-managed inbound arrives by adoption, so its protocol can be one the
+// master never assigns itself. Editing its share metadata must still work.
+func TestUpdateInbound_NodeMtprotoShareAddrIsEditable(t *testing.T) {
+	setupConflictDB(t)
+	nodeID := 5
+	seedNodeRow(t, database.GetDB(), &model.Node{Id: nodeID, Name: "n5", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
+	seedInboundConflictNode(t, "mt-node", "127.0.0.1", 4063, model.MTProto, `{}`,
+		`{"clients":[{"email":"mt-node-c","enable":true,"secret":"ee0123456789abcdef0123456789abcdef"}]}`, &nodeID)
+
+	var existing model.Inbound
+	if err := database.GetDB().Where("tag = ?", "mt-node").First(&existing).Error; err != nil {
+		t.Fatalf("read seeded row: %v", err)
+	}
+
+	update := existing
+	update.ShareAddrStrategy = "custom"
+	update.ShareAddr = "new-share.example.com"
+	updated, _, err := (&InboundService{}).UpdateInbound(&update)
+	if err != nil {
+		t.Fatalf("UpdateInbound on a node-managed mtproto inbound: %v", err)
+	}
+	if updated.NodeID == nil || *updated.NodeID != nodeID {
+		t.Fatalf("nodeID = %v, want %d preserved", updated.NodeID, nodeID)
+	}
+
+	var hosts []model.Host
+	if err := database.GetDB().Where("inbound_id = ?", existing.Id).Find(&hosts).Error; err != nil {
+		t.Fatalf("load hosts: %v", err)
+	}
+	if len(hosts) != 1 || hosts[0].Address != "new-share.example.com" {
+		t.Fatalf("hosts = %+v, want one new-share.example.com host", hosts)
+	}
+}
+
+// Converting a node inbound to a protocol the master's sidecars only reconcile
+// for local rows is still refused: those loops query node_id IS NULL.
+func TestUpdateInbound_RejectsProtocolChangeToNodeIneligible(t *testing.T) {
+	setupConflictDB(t)
+	nodeID := 6
+	seedNodeRow(t, database.GetDB(), &model.Node{Id: nodeID, Name: "n6", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
+	seedInboundConflictNode(t, "vless-node", "127.0.0.1", 4064, model.VLESS,
+		`{"network":"tcp","security":"none"}`,
+		`{"clients":[{"id":"11111111-2222-4333-8444-555555555555","email":"vn-c","enable":true}],"decryption":"none"}`, &nodeID)
+
+	var existing model.Inbound
+	if err := database.GetDB().Where("tag = ?", "vless-node").First(&existing).Error; err != nil {
+		t.Fatalf("read seeded row: %v", err)
+	}
+
+	update := existing
+	update.Protocol = model.MTProto
+	update.Settings = `{"clients":[{"email":"vn-c","enable":true,"secret":"ee0123456789abcdef0123456789abcdef"}]}`
+	if _, _, err := (&InboundService{}).UpdateInbound(&update); err == nil ||
+		!strings.Contains(err.Error(), "cannot be assigned to a node") {
+		t.Fatalf("err = %v, want a node-eligibility refusal", err)
+	}
+}