mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-11 01:02:10 +03:00
fix false-positive sustained-loss verdict on long runs of silent hops
path_findings() flagged ICMP rate limiting as real loss whenever two consecutive traceroute hops both went silent, but real transit paths routinely have long runs of silent hops (10 in a row, confirmed on a live --full run) while the destination itself answers cleanly. Only flag loss that persists all the way to the last hop in the trace; a run that clears before the end is now ignored regardless of how many hops it spans.
This commit is contained in:
parent
4ffc39906b
commit
4888862305
2 changed files with 35 additions and 3 deletions
|
|
@ -131,11 +131,13 @@ def path_findings(trace: TraceResult) -> list[Finding]:
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
findings: list[Finding] = []
|
findings: list[Finding] = []
|
||||||
|
# A run of lossy hops that clears before the last hop we have data for is ICMP
|
||||||
|
# rate limiting on those routers, not a real problem, no matter how many hops
|
||||||
|
# in a row stayed silent; only loss that persists all the way to the end is real.
|
||||||
|
loss_persists_to_the_end = trace.hops[-1].loss_pct >= 20.0
|
||||||
for index, hop in enumerate(trace.hops[:-1]):
|
for index, hop in enumerate(trace.hops[:-1]):
|
||||||
following = trace.hops[index + 1]
|
following = trace.hops[index + 1]
|
||||||
# A single lossy hop with clean hops after it is ICMP rate limiting on that
|
if loss_persists_to_the_end and hop.loss_pct >= 20.0 and following.loss_pct >= 20.0:
|
||||||
# router, not a real problem; loss that persists to the next hop is real.
|
|
||||||
if hop.loss_pct >= 20.0 and following.loss_pct >= 20.0:
|
|
||||||
findings.append(
|
findings.append(
|
||||||
Finding(
|
Finding(
|
||||||
id="path.loss_jump",
|
id="path.loss_jump",
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,36 @@ def test_path_findings_report_an_incomplete_trace():
|
||||||
assert [f.id for f in path_findings(trace)] == ["path.incomplete"]
|
assert [f.id for f in path_findings(trace)] == ["path.incomplete"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_findings_ignore_a_long_run_of_silent_hops_that_recovers_at_the_destination():
|
||||||
|
# Real transit routers commonly rate-limit ICMP for many consecutive hops in a
|
||||||
|
# row (not just one) while the actual destination still answers cleanly.
|
||||||
|
trace = TraceResult(
|
||||||
|
target="8.8.8.8",
|
||||||
|
resolved_ip="8.8.8.8",
|
||||||
|
backend="icmp_win",
|
||||||
|
hops=[TraceHop(ttl=1, ip="192.168.1.1", loss_pct=0.0, avg_ms=1.0)]
|
||||||
|
+ [TraceHop(ttl=n, ip=None, loss_pct=100.0, avg_ms=None) for n in range(2, 11)]
|
||||||
|
+ [TraceHop(ttl=11, ip="8.8.8.8", loss_pct=0.0, avg_ms=7.0)],
|
||||||
|
completed=True,
|
||||||
|
)
|
||||||
|
assert [f.id for f in path_findings(trace)] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_findings_flag_loss_that_persists_all_the_way_to_the_last_hop():
|
||||||
|
trace = TraceResult(
|
||||||
|
target="8.8.8.8",
|
||||||
|
resolved_ip="8.8.8.8",
|
||||||
|
backend="icmp_win",
|
||||||
|
hops=[TraceHop(ttl=1, ip="192.168.1.1", loss_pct=0.0, avg_ms=1.0)]
|
||||||
|
+ [TraceHop(ttl=n, ip=None, loss_pct=100.0, avg_ms=None) for n in range(2, 11)],
|
||||||
|
completed=False,
|
||||||
|
)
|
||||||
|
findings = path_findings(trace)
|
||||||
|
loss = [f for f in findings if f.id == "path.loss_jump"]
|
||||||
|
assert len(loss) == 1
|
||||||
|
assert "hop 2" in loss[0].detail
|
||||||
|
|
||||||
|
|
||||||
from netsleuth.config import VpnBands
|
from netsleuth.config import VpnBands
|
||||||
from netsleuth.models import AdapterLeakResult, CfTrace, DnsLeak, IpGeo, LocalNet, Signal
|
from netsleuth.models import AdapterLeakResult, CfTrace, DnsLeak, IpGeo, LocalNet, Signal
|
||||||
from netsleuth.interpret import SIGNAL_WEIGHTS, assess_vpn, gather_vpn_signals, score_vpn
|
from netsleuth.interpret import SIGNAL_WEIGHTS, assess_vpn, gather_vpn_signals, score_vpn
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue