diff --git a/internal/web/service/inbound_node.go b/internal/web/service/inbound_node.go index 775fa6773..74ce45f26 100644 --- a/internal/web/service/inbound_node.go +++ b/internal/web/service/inbound_node.go @@ -173,13 +173,9 @@ func (s *InboundService) ReconcileNode(ctx context.Context, rt *runtime.Remote, // rest were never imported, so their absence from the local DB must not // delete them from the node. Only a selected tag missing locally (the // panel deleted it while the node was unreachable) may be swept. - var selected map[string]struct{} - if n.InboundSyncMode == "selected" { - selected = make(map[string]struct{}, len(n.InboundTags)) - for _, tag := range n.InboundTags { - selected[tag] = struct{}{} - } - } + // The node reports a panel-created inbound with its n- prefix stripped, + // so the selected set must carry both forms or the sweep never matches. + selected := nodeSelectedTagSet(n) for _, tag := range remoteTags { if _, want := desiredTags[tag]; want { continue diff --git a/internal/web/service/inbound_node_reconcile_test.go b/internal/web/service/inbound_node_reconcile_test.go index 9817652f1..d10c69008 100644 --- a/internal/web/service/inbound_node_reconcile_test.go +++ b/internal/web/service/inbound_node_reconcile_test.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -404,3 +405,29 @@ func TestEnsureInboundTagAllowed(t *testing.T) { t.Fatalf("all-mode node must stay without tags, got %#v", gotAll.InboundTags) } } + +// A panel-created node inbound is stored as "n-tag" and pushed to the node +// with the prefix stripped, so the sweep's selected set must match both forms. +func TestReconcileNode_SelectedModeSweepsPrefixedSelectedTag(t *testing.T) { + setupConflictDB(t) + + ts, deletedIDs := fakeNodePanel(t, map[string]int{ + "keep": 1, + "selected-gone": 2, + "unmanaged": 3, + }) + node := reconcileTestNode(t, ts, "sel-prefix-node", "selected", nil) + prefix := fmt.Sprintf("n%d-", node.Id) + node.InboundTags = []string{prefix + "keep", prefix + "selected-gone"} + seedInboundConflictNode(t, prefix+"keep", "", 443, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`, &node.Id) + + svc := InboundService{} + if err := svc.ReconcileNode(context.Background(), runtime.NewRemote(node, nil), node); err != nil { + t.Fatalf("ReconcileNode: %v", err) + } + + got := deletedIDs() + if len(got) != 1 || got[0] != 2 { + t.Fatalf("deleted remote ids = %v, want [2] (prefixed selected tag must be swept, unmanaged 3 must survive)", got) + } +}