'--target AS<n>' still describes this machine's own egress (an asn
target isn't something ip_geo can look up directly), same as auto
mode, but the ipv4/ipv6 forcing added for the egress_v4 mislabel fix
only checked target_kind is None. widen it to also cover 'asn' so
target-mode runs get the same protocol-pinned identity lookup as
auto mode instead of falling back to the unpinned shared client.
target mode's filename fell back to the local machine's egress asn
unconditionally, so 'netcheck --target AS15169' wrote report_<local-asn>_*
instead of report_AS15169_*. meta.target already carries the right
subject when one was given; prefer it and fall back to the egress asn
only in auto mode.
a hop with no icmp reply has ip=None; the sustained-loss finding
formatted it straight into the report text as the literal string
'(None)'. seen live in a --quick smoke test report.
the shared http client has no address family pinned, so on a dual-stack
host the 'egress_v4' gather could connect over ipv6 (os/happy-eyeballs
preference) and hand back an ipv6 literal labeled as egress_v4, with a
mismatched ip_version pulled from a different provider's own record.
mirror the existing egress_v6 forcing so each slot can only resolve
its own protocol. caught live during the --quick smoke test: the
report was analyzing an unrelated ipv6 asn instead of the real isp.
time.time() - path.stat().st_mtime went negative under load when NTFS
stamped a write's mtime slightly ahead of the wall clock read a moment
later, making a ttl_hours=0 cache lookup serve stale data instead of
refetching. surfaced as a flaky failure in test_cached_json_refetches_once_the_entry_expires
during the full suite run before task 40's smoke test.
subprocess_seconds was being applied per hop instead of as a total trace
budget, so a path with several non-responding hops could block for up to
max_hops * subprocess_seconds. The blocking call also ran on a
ThreadPoolExecutor worker via asyncio.to_thread, which concurrent.futures
joins at process exit - so an orphaned hop stuck past its timeout kept the
whole CLI alive long after diagnose() had already written its report.
Divide the budget across hops (capped at 4s per hop) with an early exit
once the total deadline passes, and dispatch the call on a raw daemon
thread instead so it can never hold the process open even in the worst
case.