Преглед на файлове

fix(node): sweep a selected inbound the node reports without its prefix

In "selected" sync mode the reconcile sweep built its set of managed tags
verbatim from node.InboundTags. A panel-created node inbound is stored with
an n<id>- prefix (composeInboundTag) and pushed to the node with that prefix
stripped (wireInbound), so the tag the node reports never matched the set and
the sweep skipped it.

The effect is the case the sweep exists for: an operator deletes a node
inbound while the node is offline, and the node keeps serving it — and its
clients — indefinitely. Only unprefixed tags were unaffected, which is why
the existing selected-mode test did not catch it.

nodeSelectedTagSet already builds both tag forms for exactly this reason and
is used by the snapshot filter; the sweep now uses it too, so the two agree.
Sanaei преди 20 часа
родител
ревизия
6f40a51d62
променени са 2 файла, в които са добавени 30 реда и са изтрити 7 реда
  1. 3 7
      internal/web/service/inbound_node.go
  2. 27 0
      internal/web/service/inbound_node_reconcile_test.go

+ 3 - 7
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<id>- 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

+ 27 - 0
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<id>-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)
+	}
+}