From 0775fcaad2ed9129916f1147487b2f4c3f71c782 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Fri, 4 Sep 2026 02:35:02 +0200 Subject: [PATCH] fix(node): keep an adopted inbound alias across a remote id cache refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AdoptInboundAlias maps a central tag onto a node inbound that carries a different name, recording the pairing in both remoteIDByTag and adoptedAliases. refreshRemoteIDs then rebuilt remoteIDByTag from the tags the node reports and nothing else, so the central-tag entry was dropped on the next cache miss for any other tag. After that every op on the adopted inbound failed to resolve, and UpdateInbound falls back to AddInbound — creating a duplicate inbound on the node at the same port. cacheGetTag only recovers an n- prefix flip, never an arbitrary alias, so the pairing could not be rediscovered until a master restart. The rebuild now re-applies adoptedAliases onto the fresh map, which keeps the map the single place a tag is resolved from. --- internal/web/runtime/remote.go | 7 +++++++ internal/web/runtime/remote_test.go | 32 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/internal/web/runtime/remote.go b/internal/web/runtime/remote.go index 594af642d..b6c8cef6d 100644 --- a/internal/web/runtime/remote.go +++ b/internal/web/runtime/remote.go @@ -404,6 +404,13 @@ func (r *Remote) refreshRemoteIDs(ctx context.Context) error { next[ib.Tag] = ib.Id } r.mu.Lock() + // A rebuild sees only node-reported tags, so the adopted aliases must be + // re-applied or a later op on an adopted inbound re-creates it as a duplicate. + for centralTag, nodeTag := range r.adoptedAliases { + if id, ok := next[nodeTag]; ok { + next[centralTag] = id + } + } r.remoteIDByTag = next r.mu.Unlock() return nil diff --git a/internal/web/runtime/remote_test.go b/internal/web/runtime/remote_test.go index a97073006..bd0916eaa 100644 --- a/internal/web/runtime/remote_test.go +++ b/internal/web/runtime/remote_test.go @@ -440,3 +440,35 @@ func TestSanitizeStreamSettingsForRemote(t *testing.T) { }) } } + +// An adopted alias maps a central tag onto a differently-named node inbound. +// refreshRemoteIDs rebuilds the cache from node-reported tags only, so the +// alias must be re-applied or every later op on that inbound misses. +func TestRemoteAdoptedAliasSurvivesRefresh(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + if req.URL.Path == "/panel/api/inbounds/list" { + _, _ = w.Write([]byte(`{"success":true,"obj":[{"id":5,"tag":"legacy-in"},{"id":6,"tag":"in-2"}]}`)) + return + } + http.NotFound(w, req) + })) + defer srv.Close() + + r := NewRemote(nodeForPlainServer(t, srv, "verify", "tok"), nil) + central := &model.Inbound{Tag: "central-in", Settings: `{"clients":[]}`} + r.AdoptInboundAlias(central, RemoteInboundOption{Id: 5, Tag: "legacy-in"}) + + // Resolving a different tag misses the cache and forces a full refresh. + if _, err := r.resolveRemoteID(context.Background(), "in-2"); err != nil { + t.Fatalf("resolveRemoteID(in-2): %v", err) + } + + id, err := r.resolveRemoteID(context.Background(), central.Tag) + if err != nil { + t.Fatalf("resolveRemoteID(%s) after refresh: %v", central.Tag, err) + } + if id != 5 { + t.Fatalf("adopted alias resolved to %d, want 5", id) + } +}