ip_geo: never let cf-trace's own ip leak into a target lookup

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.
This commit is contained in:
Andrew Pimenov 2026-08-09 03:52:25 +03:00
parent 5e4aa8b101
commit 5da12ae629
2 changed files with 40 additions and 1 deletions

View file

@ -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),

View file

@ -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"