From 6a674c7f0c8c3c5c031ca58600e4cb37f127018a Mon Sep 17 00:00:00 2001 From: n0ctal <4c866w5fn9@privaterelay.appleid.com> Date: Tue, 18 Aug 2026 14:50:09 +0500 Subject: [PATCH] fix(node): keep disabled inbounds the node snapshot cannot report (#6221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- internal/web/service/inbound_node.go | 6 ++++ internal/web/service/node_dirty_test.go | 37 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/internal/web/service/inbound_node.go b/internal/web/service/inbound_node.go index 13acdbb37..8f3a71d6a 100644 --- a/internal/web/service/inbound_node.go +++ b/internal/web/service/inbound_node.go @@ -714,6 +714,12 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi if dirty { 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 { // A node mid-restart or with a transient DB error can return an empty // inbound list with success=true. Treat "zero inbounds reported" as diff --git a/internal/web/service/node_dirty_test.go b/internal/web/service/node_dirty_test.go index 26f89d6e3..de4872e1d 100644 --- a/internal/web/service/node_dirty_test.go +++ b/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 // 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