diff --git a/docs/API_Documentation.md b/docs/API_Documentation.md index 564c135..a037081 100644 --- a/docs/API_Documentation.md +++ b/docs/API_Documentation.md @@ -203,6 +203,10 @@ Returns network edges (connections between nodes) based on traceroutes and neigh Traceroute edges are collected over the last 12 hours. Neighbor edges are based on port 71 packets. +A completed traceroute is observed as a response packet, whose `from`/`to` are reversed +with respect to the traced path, so edges are built as `to -> route[] -> from`. Unknown +hop placeholders (`0xFFFFFFFF`) are skipped. + Query Parameters - `type` (optional, string): `traceroute` or `neighbor`. If omitted, returns both. - `node_id` (optional, int): Filter edges to only those touching a node. @@ -324,13 +328,21 @@ Returns traceroute details and derived paths for a packet. Path Parameters - `packet_id` (required, int): Packet ID. +A completed traceroute is observed as a response packet, whose `from`/`to` are reversed +with respect to the traced path. `initiator` and `target` resolve this: the forward path +runs `initiator -> forward_hops -> target` and the return path runs +`target -> reverse_hops -> initiator`, in both directions of travel. `forward_hops` and +`reverse_hops` contain only the intermediate hops, never the endpoints. + Response Example ```json { "packet": { "id": 123, - "from": 111, - "to": 222, + "from": 222, + "to": 111, + "initiator": 111, + "target": 222, "channel": "main" }, "traceroute_packets": [ @@ -338,19 +350,20 @@ Response Example "index": 0, "gateway_node_id": 333, "done": true, - "forward_hops": [111, 444, 222], - "reverse_hops": [222, 444, 111] + "forward_hops": [444, 555], + "reverse_hops": [555, 444] } ], "unique_forward_paths": [ - { "path": [111, 444, 222], "count": 2 } + { "path": [444, 555], "count": 2 } ], "unique_reverse_paths": [ - [222, 444, 111] + [555, 444] ], - "winning_paths": [ - [111, 444, 222] - ] + "winning_paths": { + "forward": [[111, 444, 555, 222]], + "reverse": [[222, 555, 444, 111]] + } } ``` diff --git a/meshview/templates/traceroute.html b/meshview/templates/traceroute.html index 84aa833..25946f4 100644 --- a/meshview/templates/traceroute.html +++ b/meshview/templates/traceroute.html @@ -129,7 +129,7 @@ function uniqueForwardPaths(data, originId, targetId) { function uniqueReversePaths(data, originId, targetId) { return (data?.unique_reverse_paths || []) - .map(path => addMissingEndpoints(path || [], targetId, null)) + .map(path => addMissingEndpoints(path || [], targetId, originId)) .filter(path => path.length > 1); } @@ -157,8 +157,9 @@ async function loadTraceroute() { const nodes = new Map(); const edges = []; - const originId = data?.packet?.from != null ? String(data.packet.from) : null; - const targetId = data?.packet?.to != null ? String(data.packet.to) : null; + // A response packet carries from/to reversed, so use the endpoints the API resolved. + const originId = data?.packet?.initiator != null ? String(data.packet.initiator) : null; + const targetId = data?.packet?.target != null ? String(data.packet.target) : null; const forwardPaths = (data?.winning_paths?.forward || []).map(path => path.map(id => String(id))); const reversePaths = (data?.winning_paths?.reverse || []).map(path => path.map(id => String(id))); const winningPathKeys = new Set([ diff --git a/meshview/traceroute.py b/meshview/traceroute.py new file mode 100644 index 0000000..2a76b05 --- /dev/null +++ b/meshview/traceroute.py @@ -0,0 +1,38 @@ +"""Assemble node paths from Meshtastic RouteDiscovery payloads. + +A traceroute response carries the endpoints reversed with respect to the path +being traced: `to` is the node that started the traceroute and `from` is the node +that answered. Only the response direction is affected, so both orientations are +handled here rather than at each call site. +""" + +# Firmware inserts this into route[] as a placeholder for a hop it could not identify. +NODENUM_BROADCAST = 0xFFFFFFFF + + +def strip_unknown_hops(hops): + return [hop for hop in hops if hop != NODENUM_BROADCAST] + + +def forward_path(from_node_id, to_node_id, hops, done, gateway_node_id=None): + """Path towards the traced node, starting at the node that began the traceroute.""" + if done: + return [node for node in (to_node_id, *hops, from_node_id) if node is not None] + + path = [node for node in (from_node_id, *hops) if node is not None] + # Some nodes add themselves to the route before uplinking. + if gateway_node_id is not None and (not path or path[-1] != gateway_node_id): + path.append(gateway_node_id) + return path + + +def return_path(from_node_id, to_node_id, hops): + """Path back from the traced node, starting at the node that answered.""" + return [node for node in (from_node_id, *hops, to_node_id) if node is not None] + + +def endpoints(from_node_id, to_node_id, done): + """The (initiator, target) of the traceroute, regardless of packet direction.""" + if done: + return to_node_id, from_node_id + return from_node_id, to_node_id diff --git a/meshview/web.py b/meshview/web.py index 03f61fd..272321d 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -17,7 +17,7 @@ from jinja2 import Environment, PackageLoader, Undefined, select_autoescape from markupsafe import Markup from meshtastic.protobuf.portnums_pb2 import PortNum -from meshview import config, database, decode_payload, migrations, models, store +from meshview import config, database, decode_payload, migrations, models, store, traceroute from meshview.__version__ import ( __version_string__, ) @@ -395,14 +395,17 @@ async def graph_traceroute(request): route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route) if route is None: continue - path = [packet.from_node_id] - path.extend(route.route) + path = traceroute.forward_path( + packet.from_node_id, + packet.to_node_id, + route.route, + tr.done, + tr.gateway_node_id, + ) + if not path: + continue if tr.done: - dest = packet.to_node_id - path.append(packet.to_node_id) - elif path[-1] != tr.gateway_node_id: - # It seems some nodes add them self to the list before uplinking - path.append(tr.gateway_node_id) + dest = path[-1] if not tr.done and tr.gateway_node_id not in node_seen_time and tr.import_time_us: node_seen_time[path[-1]] = tr.import_time_us diff --git a/meshview/web_api/api.py b/meshview/web_api/api.py index e4dbe66..1a47a24 100644 --- a/meshview/web_api/api.py +++ b/meshview/web_api/api.py @@ -10,7 +10,7 @@ from aiohttp import web from sqlalchemy import func, select from meshtastic.protobuf.portnums_pb2 import PortNum -from meshview import database, decode_payload, store +from meshview import database, decode_payload, store, traceroute from meshview.__version__ import __version__, _git_revision_short, get_version_info from meshview.config import CONFIG from meshview.models import DailySnapshot, Node @@ -630,8 +630,13 @@ async def api_edges(request): if route is None or tr.packet is None: continue - path = [tr.packet.from_node_id] + list(route.route) - path.append(tr.packet.to_node_id if tr.done else tr.gateway_node_id) + path = traceroute.forward_path( + tr.packet.from_node_id, + tr.packet.to_node_id, + traceroute.strip_unknown_hops(route.route), + tr.done, + tr.gateway_node_id, + ) for a, b in zip(path, path[1:], strict=False): if (a, b) not in edges: @@ -989,21 +994,16 @@ async def api_traceroute(request): from_node_id = packet.from_node_id to_node_id = packet.to_node_id - winning_forward_with_endpoints = [] - for path in set(winning_forward_paths): - full_path = list(path) - if to_node_id is not None and (not full_path or full_path[-1] != to_node_id): - full_path = [to_node_id, *full_path] - if from_node_id is not None and (not full_path or full_path[-1] != from_node_id): - full_path = [*full_path, from_node_id] - winning_forward_with_endpoints.append(full_path) + # Winning paths are only built from responses, which carry the endpoints reversed. + winning_forward_with_endpoints = [ + traceroute.forward_path(from_node_id, to_node_id, path, done=True) + for path in set(winning_forward_paths) + ] - winning_reverse_with_endpoints = [] - for path in set(winning_reverse_paths): - full_path = list(path) - if to_node_id is not None and (not full_path or full_path[0] != to_node_id): - full_path = [to_node_id, *full_path] - winning_reverse_with_endpoints.append(full_path) + winning_reverse_with_endpoints = [ + traceroute.return_path(from_node_id, to_node_id, path) + for path in set(winning_reverse_paths) + ] winning_paths_json = { "forward": winning_forward_with_endpoints, @@ -1013,12 +1013,17 @@ async def api_traceroute(request): # -------------------------------------------- # Final API output # -------------------------------------------- + initiator, target = traceroute.endpoints( + from_node_id, to_node_id, any(tr["done"] for tr in tr_groups) + ) return web.json_response( { "packet": { "id": packet.id, "from": packet.from_node_id, "to": packet.to_node_id, + "initiator": initiator, + "target": target, "channel": packet.channel, }, "traceroute_packets": tr_groups,