From 168cd14f5124ba4427d331be8ba4655875657f36 Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sun, 9 Aug 2026 11:27:20 +0300 Subject: [PATCH] ip_geo: russian dual-stack mismatch note alongside english --- src/netcheck/cli.py | 4 +++- src/netcheck/exporter.py | 5 +++-- src/netcheck/ip_geo.py | 9 +++++++-- tests/test_ip_geo_normalize.py | 12 ++++++++---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/netcheck/cli.py b/src/netcheck/cli.py index 2906f25..f8ad7e3 100644 --- a/src/netcheck/cli.py +++ b/src/netcheck/cli.py @@ -136,11 +136,13 @@ async def _identity(client: httpx.AsyncClient, settings: Settings, options: Opti v6 = v6_merged if v6_merged.ip else None except (httpx.HTTPError, OSError): v6 = None + dual_stack = dual_stack_mismatch(merged, v6) return { "egress_v4": merged, "egress_v6": v6, "cf_trace": cf, - "dual_stack_note": dual_stack_mismatch(merged, v6), + "dual_stack_note": dual_stack[0] if dual_stack else None, + "dual_stack_note_ru": dual_stack[1] if dual_stack else None, "_flags": flags, } diff --git a/src/netcheck/exporter.py b/src/netcheck/exporter.py index 67a22d9..4af672d 100644 --- a/src/netcheck/exporter.py +++ b/src/netcheck/exporter.py @@ -315,8 +315,9 @@ def _connection(report: dict, lang: str = "en") -> list[str]: [_t(lang, "Cloudflare colo", "Дата-центр Cloudflare"), cf.get("colo") or "—"], ] lines = [title, ""] + _table([_t(lang, "Field", "Поле"), _t(lang, "Value", "Значение")], rows, lang) + [""] - if geo_bundle.get("dual_stack_note"): - lines += [f"> {geo_bundle['dual_stack_note']}", ""] + note = geo_bundle.get("dual_stack_note_ru" if lang == "ru" else "dual_stack_note") + if note: + lines += [f"> {note}", ""] return lines diff --git a/src/netcheck/ip_geo.py b/src/netcheck/ip_geo.py index 25faae3..edf2272 100644 --- a/src/netcheck/ip_geo.py +++ b/src/netcheck/ip_geo.py @@ -194,15 +194,20 @@ def merge_geo(candidates: list[tuple[str, IpGeo]]) -> IpGeo: return merged -def dual_stack_mismatch(v4: IpGeo | None, v6: IpGeo | None) -> str | None: +def dual_stack_mismatch(v4: IpGeo | None, v6: IpGeo | None) -> tuple[str, str] | None: if v4 is None or v6 is None or not v4.asn or not v6.asn: return None if v4.asn == v6.asn and (v4.country_code or "") == (v6.country_code or ""): return None - return ( + en = ( f"IPv4 egress {v4.ip} is {v4.asn} ({v4.country_code}) but IPv6 egress {v6.ip} " f"is {v6.asn} ({v6.country_code}); the two stacks leave through different networks." ) + ru = ( + f"Исходящий IPv4 {v4.ip} — это {v4.asn} ({v4.country_code}), а исходящий IPv6 {v6.ip} — " + f"{v6.asn} ({v6.country_code}); два стека выходят через разные сети." + ) + return en, ru import asyncio diff --git a/tests/test_ip_geo_normalize.py b/tests/test_ip_geo_normalize.py index e355bdc..6de54d7 100644 --- a/tests/test_ip_geo_normalize.py +++ b/tests/test_ip_geo_normalize.py @@ -183,10 +183,14 @@ def test_merge_ignores_a_provider_that_returned_nothing(): def test_dual_stack_mismatch_is_reported_not_resolved(): v4 = IpGeo(ip="203.0.113.44", asn="AS64500", country_code="NL") v6 = IpGeo(ip="2001:db8::1", asn="AS64777", country_code="DE") - note = dual_stack_mismatch(v4, v6) - assert note is not None - assert "AS64500" in note - assert "AS64777" in note + pair = dual_stack_mismatch(v4, v6) + assert pair is not None + en, ru = pair + assert "AS64500" in en + assert "AS64777" in en + assert "AS64500" in ru + assert "AS64777" in ru + assert any(ch in ru for ch in "абвгдеёжзийклмнопрстуфхцчшщъыьэюя") def test_dual_stack_agreement_produces_no_note():