From 4b0e9f9b6032fa9f4ddd144f6f042ab73792d371 Mon Sep 17 00:00:00 2001 From: n0ctal <4c866w5fn9@privaterelay.appleid.com> Date: Sun, 16 Aug 2026 17:02:50 +0500 Subject: [PATCH] fix(nodes): log the inbound the node snapshot removes centrally (#6219) The orphan sweep deletes a central inbound and the traffic history of every client on it, but wrote nothing. An inbound that vanishes minutes after being created is then indistinguishable from one that never arrived, and the only way to tell them apart is reading the source. Name the node, tag, id and port so the removal is visible in the panel log. Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com> --- internal/web/service/inbound_node.go | 3 ++ .../service/inbound_node_sweep_log_test.go | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 internal/web/service/inbound_node_sweep_log_test.go diff --git a/internal/web/service/inbound_node.go b/internal/web/service/inbound_node.go index ec6b1aae5..13acdbb37 100644 --- a/internal/web/service/inbound_node.go +++ b/internal/web/service/inbound_node.go @@ -729,6 +729,9 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi if unmanagedTag(c.Tag) { continue } + // This drops the central inbound and its clients' traffic history, so say + // so: silent removal is indistinguishable from an inbound never arriving. + logger.Warningf("setRemoteTraffic: node %d no longer reports inbound %q (id %d, port %d) — removing it centrally", nodeID, c.Tag, c.Id, c.Port) var goneEmails []string if err := tx.Model(xray.ClientTraffic{}). Where("inbound_id = ?", c.Id). diff --git a/internal/web/service/inbound_node_sweep_log_test.go b/internal/web/service/inbound_node_sweep_log_test.go new file mode 100644 index 000000000..b8c0192c1 --- /dev/null +++ b/internal/web/service/inbound_node_sweep_log_test.go @@ -0,0 +1,50 @@ +package service + +import ( + "strconv" + "strings" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/logger" + "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" + + "github.com/op/go-logging" +) + +// Removing a central inbound also drops its clients' traffic history, so the +// sweep must name what it removed — a silent drop is undiagnosable in the field. +func TestNodeSnapshotSweepLogsRemovedInbound(t *testing.T) { + logger.InitLogger(logging.WARNING) + setupBulkDB(t) + nodeID, _ := setupNodeRuntime(t) + + gone := nodeInbound(t, nodeID, 31777, nil) + survivor := nodeInbound(t, nodeID, 31778, nil) + + // The snapshot reports only the survivor, so the other row is swept. + snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{ + Tag: survivor.Tag, Port: survivor.Port, Protocol: model.VLESS, Enable: true, + Settings: survivor.Settings, + }}} + if _, err := (&InboundService{}).setRemoteTrafficLocked(nodeID, snap, false); err != nil { + t.Fatalf("setRemoteTrafficLocked: %v", err) + } + + var remaining int64 + if err := database.GetDB().Model(model.Inbound{}).Where("id = ?", gone.Id).Count(&remaining).Error; err != nil { + t.Fatalf("count swept inbound: %v", err) + } + if remaining != 0 { + t.Fatalf("fixture did not sweep inbound %d; the log assertion below would be meaningless", gone.Id) + } + + want := strconv.Itoa(gone.Id) + for _, line := range logger.GetLogs(200, "warning") { + if strings.Contains(line, gone.Tag) && strings.Contains(line, want) { + return + } + } + t.Fatalf("sweep removed inbound %q (id %d) without naming it in the log", gone.Tag, gone.Id) +}