From a5f46d63601702c957b3dfa7270be480ba243cbf Mon Sep 17 00:00:00 2001 From: nullrouten0 Date: Tue, 27 Oct 2026 17:10:03 -0700 Subject: [PATCH] Bound the traceroute data a request has to draw (#182) * Bound the traceroute data a request has to draw Every TRACEROUTE message is stored as a new row under its packet id, and its route can list any number of hops. /graph/traceroute decodes every row, looks up every node on every route and runs Graphviz over the result inside the request handler, so a publisher can make one page view take a very long time and stall every other request meanwhile: - 3,000 rows of 8 random hops for one packet id: the graph took about 43 s and other requests waited up to 24 s. - 150 rows of 400 hops: about 40 s, then a 500, with other requests waiting about 10 s. Real traceroutes are small. In 17 days of bay-area traffic no packet id had more than 72 rows, and none of 66,709 routes had more than 8 hops in either direction, which is the firmware's limit. get_traceroute() now returns at most the 200 earliest rows, and the TRACEROUTE decoder treats a route longer than 8 hops as undecodable, which every caller already skips. Co-Authored-By: Claude Opus 5.5 * Remove a blank line that fails the ruff format check `ruff format --check .` (the Ruff workflow, ruff 0.13.3) fails on develop because of this line, so every pull request's lint job is red regardless of its content. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Nathan <201262769+nullrouten0@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- meshview/decode_payload.py | 15 ++++++++++++++- meshview/store.py | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/meshview/decode_payload.py b/meshview/decode_payload.py index 080a935..bba46c9 100644 --- a/meshview/decode_payload.py +++ b/meshview/decode_payload.py @@ -17,12 +17,25 @@ def text_message(payload): return payload.decode("utf-8") +# The firmware keeps at most 8 hops in each direction (none of 66,709 real traceroutes in 17 +# days of bay-area traffic had more). A longer route can only be forged, and one with +# thousands of hops makes every page that draws it slow, so treat it as undecodable. +MAX_ROUTE_HOPS = 8 + + +def route_discovery(payload): + route = RouteDiscovery.FromString(payload) + if max(len(route.route), len(route.route_back)) > MAX_ROUTE_HOPS: + return None + return route + + DECODE_MAP = { PortNum.POSITION_APP: Position.FromString, PortNum.NEIGHBORINFO_APP: NeighborInfo.FromString, PortNum.NODEINFO_APP: User.FromString, PortNum.TELEMETRY_APP: Telemetry.FromString, - PortNum.TRACEROUTE_APP: RouteDiscovery.FromString, + PortNum.TRACEROUTE_APP: route_discovery, PortNum.ROUTING_APP: Routing.FromString, PortNum.TEXT_MESSAGE_APP: text_message, PortNum.MAP_REPORT_APP: MapReport.FromString, diff --git a/meshview/store.py b/meshview/store.py index d0e0e3d..f90e265 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -152,12 +152,19 @@ async def has_packets(node_id, portnum): ) +# Every TRACEROUTE message stores a row under its packet id, so anyone who can publish can add +# thousands. Real ones have a few dozen (one per gateway that heard it; the most seen in 17 days +# of bay-area traffic was 72), so only the earliest rows are used. +MAX_TRACEROUTE_ROWS = 200 + + async def get_traceroute(packet_id): async with database.async_session() as session: result = await session.execute( select(Traceroute) .where(Traceroute.packet_id == packet_id) .order_by(Traceroute.import_time_us) + .limit(MAX_TRACEROUTE_ROWS) ) return result.scalars()