Opera Proxy — справочник для zapret-gui

SkillProductivity

Complete reference for Opera Proxy in the zapret-gui project (Keenetic routers on Entware / OpenWrt / Linux): a standalone Opera VPN (SurfEasy) client that sets up a local HTTP or SOCKS5 proxy. Use for any tasks about: opera-proxy CLI flags (-country/-bind-address/-socks-mode/-proxy-bypa

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Opera Proxy — справочник для zapret-gui skill

What this skill tells your AI

The instructions your AI receives, as published by avatardd/zapret-gui in .claude/skills/opera-proxy/SKILL.md and read by ahel’s review.

Единый источник истины о том, как работает opera-proxy и как с ним обращаться в zapret-gui. Читать перед тем, как трогать менеджер, watchdog, установку бинарника или объяснять «почему прокси не проксирует».

Источники истины (в порядке убывания авторитета):

  1. Сам бинарник. opera-proxy -h — единственный достоверный список флагов и дефолтов для КОНКРЕТНОЙ версии; opera-proxy -version печатает ровно тег релиза (v1.28.0). Всё в §3 сверено с выводом -h реальной сборки opera-proxy.linux-amd64 v1.28.0.
  2. Alexey71/opera-proxy (MIT) — исходники и релизы, откуда мы ставим бинарник. Апстрим релизится часто (раз в 1–2 недели), флаги между версиями добавляются.
  3. Наш кодcore/opera_proxy_manager.py (жизненный цикл, валидация, детект, страны, лог), core/opera_proxy_watchdog.py (проба и рестарт), api/opera_proxy.py (REST), web/js/pages/opera_proxy.js (страница), core/ext_binary_installer.py (BINARIES["opera"]), core/cli.py (zapret-gui opera), core/config_manager.py (секция opera_proxy), app.py (автозапуск при boot), core/tunnel_monitor.py, core/update_checker.py, core/selfcheck.py.

⚠️ Главное, что путает при отладке: живой процесс ≠ работающий прокси. opera-proxy сначала регистрируется в API SurfEasy и только потом открывает слушателя. Без доступа к API он бесконечно ретраится, процесс висит, порт закрыт. Поэтому статус отдаёт listening отдельно от running (§8), а причина всегда в «Логе» (§7).


1. Что это такое и чем НЕ является

Что. Standalone-клиент Opera VPN. Использует ту же инфраструктуру SurfEasy, что встроенный VPN в браузере Opera: клиент сам регистрирует анонимное устройство в API и получает доступ к прокси-узлам. Аккаунта, подписки и своего сервера не нужно — отсюда роль «бесплатный запасной канал».

Чем НЕ является:

  • Не туннель и не метод единого слоя маршрутизации. Это локальный HTTP/SOCKS5-прокси на порту. Он ничего не заворачивает прозрачно: трафик пойдёт через него, только если приложение/браузер настроены на этот порт либо кто-то снаружи (redsocks/tproxy) направил соединения туда. В config_manager.routing.tunnel_priority opera намеренно отсутствует, а auto_remediation его не выбирает — UnifiedRoute(method="opera:…") не существует, это закреплено тестом tests/test_dev_merge_regressions.py::test_opera_not_in_default_priority.
  • Не приватный VPN. Инфраструктура чужая, вход анонимный и бесплатный: скорость/доступность не гарантированы, для чувствительного трафика — свой сервер (sing-box / AmneziaWG).
  • Не «настроил и забыл» на уровне протокола. Логин обновляется каждые 4 ч (-refresh), при потере API-доступа прокси перестаёт работать.

2. Цепочка подключения (что происходит на самом деле)

opera-proxy -country EU -bind-address 127.0.0.1:18080
   │
   ├─1. bootstrap-DNS: резолв api2.sec-tunnel.com через ВСТРОЕННЫЙ список
   │     DoH/DoT-резолверов (НЕ через DNS роутера!) — см. -bootstrap-dns
   ├─2. POST https://api2.sec-tunnel.com/v4/register_subscriber
   │     — анонимная регистрация (login/password/client-type зашиты)
   ├─3. регистрация устройства → получение proxy-credentials
   ├─4. discover: список прокси-узлов для выбранной страны
   ├─5. server-selection (по умолчанию fastest → замер скачивания)
   └─6. ТОЛЬКО ТЕПЕРЬ открывается слушатель на bind-address
         │
         └─ клиентское соединение → CONNECT на <cc>0.sec-tunnel.com:443
              по TLS, Proxy-Authorization: Basic <device-creds>

Ключевые следствия, которые объясняют 90 % жалоб:

ФактЧто из него следует
Порт открывается после успешной инициализации«Процесс есть, порт закрыт» — это не наш баг, а незавершённая регистрация
Резолв идёт через DoH-пул, а не системный DNSПровайдер/роутер режет DoH (443 на резолверы) → клиент не стартует вовсе. Лечится -bootstrap-dns dns://127.0.0.1:53
-init-retries по умолчанию 0 = бесконечно, интервал 5 сКлиент не падает и не сдаётся: он будет ретраиться вечно. Именно поэтому наши таймауты обязаны быть жёсткими (§6)
Узлы — eu0/as0/am0.sec-tunnel.com:443, обычный TLS+CONNECTЕсли DPI режет по SNI — помогает -fake-SNI; блок по IP узлов не лечится ничем из наших настроек
Логин обновляется каждые 4 чДолгоживущий процесс может «умереть» логически, оставаясь живым процессом → watchdog по TCP-пробе (§9)

3. CLI-флаги (сверено с -h v1.28.0)

3.1. Те, что передаём мы

Порядок и условия — OperaProxyManager.start():

opera-proxy -country <CC> -bind-address <host:port>
            [-socks-mode] [-proxy-bypass <list>] [-fake-SNI <domain>]
            -verbosity <N>
ФлагДефолт бинарникаЧто делает
-countryEUРегион. Для -list-proxies-all допускает список через запятую и ALL
-bind-address127.0.0.1:18080Адрес слушателя
-socks-modeвыклSOCKS вместо HTTP на том же порту
-proxy-bypassпустоСписок через запятую: хосты/URL-паттерны мимо прокси, регистр не важен, поддерживает * в именах хостов
-fake-SNIпустоДомен, который подставляется как SNI в исходящий TLS и в туннелируемый ClientHello, где это возможно — то есть маскирует не только связь с SurfEasy
-verbosity2010 debug, 20 info, 30 warning, 40 error, 50 critical, 60 silent

В GUI-селекторе verbosity нет уровня 50 — это осознанно (шкала 10/20/30/40/60), но validate_settings пропускает любой int 0…60, так что 50 через API задать можно.

3.2. Служебные, которые вызываем отдельно

ФлагГде используем
-version_get_version() — печатает ровно тег (v1.28.0), на этом держится сравнение «уже актуальная версия» в установщике и has_update в «Обновлениях»
-list-countries_fetch_countries()сетевая операция, см. §6

3.3. Что бинарник умеет, а GUI не показывает

Знать полезно: это готовые обходные пути, когда «не работает».

ФлагЗачем
-bootstrap-dnsСписок резолверов для поиска API. Дефолт — 9 публичных DoH (1.1.1.3, 8.8.8.8, dns.google, security.cloudflare-dns.com, dns.quad9.net, dns.adguard-dns.com, wikimedia-dns.org, doh.cleanbrowsing.org, fidelity.vm-0.com). Схемы: dns://, https://, tls://, tcp://
-api-proxy, -api-proxy-file, -api-proxy-list-url, -api-proxy-parallelДостучаться до API SurfEasy через сторонний прокси, когда api2.sec-tunnel.com недоступен. Кандидаты перебираются по порядку
-proxyБазовый прокси для ВСЕХ исходящих (http/https/socks5/socks5h://[user:pass@]host:port)
-server-selectionfirst / random / fastest (дефолт). fastest перед стартом качает тестовый файл с каждого узла — на слабом канале это заметная задержка старта; таймаут отбора — 1 мин
-timeoutТаймаут сетевых операций, дефолт 30s
-refresh, -refresh-retryИнтервал обновления логина (4 ч) и повтора (5 с)
-init-retries, -init-retry-intervalПопытки инициализации (0 = бесконечно) и пауза (5 с)
-override-proxy-address, -discover-csv, -list-proxies, -list-proxies-all[-out], -proxy-blacklist, -sort-proxies-by, -estimate-proxy-speedРучная работа со списком узлов в обход discover
-cafileСвой CA-бандл
-configЧитать конфигурацию из файла «ключ значение»
-api-login/-api-password/-api-client-type/-api-client-version/-api-user-agentПодмена зашитых учёток и «маскировки под Opera»

При добавлении нового флага в GUI: сначала проверь -h на той версии, которую реально ставит установщик. Флаг, которого нет, роняет старт целиком (Go печатает usage и выходит с кодом 2) — старт вернёт ошибку, а причина будет в буфере «Лог».


4. Наши настройки (config_manager, секция opera_proxy)

КлючДефолтКомментарий
enabledfalse«Opera должна работать». Выставляется при успешном up (API и CLI), снимается при down. Гейт для boot-автозапуска и watchdog'а
autostartfalseПодъём после перезагрузки и watchdog — одна галка в GUI
countryEUEU / AS / AM (список стран см. §6)
bind127.0.0.1:180800.0.0.0:… — отдать в LAN; IPv6 — [::1]:…
socks_modefalse
proxy_bypass""
fake_sni""
verbosity20
debug_logfalseГлубина буфера «Лог»: 60 → 600 строк. Применяется со следующего запуска
installed_tag / installed_arch""Исторические ключи, сейчас никем не пишутся

Единый источник параметров запуска — start_kwargs_from_config(). Все три пути старта (API up, boot-автозапуск в app.py, рестарт watchdog'ом) и CLI обязаны ходить через него. Раньше они расходились, и после перезагрузки прокси поднимался без fake_sni/proxy_bypass/verbosity, а CLI вообще стартовал с дефолтами — это чинилось дважды, не отменяй.

Настройки применяются только при следующем запуске. PUT /config живой процесс не трогает: после «Сохранить» нужен «Остановить» → «Запустить».


5. Валидация (parse_bind, validate_settings)

Обе функции — в core/opera_proxy_manager.py, и они общие для API, старта, watchdog-пробы и монитора. Не заводи локальный разбор адреса.

parse_bind(bind) -> (host, port) понимает 127.0.0.1:18080, localhost:8080, [::1]:18080, [::]:18080; кидает ValueError с человекочитаемой причиной на пустом адресе, адресе без порта, порте вне 1…65535 и на голом IPv6 без скобок.

validate_settings(dict) -> dict нормализует только переданные ключи (частичный PUT): страна → upper и isalnum, bind → канонизируется через parse_bind, bool-поля принимают true/1/yes/on, proxy_bypass — пробелы вокруг элементов вычищаются (а пробел внутри записи это ошибка), fake_sni — только [A-Za-z0-9.-_], verbosity — целое 0…60. Ошибка → ValueError, API отвечает HTTP 400 с текстом.

start() прогоняет ту же валидацию перед Popen: в settings.json мог остаться мусор с тех времён, когда валидации не было.


6. Страны: почему это дорого и как устроен кэш

-list-countriesне локальная команда. Бинарник ради неё проходит шаги 1–4 из §2, то есть регистрирует новое устройство в API SurfEasy.

Поэтому:

  • detect() никогда не вызывает -list-countries сам. Он дешёвый: версия берётся из кэша по (path, mtime, size), страны — из памяти.
  • Реальный запрос делает только list_countries(refresh=True)GET /api/opera-proxy/countries?refresh=1 (кнопка «Обновить список стран»).
  • Анти-дребезг _COUNTRIES_MIN_REFRESH_SEC = 60 + неблокирующий лок: два клика или две вкладки не породят две регистрации.
  • Таймаут _COUNTRIES_TIMEOUT = 30 секунд. Он обязан быть, потому что при недоступном API бинарник ретраится бесконечно (§2). Истёк → отдаём «opera-proxy не ответил за 30s — нет доступа к API SurfEasy?».
  • Формат вывода — CSV с заголовком country code,country name; парсер пропускает строки, начинающиеся с country.

Историческая ошибка, которую нельзя повторить: detect() вызывался из общего поллинга страницы (раз в 3 с) — получалась регистрация устройства каждые три секунды плюс два форка на тик на роутерном CPU. Поллинг GUI тянет только /status; detect и config — по открытию страницы и по кнопке «Обновить».

Базовые регионы: EU (Europe), AS (Asia), AM (Americas).


7. Менеджер: процесс, pid-файл, лог

core/opera_proxy_manager.py, singleton get_opera_proxy_manager().

Поиск бинарника (_find_binary, первый существующий и исполняемый): /opt/usr/bin/opera-proxy/opt/bin/opera-proxy/usr/local/bin/opera-proxy/usr/bin/opera-proxy.

Старт. Popen(..., stdout=PIPE, stderr=STDOUT, start_new_session=True), затем sleep(1) и проверка poll(): мгновенные падения — это занятый порт и негодные аргументы. PID пишется в <config_dir>/opera-proxy.pid.

Дренаж stdout — обязателен. Поток opera-proxy-drain непрерывно читает пайп в кольцевой буфер. Без него OS-буфер (~64 КБ) переполняется, Go-шный логгер блокируется на write() вместе с обработчиком соединения, и прокси перестаёт форвардить трафик. Это не теория: регресс-тест TestProxying::test_traffic_survives_chatty_logging гоняет 512 КБ через CONNECT при -verbosity 10 и падает по таймауту, если дренаж убрать. Буфер заводится до Popen, поэтому в «Логе» видна и командная строка, и причина неудачного старта.

Стоп. Есть объект процесса → SIGTERM, wait(3), иначе kill. Объекта нет (GUI перезапускали) → гасим по pid-файлу с проверкой _pid_is_opera() (читаем /proc/<pid>/cmdline; файл лежит в /opt и переживает перезагрузку, так что чужой PID вполне возможен), затем SIGTERM → до 3 с → SIGKILL.

Лог. read_log(lines){ok, debug, captured, log}; глубина 60 строк, 600 в режиме отладки (opera_proxy.debug_log), каждая строка обрезается до 400 символов. Подробность строк задаёт -verbosity самого бинарника, наш режим отладки влияет только на глубину.


8. Статус: runninglistening

status(probe=True) возвращает:

{"running": true, "pid": 1234, "bind": "127.0.0.1:18080", "listening": false}
  • running — жив ли процесс (объект в памяти или pid-файл + /proc).
  • bindфактический адрес запущенного процесса (_running_bind), после перезапуска GUI — из конфига.
  • listening — TCP-проба, есть только когда running. Именно это отличает «работает» от «висит в ретраях регистрации» (§2).

GUI пишет «Запущен, но порт не отвечает» и отправляет в «Лог»; CLI — bind: … (порт не отвечает).


9. Watchdog

core/opera_proxy_watchdog.py. Гейт: enabled И autostart — обе проверяются и в reconfigure(), и на каждом тике (конфиг могли поменять из другой вкладки).

Константы: интервал 60 с, порог 3 подряд, cooldown 120 с, не больше 6 рестартов в час.

probe_proxy(bind, timeout) — общая TCP-проба (её же использует status() и tunnel_monitor): разбирает адрес через parse_bind, ходит через socket.create_connection (то есть IPv6 работает), wildcard заменяет на loopback (0.0.0.0127.0.0.1, ::::1).

Тик:

  1. enabled/autostart сняты → watchdog сам себя останавливает.
  2. Процесс мёртв → счётчик, на третий раз рестарт.
  3. Bind не парсится → проба пропускается, счётчик сбрасывается. Иначе был бы вечный цикл «проба провалилась → рестарт → прокси не стартует».
  4. Проба прошла → счётчик в ноль; не прошла → как п.2.

Рестарт = stop() + start(**start_kwargs_from_config()), то есть watchdog всегда приводит прокси к тому, что записано в конфиге.


10. Установка бинарника

core/ext_binary_installer.py, BINARIES["opera"].

"repo": "Alexey71/opera-proxy",
"release_tag": "",          # пусто = /releases/latest
"pinned_tag": "v1.28.0",    # версия, для которой известны sha256
"allow_unpinned": True,
"dest": "/opt/usr/bin/opera-proxy",
"arch_map": {"aarch64": "opera-proxy.linux-arm64",
             "x86_64":  "opera-proxy.linux-amd64",
             "mipsel":  "opera-proxy.linux-mipsle",
             "mips":    "opera-proxy.linux-mips",
             "armv7":   "opera-proxy.linux-arm"},

Ставим последний релиз. Закреплённый тег давал тупик: «Обновления» видят новую версию апстрима, а кнопка «Установить» молча ставит старую. Проверка «уже актуально» работает благодаря тому, что -version печатает ровно тег.

Политика sha256 (общая для всех allow_unpinned-бинарников):

СитуацияПоведение
Тег == pinned_tag, хэш для арх. естьСверка, несовпадение → InstallError, установка прервана
Тег == pinned_tag, хэша для арх. НЕТОтказ: это дыра в манифесте, а не «версия новее»
Тег новее, у релиза есть файл контрольных суммСверяемся с ним (_verify_downloaded_file)
Тег новее, контрольных сумм нетСтавим, но sha256_verified: false + предупреждение в лог и тост в GUI

Апстрим сейчас не публикует checksums (checksums.txt, SHA256SUMS, *.sha256 — 404), так что на практике для версий новее pinned_tag работает последняя строка. Поднимая pinned_tag, обязательно пересчитай sha256 всех четырёх сборок с релизных URL и проверь процедуру на предыдущей версии (хэши старого тега должны совпасть с тем, что уже лежит в манифесте).

Архитектуры. Апстрим публикует ~29 ассетов (linux/darwin/freebsd/android, включая linux-arm и linux-386); мы маппим пять, которые встречаются на роутерах: aarch64, x86_64, mipsel, mips, armv7 (opera-proxy.linux-arm, ELF ARM EABI5). Сборки под riscv64 нет (404) — на ней установка честно отказывает «Архитектура … не поддерживается». Прежде чем добавлять новую архитектуру в arch_map, проверь, что ассет реально существует в релизе (curl -o /dev/null -w '%{http_code}' -L <release-url>/<asset>), и посчитай его sha256.


11. API (api/opera_proxy.py)

МетодПутьЧто делает
GET/api/opera-proxy/statusrunning, pid, bind, listening
GET/api/opera-proxy/detectДёшево: installed, binary, version, страны из кэша
GET/api/opera-proxy/countries[?refresh=1]Без refresh — кэш; с refreshсетевой запрос (§6)
POST/api/opera-proxy/upКонфиг + валидированные overrides из тела; при успехе enabled=true и reconfigure() watchdog'а
POST/api/opera-proxy/downenabled=false, стоп, reconfigure()
GET/PUT/api/opera-proxy/configНастройки; PUT валидирует (HTTP 400 с текстом)
GET/POST/api/opera-proxy/debugФлаг debug_log
GET/api/opera-proxy/log?lines=NХвост вывода
POST/api/opera-proxy/install | /uninstallinstall_binary_by_name("opera") / uninstall_binary("opera")

Тело запроса читается через _body() — кривой JSON даёт {}, а не HTTP 500.

CLI: zapret-gui opera {status|start|stop} — тот же путь, что у GUI (настройки из конфига, выставление enabled).

Прочие потребители: /api/dashboard/status (ключ opera), core/selfcheck.py (наличие и версия), core/update_checker.py::_check_opera (сравнение с _github_latest), core/tunnel_monitor.py::_read_opera_stats (порт берётся из настроек, не хардкод; метрики эмулируются из числа ESTABLISHED-соединений).


12. Диагностика «не работает»

  1. Бинарник есть? /api/opera-proxy/detectinstalled, version. Кнопка «Запустить» в GUI гасится, пока детект не подтвердит наличие.
  2. Процесс поднялся? status.running. Нет → смотри error от up и «Лог»: там командная строка и причина (занятый порт, негодный флаг).
  3. Порт отвечает? status.listening. running && !listening — почти всегда незавершённая инициализация:
    • в «Логе» строки Attempting action "anonymous registration" / Action … failed — API недоступен;
    • в ошибке видны lookup api2.sec-tunnel.com … dns-query … — не работает bootstrap-DoH (провайдер режет DoH, перехват DNS, MITM-сертификат). Обход — -bootstrap-dns dns://<локальный резолвер> (в GUI не вынесено);
    • dial tcp … i/o timeout на *.sec-tunnel.com — блок самой инфраструктуры SurfEasy; помогает только внешний обход (-api-proxy/-proxy) или другой канал.
  4. Порт слушает, но трафик не идёт?
    • проверь, что клиент настроен на нужный режим: socks_mode меняет протокол на том же порту — HTTP-клиент в SOCKS-порт не пойдёт;
    • bind=127.0.0.1 виден только самому роутеру: с ноутбука не подключиться, нужен 0.0.0.0;
    • proxy_bypass мог увести нужный домен напрямую.
  5. Прокси «зависает» под нагрузкой. Первым делом проверь, что дренаж stdout жив (§7) — это классика для verbosity ≤ 20.
  6. Watchdog дёргает исправный прокси. Смотри bind: проба ходит по адресу из конфига, а процесс мог быть запущен с другим (override в up).
  7. Страны не загружаются. Это отдельная сетевая операция (§6); её отказ не мешает прокси работать — регион задаётся кодом, а не списком.
  8. «Обновления» показывают новую версию. Кнопка «Обновить до последней версии» на странице; после замены файла работающий процесс остаётся на старой версии — нужен перезапуск.

13. Инварианты — что не ломать

  1. detect() дешёвый. Никаких сетевых вызовов: его дёргают поллинг GUI, selfcheck и update-checker.
  2. Дренаж stdout не отключать и не заменять на «читать по запросу».
  3. Один источник параметров запускаstart_kwargs_from_config().
  4. Один разбор адресаparse_bind(); проба — только probe_proxy().
  5. enabled выставляют все пути старта/остановки (API, CLI), иначе boot-автозапуск и watchdog молча мертвы.
  6. Валидация до записи в конфиг, а не при старте: иначе мусор оседает в settings.json и всплывает usage-дампом Go-бинарника.
  7. Форма настроек не перерисовывается по таймеру (затирает ввод) — рендер один раз, как в usque.js.
  8. opera не добавлять в tunnel_priority / unified-методы — это не прозрачный метод маршрутизации (§1).
  9. Флаги сверять с -h целевой версии перед добавлением в GUI.

Регресс-тесты: tests/test_opera_proxy.py (менеджер, watchdog, API, CLI, монитор + сквозной прогон трафика через CONNECT-прокси), tests/test_ext_binary_installer.py::TestOperaLatestRelease (политика latest/pinned/unpinned).

Signals

GitHub stars
133
Forks
10
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
opera-proxy
Source
github.com/avatardd/zapret-gui