Summary
GET /api/analytics/topology returns nodes listed in nodeBlacklist. The response filter exists but never removes anything, so a blacklisted node's pubkey and name remain in the topology analytics.
Root cause
filterBlacklistedFromTopology (cmd/server/routes.go) type-asserts each part of the response to a typed slice: []TopRepeater, []TopPair, []BestPathEntry, []MultiObsNode and map[string]*ObserverReach.
computeAnalyticsTopology (cmd/server/store.go) builds those parts as []map[string]interface{} and map[string]interface{}. Every assertion therefore fails, each branch is silently skipped, and the data is returned unfiltered. No server test covers the blacklist on this route.
A related hazard: the handler receives the shared cached object, either the steady-state recomputer snapshot or topoCache, and the filter writes its result back into that map. A filter that did match would modify the cached data in place for every later request and race with concurrent readers.
Affected endpoint and fields
/api/analytics/topology, both the default steady-state snapshot and the region/area/window queries. Five parts carry node pubkeys:
topRepeaters[].pubkey
topPairs[].pubkeyA / topPairs[].pubkeyB
bestPathList[].pubkey
multiObsNodes[].pubkey
perObserverReach{}.rings[].nodes[].pubkey
The same entries also carry the node's name.
Privacy impact
An operator who blacklists a node expects it to be hidden from the API. It stays hidden on the node endpoints, but the topology analytics still reveal its pubkey, name, repeater rank, pairings and per-observer reachability.
Reproduction
- Pick a repeater that appears in
/api/analytics/topology.
- Add its pubkey to
nodeBlacklist and restart.
- The node is gone from
/api/nodes/<pubkey>, but /api/analytics/topology still lists it in the parts above.
The QA script qa/scripts/blacklist-test.sh reports this as hide-failed: /api/analytics/topology lists the blacklisted pubkey when the test node is present in the topology. A handler test over data built by computeAnalyticsTopology, with a node that is present in all five parts before blacklisting, fails on current master (1ee44d72): the node is still present in every part afterwards.
Expected behaviour (fail closed)
- A blacklisted node is absent from all five parts. It is matched the way every other privacy check matches (
Config.IsBlacklisted: trimmed, case-insensitive). A pair is dropped if either side is blacklisted.
- The existing name-based hiding (
HiddenNamePrefixes) keeps applying to the same entries.
- A part or entry the filter cannot interpret (unknown shape, pubkey of an unexpected type) is dropped, never passed through.
- The shared cached topology is never modified. Filtering works on a copy, and a blacklist change takes effect on the next response without stale data.
- Responses for nodes that are not blacklisted are unchanged, and the topology computation itself is untouched.
Acceptance criteria
- A handler test over the real
computeAnalyticsTopology output proves the target is present before blacklisting and absent from all five parts after. It covers lower-, upper- and mixed-case and padded entries, multiple blacklisted nodes, and both sides of a pair.
- Tests cover an empty or missing blacklist (response unchanged), a missing single part, unknown shapes (fail closed), no mutation of the cached object or recomputer snapshot, cold and warm requests, and concurrent requests, blacklist changes and cache invalidation under
-race.
qa/scripts/blacklist-test.sh passes against a build with the fix, and reports hide-failed against one without it, for a test node that is present in the topology.
Summary
GET /api/analytics/topologyreturns nodes listed innodeBlacklist. The response filter exists but never removes anything, so a blacklisted node's pubkey and name remain in the topology analytics.Root cause
filterBlacklistedFromTopology(cmd/server/routes.go) type-asserts each part of the response to a typed slice:[]TopRepeater,[]TopPair,[]BestPathEntry,[]MultiObsNodeandmap[string]*ObserverReach.computeAnalyticsTopology(cmd/server/store.go) builds those parts as[]map[string]interface{}andmap[string]interface{}. Every assertion therefore fails, each branch is silently skipped, and the data is returned unfiltered. No server test covers the blacklist on this route.A related hazard: the handler receives the shared cached object, either the steady-state recomputer snapshot or
topoCache, and the filter writes its result back into that map. A filter that did match would modify the cached data in place for every later request and race with concurrent readers.Affected endpoint and fields
/api/analytics/topology, both the default steady-state snapshot and the region/area/window queries. Five parts carry node pubkeys:topRepeaters[].pubkeytopPairs[].pubkeyA/topPairs[].pubkeyBbestPathList[].pubkeymultiObsNodes[].pubkeyperObserverReach{}.rings[].nodes[].pubkeyThe same entries also carry the node's name.
Privacy impact
An operator who blacklists a node expects it to be hidden from the API. It stays hidden on the node endpoints, but the topology analytics still reveal its pubkey, name, repeater rank, pairings and per-observer reachability.
Reproduction
/api/analytics/topology.nodeBlacklistand restart./api/nodes/<pubkey>, but/api/analytics/topologystill lists it in the parts above.The QA script
qa/scripts/blacklist-test.shreports this ashide-failed: /api/analytics/topology lists the blacklisted pubkeywhen the test node is present in the topology. A handler test over data built bycomputeAnalyticsTopology, with a node that is present in all five parts before blacklisting, fails on current master (1ee44d72): the node is still present in every part afterwards.Expected behaviour (fail closed)
Config.IsBlacklisted: trimmed, case-insensitive). A pair is dropped if either side is blacklisted.HiddenNamePrefixes) keeps applying to the same entries.Acceptance criteria
computeAnalyticsTopologyoutput proves the target is present before blacklisting and absent from all five parts after. It covers lower-, upper- and mixed-case and padded entries, multiple blacklisted nodes, and both sides of a pair.-race.qa/scripts/blacklist-test.shpasses against a build with the fix, and reportshide-failedagainst one without it, for a test node that is present in the topology.