tradein/proxy: запретить сбор без прокси (сейчас пустой прокси = прямое подключение с IP сервера), затем убрать мёртвые переменные mobileproxy #2616

Closed
opened 2026-08-01 20:12:32 +00:00 by lekss361 · 1 comment
Owner

Второй шаг выпиливания mobileproxy после #2614 (мёртвые узлы и их ключи из пула удалены). Порядок здесь обратный интуитивному, поэтому вынесено отдельно.

⚠️ Почему нельзя просто удалить переменные

packages/scraper-kit/src/scraper_kit/providers/_proxy.py:62-65:

if lease is None:
    # off / пул пуст / ошибка acquire → env как сейчас (сбор не ломаем).
    yield env_fallback_url
    return

env_fallback_url = None означает не отказ, а прямое подключение с IP сервера. То же в браузерном контейнере: browser/server.py:213-224, _provider_proxy() при отсутствии BROWSER_PROXY_* и legacy-переменной возвращает None, camoufox идёт напрямую.

Сейчас от этого случайно защищает сам факт, что переменные указывают на мёртвые узлы: запрос падает с ошибкой прокси. Удалить переменные первыми — значит превратить «сбор упал» в «сбор пошёл с нашего адреса», то есть засветить IP сервера перед Авито и Цианом.

Шаг 1 — fail-closed (сначала)

Отсутствие прокси в проде должно означать «не ходить», а не «идти напрямую».

⚠️ Аккуратно: в dev прямое подключение легитимно и задокументировано (providers/cian/detail.py:118 — «Пусто → прямое подключение (dev/no-op)»). Нужен признак окружения или явный флаг, а не безусловный запрет — иначе сломается локальная разработка и тесты.

Точки правки: providers/_proxy.py::curl_proxy_url, browser_fetcher.py::_pool_proxy, browser/server.py::_provider_proxy. Это горячий путь скрапера — правка рискованная, нужен эмпирический прогон, а не только тесты.

Отдельно продумать: отказ должен быть отличим в мониторинге от бана и от сетевой ошибки, иначе «нет прокси» будет выглядеть как «источник забанил» и уведёт диагностику не туда.

Шаг 2 — чистка переменных (после шага 1)

Из /opt/gendesign/tradein-mvp/backend/.env.runtime — все указывают на закрытые аккаунты, проверено пробой (407 / connection refused / invalid credentials):

AVITO_PROXY_URL         CIAN_PROXY_URL         YANDEX_PROXY_URL
BROWSER_PROXY_AVITO     BROWSER_PROXY_CIAN     BROWSER_PROXY_YANDEX
AVITO_PROXY_ROTATE_URL  CIAN_PROXY_ROTATE_URL  YANDEX_PROXY_ROTATE_URL

Оставить SCRAPER_PROXY_URL — единственный живой (резидентный ASocks 212.8.249.134:10423).

Плюс убрать соответствующие поля из app/core/config.py и упоминания из .env.example, DEPLOY.md, docker-compose.prod.yml.

Шаг 3 — удалить мёртвую ручку

POST /api/v1/admin/scraper/{source}/rotate-ip (admin.py:2377) работает по *_PROXY_ROTATE_URL, то есть по changeip-ссылкам закрытых аккаунтов. После шага 2 становится мёртвым кодом. Ротация живых узлов идёт через POST /api/v1/admin/proxies/{proxy_id}/rotate (PR #2611, в проде).

Промежуточный вариант, если нужно закрыть быстро

Не удалять переменные, а переставить их значения на живой SCRAPER_PROXY_URL. Правка только значений в .env.runtime, кода не касается, прямого подключения не возникает, горячий путь не трогается. Шаги 1 и 3 при этом остаются как долг, но перестают быть срочными.

Состояние пула после #2614

 1 | asocks-residential-1 | affinity=domclick | enabled | rotate_url → api.asocks.com
 9 | asocks-mobile-1      | affinity=any      | enabled | rotate_url → api.asocks.com
10 | asocks-mobile-2      | affinity=any      | enabled | rotate_url → api.asocks.com
11 | asocks-mobile-3      | affinity=any      | enabled | rotate_url → api.asocks.com

Секретов в rotate_url нет. Самовосстановление (#2609) вернёт узел в строй после транзиентного сбоя, защита последнего узла не даст забрать единственный домкликовский.

Связано: #2613, #2600, PR #2609, #2611, #2614.

Второй шаг выпиливания mobileproxy после #2614 (мёртвые узлы и их ключи из пула удалены). Порядок здесь обратный интуитивному, поэтому вынесено отдельно. ## ⚠️ Почему нельзя просто удалить переменные `packages/scraper-kit/src/scraper_kit/providers/_proxy.py:62-65`: ```python if lease is None: # off / пул пуст / ошибка acquire → env как сейчас (сбор не ломаем). yield env_fallback_url return ``` `env_fallback_url = None` означает не отказ, а **прямое подключение с IP сервера**. То же в браузерном контейнере: `browser/server.py:213-224`, `_provider_proxy()` при отсутствии `BROWSER_PROXY_*` и legacy-переменной возвращает `None`, camoufox идёт напрямую. Сейчас от этого случайно защищает сам факт, что переменные указывают на мёртвые узлы: запрос падает с ошибкой прокси. Удалить переменные первыми — значит превратить «сбор упал» в «сбор пошёл с нашего адреса», то есть засветить IP сервера перед Авито и Цианом. ## Шаг 1 — fail-closed (сначала) Отсутствие прокси в проде должно означать «не ходить», а не «идти напрямую». ⚠️ Аккуратно: в dev прямое подключение легитимно и задокументировано (`providers/cian/detail.py:118` — «Пусто → прямое подключение (dev/no-op)»). Нужен признак окружения или явный флаг, а не безусловный запрет — иначе сломается локальная разработка и тесты. Точки правки: `providers/_proxy.py::curl_proxy_url`, `browser_fetcher.py::_pool_proxy`, `browser/server.py::_provider_proxy`. Это **горячий путь скрапера** — правка рискованная, нужен эмпирический прогон, а не только тесты. Отдельно продумать: отказ должен быть отличим в мониторинге от бана и от сетевой ошибки, иначе «нет прокси» будет выглядеть как «источник забанил» и уведёт диагностику не туда. ## Шаг 2 — чистка переменных (после шага 1) Из `/opt/gendesign/tradein-mvp/backend/.env.runtime` — все указывают на закрытые аккаунты, проверено пробой (407 / connection refused / invalid credentials): ``` AVITO_PROXY_URL CIAN_PROXY_URL YANDEX_PROXY_URL BROWSER_PROXY_AVITO BROWSER_PROXY_CIAN BROWSER_PROXY_YANDEX AVITO_PROXY_ROTATE_URL CIAN_PROXY_ROTATE_URL YANDEX_PROXY_ROTATE_URL ``` Оставить `SCRAPER_PROXY_URL` — единственный живой (резидентный ASocks `212.8.249.134:10423`). Плюс убрать соответствующие поля из `app/core/config.py` и упоминания из `.env.example`, `DEPLOY.md`, `docker-compose.prod.yml`. ## Шаг 3 — удалить мёртвую ручку `POST /api/v1/admin/scraper/{source}/rotate-ip` (`admin.py:2377`) работает по `*_PROXY_ROTATE_URL`, то есть по changeip-ссылкам закрытых аккаунтов. После шага 2 становится мёртвым кодом. Ротация живых узлов идёт через `POST /api/v1/admin/proxies/{proxy_id}/rotate` (PR #2611, в проде). ## Промежуточный вариант, если нужно закрыть быстро Не удалять переменные, а **переставить их значения** на живой `SCRAPER_PROXY_URL`. Правка только значений в `.env.runtime`, кода не касается, прямого подключения не возникает, горячий путь не трогается. Шаги 1 и 3 при этом остаются как долг, но перестают быть срочными. ## Состояние пула после #2614 ``` 1 | asocks-residential-1 | affinity=domclick | enabled | rotate_url → api.asocks.com 9 | asocks-mobile-1 | affinity=any | enabled | rotate_url → api.asocks.com 10 | asocks-mobile-2 | affinity=any | enabled | rotate_url → api.asocks.com 11 | asocks-mobile-3 | affinity=any | enabled | rotate_url → api.asocks.com ``` Секретов в `rotate_url` нет. Самовосстановление (#2609) вернёт узел в строй после транзиентного сбоя, защита последнего узла не даст забрать единственный домкликовский. Связано: #2613, #2600, PR #2609, #2611, #2614.
Collaborator

Все три шага закрыты.

Шаг 1 (fail-closed) — PR #2634 (04.08, merged + прод-verified). Отсутствие прокси в prod = явный отказ (NoProxyAvailableError в scraper-kit, 503 в browser-сайдкаре), а не прямое подключение. Отдельно дожат по ревью: guard был мёртв для avito-путей (прямое конструирование BrowserFetcher минуя build_browser_fetcher не прокидывало environment) и для сайдкара (ENVIRONMENT не задан в compose) — исправлено, добавлен source-level тест-гард на новые construction-sites.

Шаги 2-3 — PR #2650 (merged + прод-verified). Удалены 9 мёртвых переменных из кода/доков/compose и мёртвая ручка POST /scraper/{source}/rotate-ip с её кнопками во фронте. Каскадом застаблены _rotate_proxy_ip/_rotate_ip (changeip mobileproxy мёртв вместе с подпиской). Deep-review проследил: поведение идентично — мёртвый URL и так возвращал False после впустую потраченного round-trip, max_rot 4→0 даёт тот же исход, бюджет ban-rotation считался так же и раньше.

Побочный фикс (найден ревью): cian curl-путь брал config.cian_proxy_url = мёртвый CIAN_PROXY_URL вместо живого SCRAPER_PROXY_URL — теперь ходит через живой узел.

Прод-верификация (05.08):

  • живой _provider_proxy = os.environ.get("SCRAPER_PROXY_URL"), IS_PROD-guard цел, живых роутов rotate-ip — 0, ASocks-ротация /admin/proxies/{id}/rotate (#2611) на месте;
  • .env.runtime на сервере почищен (бэкап /opt/gendesign/tradein-mvp/backend/.env.runtime.bak-2026-08-05-2616), SCRAPER_PROXY_URL сохранён; контейнеры перечитают при ближайшем рестарте — код переменные уже не читает, рассинхрон безопасен;
  • /scraper/health живой: все три провайдера резолвятся на один ASocks-узел 212.8.249.134 (выходной IP 46.8.110.75), rotate_supported: false, browser reachable, планировщик крутится.

Про egress-изоляцию честно: per-provider узлы были фикцией (мёртвые креды; BROWSER_PROXY_AVITO — задокументированный корень «52/53 забаненных прогонов за 14 дней»). Теперь один живой узел на все источники — это фиксация реальности, а не новая точка отказа; расширение ёмкости остаётся в #2638 (на владельце).

Закрываю.

Все три шага закрыты. **Шаг 1 (fail-closed)** — PR #2634 (04.08, merged + прод-verified). Отсутствие прокси в prod = явный отказ (`NoProxyAvailableError` в scraper-kit, 503 в browser-сайдкаре), а не прямое подключение. Отдельно дожат по ревью: guard был мёртв для avito-путей (прямое конструирование `BrowserFetcher` минуя `build_browser_fetcher` не прокидывало `environment`) и для сайдкара (`ENVIRONMENT` не задан в compose) — исправлено, добавлен source-level тест-гард на новые construction-sites. **Шаги 2-3** — PR #2650 (merged + прод-verified). Удалены 9 мёртвых переменных из кода/доков/compose и мёртвая ручка `POST /scraper/{source}/rotate-ip` с её кнопками во фронте. Каскадом застаблены `_rotate_proxy_ip`/`_rotate_ip` (changeip mobileproxy мёртв вместе с подпиской). Deep-review проследил: **поведение идентично** — мёртвый URL и так возвращал False после впустую потраченного round-trip, `max_rot 4→0` даёт тот же исход, бюджет ban-rotation считался так же и раньше. **Побочный фикс** (найден ревью): cian curl-путь брал `config.cian_proxy_url` = мёртвый `CIAN_PROXY_URL` вместо живого `SCRAPER_PROXY_URL` — теперь ходит через живой узел. **Прод-верификация (05.08):** - живой `_provider_proxy` = `os.environ.get("SCRAPER_PROXY_URL")`, IS_PROD-guard цел, живых роутов `rotate-ip` — 0, ASocks-ротация `/admin/proxies/{id}/rotate` (#2611) на месте; - **`.env.runtime` на сервере почищен** (бэкап `/opt/gendesign/tradein-mvp/backend/.env.runtime.bak-2026-08-05-2616`), `SCRAPER_PROXY_URL` сохранён; контейнеры перечитают при ближайшем рестарте — код переменные уже не читает, рассинхрон безопасен; - `/scraper/health` живой: все три провайдера резолвятся на один ASocks-узел `212.8.249.134` (выходной IP 46.8.110.75), `rotate_supported: false`, browser reachable, планировщик крутится. Про egress-изоляцию честно: per-provider узлы были фикцией (мёртвые креды; `BROWSER_PROXY_AVITO` — задокументированный корень «52/53 забаненных прогонов за 14 дней»). Теперь один живой узел на все источники — это фиксация реальности, а не новая точка отказа; расширение ёмкости остаётся в #2638 (на владельце). Закрываю.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2616
No description provided.