فهرست منبع

fix(node): keep disabled inbounds the node snapshot cannot report (#6221)

* fix(node): keep disabled inbounds the node snapshot cannot report

A node builds its traffic snapshot from the inbounds Xray is actually running,
so an inbound with enable=false is never in it. The central sweep reads that
absence as "the node no longer has this inbound" and deletes the row, its
clients' traffic history and its port reservation — on a perfectly healthy
node, with no way to tell it apart from a real deletion.

Disabling an inbound in the panel and waiting one sync interval is enough to
lose it. Skip disabled inbounds in the sweep: their absence carries no
information, and an explicit delete still removes them.

* chore: drop the accidentally committed dist build stub

internal/web/dist/.gitkeep is what make dist-stub creates locally. Committing
it changes fresh-clone behaviour for everyone: today a bare go build fails
loudly on //go:embed all:dist, which is the documented signal to run the stub
target; with the file present the build succeeds and the panel serves an empty
dist instead.
n0ctal 20 ساعت پیش
والد
کامیت
6a674c7f0c
2فایلهای تغییر یافته به همراه43 افزوده شده و 0 حذف شده
  1. 6 0
      internal/web/service/inbound_node.go
  2. 37 0
      internal/web/service/node_dirty_test.go

+ 6 - 0
internal/web/service/inbound_node.go

@@ -714,6 +714,12 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 		if dirty {
 		if dirty {
 			continue
 			continue
 		}
 		}
+		// Disabled inbounds are intentionally absent from the node's runtime
+		// snapshot. Their absence is not evidence of deletion; retain the row,
+		// client history and port reservation until an explicit delete occurs.
+		if !c.Enable {
+			continue
+		}
 		if len(snapTags) == 0 {
 		if len(snapTags) == 0 {
 			// A node mid-restart or with a transient DB error can return an empty
 			// A node mid-restart or with a transient DB error can return an empty
 			// inbound list with success=true. Treat "zero inbounds reported" as
 			// inbound list with success=true. Treat "zero inbounds reported" as

+ 37 - 0
internal/web/service/node_dirty_test.go

@@ -68,6 +68,43 @@ func TestSetRemoteTraffic_DirtyPreservesConfig(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestSetRemoteTraffic_MissingDisabledInboundIsNotSwept(t *testing.T) {
+	setupConflictDB(t)
+	db := database.GetDB()
+	node := &model.Node{Name: "disabled-snapshot", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
+	if err := db.Create(node).Error; err != nil {
+		t.Fatal(err)
+	}
+	disabled := &model.Inbound{
+		UserId: 1, NodeID: &node.Id, Tag: "disabled", Enable: false,
+		Port: 24443, Protocol: model.VLESS, Settings: `{"clients":[]}`,
+	}
+	reported := &model.Inbound{
+		UserId: 1, NodeID: &node.Id, Tag: "reported", Enable: true,
+		Port: 24444, Protocol: model.VLESS, Settings: `{"clients":[]}`,
+	}
+	if err := db.Create(disabled).Error; err != nil {
+		t.Fatal(err)
+	}
+	if err := db.Create(reported).Error; err != nil {
+		t.Fatal(err)
+	}
+	snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{
+		Tag: reported.Tag, Enable: true,
+		Port: reported.Port, Protocol: reported.Protocol, Settings: reported.Settings,
+	}}}
+	if _, err := (&InboundService{}).setRemoteTrafficLocked(node.Id, snap, false); err != nil {
+		t.Fatal(err)
+	}
+	var count int64
+	if err := db.Model(&model.Inbound{}).Where("id=?", disabled.Id).Count(&count).Error; err != nil {
+		t.Fatal(err)
+	}
+	if count != 1 {
+		t.Fatalf("disabled inbound rows=%d, want 1", count)
+	}
+}
+
 // Deleting a *disabled* client attached to a node inbound must still propagate
 // Deleting a *disabled* client attached to a node inbound must still propagate
 // to the node. The node's own DB carries the (disabled) client, so the central
 // to the node. The node's own DB carries the (disabled) client, so the central
 // panel has to mark the node dirty (→ reconcile) instead of dropping the delete
 // panel has to mark the node dirty (→ reconcile) instead of dropping the delete