diff --git a/tradein-mvp/browser/server.py b/tradein-mvp/browser/server.py index 71de6501..1b9d98ea 100644 --- a/tradein-mvp/browser/server.py +++ b/tradein-mvp/browser/server.py @@ -895,10 +895,16 @@ async def _launch_browser(provider: str, proxy_override: str | None = None) -> N "block_images": _BLOCK_IMAGES_BY_PROVIDER.get(provider, _BLOCK_IMAGES_DEFAULT_FALLBACK), # Анти-leak: WebRTC может слить реальный IP контейнера мимо прокси → деанон. "block_webrtc": True, - # Форсим таймзону +3 МСК. geoip берёт tz одного IP на старте, а БэкКоннект - # ротирует IP на каждый запрос (видели Ульяновск UTC+4 при выходе СПб/МСК) → - # геолик tz↔IP. Жёсткий Europe/Moscow убирает рассинхрон для РФ-мобильного. - "config": {"timezone": "Europe/Moscow"}, + # Таймзону НЕ задаём (#3187): её ставит geoip=True по exit-IP на запуске — + # из той же GeoIP-записи, что и координаты geolocation. Жёсткий + # Europe/Moscow в config (было) camoufox 0.5.5 применял поверх geoip + # (timezone — setdefault), а координаты брал из IP: для узлов вне UTC+3 + # браузер заявлял МСК при новосибирских координатах и адресе (узлы 13/15 — + # T2 NSK, UTC+7; 14 — МегаФон Самара, UTC+4 по RIPE, 17.09.2026) и писал + # LeakWarning «Please use the geoip parameter» на каждом запуске. Ротация + # узла пула меняет IP внутри той же подсети оператора (health-логи 48 ч: + # 176.59.137.x/176.59.144.x у 15, 178.176.78-79.x у 14), а смена узла + # релончит браузер (_launched_proxy) — зона, взятая на запуске, не протухает. } if proxy is not None: kwargs["proxy"] = proxy diff --git a/tradein-mvp/browser/test_server_proxy_override.py b/tradein-mvp/browser/test_server_proxy_override.py index 2ec4fdd4..dd124b6b 100644 --- a/tradein-mvp/browser/test_server_proxy_override.py +++ b/tradein-mvp/browser/test_server_proxy_override.py @@ -149,3 +149,46 @@ def test_ensure_browser_env_then_pool_relaunches(monkeypatch: pytest.MonkeyPatch asyncio.run(_run()) assert calls == [("avito", None), ("avito", "http://pool:8080")] + + +# ── #3187: гео-параметры запуска camoufox ──────────────────────────────────────── + + +def test_launch_leaves_timezone_to_geoip(monkeypatch: pytest.MonkeyPatch) -> None: + """Зона и координаты — только от geoip по exit-IP, вручную в config не задаются. + + camoufox 0.5.5 (utils.launch_options): geoip кладёт в config координаты + безусловно, а timezone — через setdefault, то есть ручной config["timezone"] + перебивает зону из IP, и координаты с зоной расходятся (узел в Новосибирске + с заявленной МСК). Ключи 'timezone' и 'geolocation:*' в config — ровно те, + на которые библиотека пишет LeakWarning (warn_manual_config). + """ + import sys + import types + + captured: dict[str, object] = {} + + class _FakeCamoufox: + def __init__(self, **kwargs: object) -> None: + captured.update(kwargs) + + async def __aenter__(self) -> object: + return object() + + fake_mod = types.ModuleType("camoufox.async_api") + fake_mod.AsyncCamoufox = _FakeCamoufox # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "camoufox", types.ModuleType("camoufox")) + monkeypatch.setitem(sys.modules, "camoufox.async_api", fake_mod) + + asyncio.run(server._launch_browser("avito", proxy_override="http://u:p@176.59.137.239:8080")) + + assert captured["geoip"] is True + assert captured["proxy"] == { + "server": "http://176.59.137.239:8080", + "username": "u", + "password": "p", + } + config = captured.get("config", {}) + assert isinstance(config, dict) + manual_geo = sorted(k for k in config if k == "timezone" or k.startswith("geolocation:")) + assert manual_geo == []