From 5da12ae629ec1d3601865e831580cfb684c64cfb Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sun, 9 Aug 2026 03:52:25 +0300 Subject: [PATCH] ip_geo: never let cf-trace's own ip leak into a target lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cf-trace has no way to describe an arbitrary target — it always echoes the ip of whoever is making the http request. gather_identity included it in the merge candidates unconditionally, so 'netcheck --target 8.8.8.8' spliced this machine's own connecting ip onto 8.8.8.8's asn/city/org fields from the other providers, producing a report that named an ip that was neither the target nor honestly labeled as the local egress. exclude cf-trace from the merge whenever an explicit target ip was requested; it's still fetched and returned separately for the connection section's own colo/warp fields. --- src/netcheck/ip_geo.py | 5 ++++- tests/test_ip_geo_normalize.py | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/netcheck/ip_geo.py b/src/netcheck/ip_geo.py index 363cdf6..25faae3 100644 --- a/src/netcheck/ip_geo.py +++ b/src/netcheck/ip_geo.py @@ -247,7 +247,10 @@ async def gather_identity( flags = provider_flags(payloads["ip-api"]) if isinstance(payloads["ip-api"], dict) else {} candidates: list[tuple[str, IpGeo]] = [] - if cf: + if cf and not ip: + # cf-trace can only ever describe the machine making the request, never an + # arbitrary target — folding it in during a target lookup would splice this + # host's own IP onto the target's asn/geo fields from the other providers. candidates.append(("cf-trace", IpGeo(ip=cf.ip, country_code=cf.loc, sources={}))) for name, normalizer in ( ("ip-api", normalize_ip_api), diff --git a/tests/test_ip_geo_normalize.py b/tests/test_ip_geo_normalize.py index b51eb5c..e355bdc 100644 --- a/tests/test_ip_geo_normalize.py +++ b/tests/test_ip_geo_normalize.py @@ -1,9 +1,12 @@ from __future__ import annotations +import httpx import pytest +from netcheck.config import Providers from netcheck.ip_geo import ( classify_ip_type, + gather_identity, normalize_freeipapi, normalize_ip_api, normalize_ipinfo, @@ -195,3 +198,36 @@ def test_dual_stack_agreement_produces_no_note(): def test_dual_stack_comparison_needs_both_sides(): assert dual_stack_mismatch(IpGeo(asn="AS64500"), None) is None assert dual_stack_mismatch(None, IpGeo(asn="AS64500")) is None + + +async def test_gather_identity_for_a_target_never_reports_this_hosts_own_ip(httpx_mock): + httpx_mock.add_response( + url="https://www.cloudflare.com/cdn-cgi/trace", + text="ip=203.0.113.9\nloc=NL\nwarp=off\ngateway=off\nrbi=off\n", + ) + httpx_mock.add_response( + url="http://ip-api.com/json/8.8.8.8", + json={ + "status": "success", + "query": "8.8.8.8", + "as": "AS15169 GOOGLE", + "org": "Google LLC", + "country": "United States", + "countryCode": "US", + "city": "Ashburn", + }, + ) + httpx_mock.add_response(url="https://freeipapi.com/api/json/8.8.8.8", json={}) + httpx_mock.add_response(url="https://ipinfo.io/8.8.8.8json", json={}) + httpx_mock.add_response(url="https://ipwho.is/8.8.8.8", json={"success": False}) + httpx_mock.add_response( + url="https://stat.ripe.net/data/network-info/data.json?resource=8.8.8.8", + json={"status": "ok", "data": {"asns": ["15169"]}}, + ) + + async with httpx.AsyncClient() as client: + merged, cf, _flags, _raw = await gather_identity(client, Providers(), ip="8.8.8.8") + + assert merged.ip == "8.8.8.8" + assert merged.asn == "AS15169" + assert cf is not None and cf.ip == "203.0.113.9"