diff --git a/internal/web/runtime/remote.go b/internal/web/runtime/remote.go index b6c8cef6d..8f8cc7246 100644 --- a/internal/web/runtime/remote.go +++ b/internal/web/runtime/remote.go @@ -407,6 +407,11 @@ func (r *Remote) refreshRemoteIDs(ctx context.Context) error { // 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 { + // A tag the node reports itself is authoritative; the alias only fills + // the gap left for a central tag the node knows under another name. + if _, reported := next[centralTag]; reported { + continue + } if id, ok := next[nodeTag]; ok { next[centralTag] = id } diff --git a/internal/web/runtime/remote_test.go b/internal/web/runtime/remote_test.go index bd0916eaa..8b152d2b7 100644 --- a/internal/web/runtime/remote_test.go +++ b/internal/web/runtime/remote_test.go @@ -441,9 +441,8 @@ 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. +// refreshRemoteIDs rebuilds the cache from node-reported tags only, so an +// adopted 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") @@ -472,3 +471,33 @@ func TestRemoteAdoptedAliasSurvivesRefresh(t *testing.T) { t.Fatalf("adopted alias resolved to %d, want 5", id) } } + +// A stale alias must never outrank the node's own report: once the node lists +// an inbound under the central tag itself, that id is the authoritative one. +func TestRemoteAdoptedAliasYieldsToNodeReportedTag(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":"central-in"},{"id":7,"tag":"legacy-in"},{"id":9,"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: 7, Tag: "legacy-in"}) + + 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): %v", central.Tag, err) + } + if id != 5 { + t.Fatalf("central tag resolved to %d via a stale alias, want 5 (the id the node reports)", id) + } +}