mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 03:54:46 +03:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user