traceparse: bsd/macos parser with multipath hops and annotations

This commit is contained in:
Andrew Pimenov 2026-08-08 22:03:37 +03:00
parent 61c22b218d
commit e8e55278a7
4 changed files with 93 additions and 0 deletions

View file

@ -110,3 +110,40 @@ def parse_windows(text: str) -> list[TraceHop]:
rdns = None rdns = None
hops.append(finalize_hop(TraceHop(ttl=ttl, ip=ip, reverse_dns=rdns, probes=probes))) hops.append(finalize_hop(TraceHop(ttl=ttl, ip=ip, reverse_dns=rdns, probes=probes)))
return hops return hops
_BSD_CONTINUATION_RE = re.compile(r"^\s+\S")
def parse_darwin(text: str) -> list[TraceHop]:
hops: list[TraceHop] = []
for line in text.splitlines():
match = _HOP_LINE_RE.match(line)
if match:
ttl, body = int(match.group(1)), match.group(2)
rdns, ip = _unix_host_and_ip(body)
hops.append(
TraceHop(
ttl=ttl,
ip=ip,
reverse_dns=rdns,
probes=_unix_probes(body),
annotations=_unix_annotations(body),
)
)
continue
if not hops or not _BSD_CONTINUATION_RE.match(line):
continue
# BSD prints one continuation line per probe that came back from a
# different router than the hop's first probe.
hop = hops[-1]
_, extra_ip = _unix_host_and_ip(line)
hop.probes.extend(_unix_probes(line))
if extra_ip and extra_ip != hop.ip:
marker = f"alt:{extra_ip}"
if marker not in hop.annotations:
hop.annotations.append(marker)
for token in _unix_annotations(line):
if token not in hop.annotations:
hop.annotations.append(token)
return [finalize_hop(hop) for hop in hops]

View file

@ -0,0 +1,6 @@
traceroute to one.one.one.one (1.1.1.1), 64 hops max, 52 byte packets
1 192.168.1.1 (192.168.1.1) 2.145 ms 1.902 ms 1.870 ms
2 10.64.0.1 (10.64.0.1) 9.001 ms 8.940 ms 8.870 ms
3 * * *
4 ae-1.r01.ams.example.net (77.88.1.5) 12.400 ms 12.100 ms 12.220 ms
5 one.one.one.one (1.1.1.1) 13.900 ms 13.700 ms 13.610 ms

View file

@ -0,0 +1,8 @@
traceroute to 203.0.113.9 (203.0.113.9), 64 hops max, 52 byte packets
1 192.168.1.1 (192.168.1.1) 2.100 ms 1.900 ms 1.850 ms
2 10.64.0.1 (10.64.0.1) 9.001 ms
10.64.0.5 (10.64.0.5) 9.220 ms
10.64.0.1 (10.64.0.1) 8.870 ms
3 * 11.200 ms *
4 border.example.net (198.51.100.7) 15.100 ms !H 15.200 ms 15.050 ms
5 filtered.example.net (198.51.100.9) 16.000 ms !X 16.100 ms !X 16.200 ms !X

View file

@ -141,3 +141,45 @@ def test_windows_destination_unreachable_hop_keeps_its_ip(trace_fixture):
def test_windows_parser_tolerates_empty_input(): def test_windows_parser_tolerates_empty_input():
assert parse_windows("") == [] assert parse_windows("") == []
from netcheck.traceparse import parse_darwin
def test_darwin_basic_hops_match_the_gnu_shape(trace_fixture):
hops = parse_darwin(trace_fixture("darwin", "bsd_basic.txt"))
assert [h.ttl for h in hops] == [1, 2, 3, 4, 5]
assert hops[0].ip == "192.168.1.1"
assert hops[0].reverse_dns is None
assert hops[3].reverse_dns == "ae-1.r01.ams.example.net"
assert hops[2].probes == [None, None, None]
def test_darwin_merges_continuation_lines_into_one_hop(trace_fixture):
hops = parse_darwin(trace_fixture("darwin", "bsd_multipath.txt"))
assert [h.ttl for h in hops] == [1, 2, 3, 4, 5]
hop = hops[1]
assert hop.probes == [9.001, 9.22, 8.87]
assert hop.ip == "10.64.0.1"
def test_darwin_records_alternate_addresses_for_a_multipath_hop(trace_fixture):
hops = parse_darwin(trace_fixture("darwin", "bsd_multipath.txt"))
assert "alt:10.64.0.5" in hops[1].annotations
assert "alt:10.64.0.1" not in hops[1].annotations
def test_darwin_keeps_bsd_only_annotations(trace_fixture):
hops = parse_darwin(trace_fixture("darwin", "bsd_multipath.txt"))
assert hops[3].annotations == ["!H"]
assert hops[4].annotations == ["!X"]
def test_darwin_partial_timeout_hop_keeps_positions(trace_fixture):
hops = parse_darwin(trace_fixture("darwin", "bsd_multipath.txt"))
assert hops[2].probes == [None, 11.2, None]
assert hops[2].loss_pct == pytest.approx(66.667)
def test_darwin_parser_tolerates_empty_input():
assert parse_darwin("") == []