refactor(proxy-client): переименован интерфейс прокси

- Зачем:
  - прежнее имя выпячивало Xray, хотя клиент поддерживает также внешний прокси, а два каталога конфигурации были неочевидны.
- Что:
  - пакет, CLI, конфигурация, состояние, тесты и документация переименованы в proxy-client.
  - машинный конфиг, профиль и два явных примера собраны в одном пользовательском каталоге.
  - свежая установка защищена от сворачивания каталога конфигурации внутрь репозитория.
- Проверка:
  - bash tests/test_proxy_client_full.sh.
  - bash tests/test_install.sh.
  - ручные start, status и xray run -test; незавершённая сетевая приёмка записана в handoff.
This commit is contained in:
ddadmin committed 2026-07-21 09:31:21 -04:00
1 parent e5323284a8
commit 6e431d875e
25 files changed
+310 -227

No files matched your search

@@ -0,0 +1,144 @@
# 04 — Проверяемая пользовательская установка Xray
**What to build:** Пользователь без root устанавливает официальный Xray
одной явной командой `install` и получает проверенный бинарник
зафиксированной версии, без скрытой замены существующей установки, без
записи за пределами пользовательских каталогов и без риска для живого
managed-экземпляра.
**Prerequisites:** 01, 03, 06 — все done. Наследуемые решения: одна
поддержанная версия Xray 26.3.27 (06); идентичность managed-процессов по
`/proc/PID/exe` и start-lock (03); Python 3.8+ stdlib-only, переносимость
Ubuntu / РЕД ОС / Astra / Alt.
**Status:** done
## Зафиксированный протокол установки
- **Только версия 26.3.27** — та же, что принимает `VERSION_LINE`.
Никакого параметра версии: будущий апгрейд — отдельный тикет, меняющий
пин скачивания, ворота рантайма, фикстуры и документацию вместе.
- **Источник:** `https://github.com/XTLS/Xray-core/releases/download/v26.3.27/`.
Маппинг архитектур (чистая функция, по `platform.machine()`):
`x86_64`/`amd64` → `Xray-linux-64.zip`; `aarch64`/`arm64` →
`Xray-linux-arm64-v8a.zip`; прочее — понятный отказ.
- **Проверка:** скачивается `<архив>.dgst`. Фактический формат официальных
файлов — четыре записи `MD5=`, `SHA1=`, `SHA2-256=`, `SHA2-512=`
(исправлено 2026-07-20 по находке CODE-ревью: однострочного `SHA256=`
в релизах нет). Принимается ровно одна запись `SHA2-256= <64 hex в
нижнем регистре>`; дубликаты, uppercase, отсутствие записи или мусор —
отказ. Хэш архива считается `hashlib.sha256`; несовпадение или
неожиданный формат — отказ до распаковки, установленный бинарник не
тронут.
- **Границы размеров:** скачивание и распаковка ограничены
зафиксированными лимитами (архив, `.dgst`, извлекаемый бинарник);
фактически прочитанные байты считаются с limit+1; несоответствие
объявленному `Content-Length` или превышение лимита — отказ без
исчерпания диска/памяти.
- **Инструменты:** `urllib.request` для скачивания, `zipfile` для
извлечения члена `xray`. Явно исключены: tar, GPG-подписи, curl, wget,
пакетные менеджеры, sudo, systemd, системные каталоги.
- **Атомарность:** приватный staging-каталог создаётся ВНУТРИ
`~/.local/bin` (одна файловая система — `os.replace` без EXDEV);
скачивание и распаковка туда; кандидату ставится режим 0755; кандидат
проходит проверку `version` (те же ворота 26.3.27); затем один
`os.replace` в `~/.local/bin/xray`. До этого вызова существующий файл
назначения не изменяется. Staging убирается при любой ошибке и
прерывании (best-effort); fsync/power-loss-гарантии не требуются.
## Чекбоксы
- [x] Команда `install [--replace]` добавлена в парсер и диспетчеризуется
ДО `load_config()`: установка не требует машинного конфига и
VLESS-профиля. `start` и запуск без подкоманды по-прежнему никогда не
начинают скачивание.
- [x] Защита managed-режима: до любого скачивания и изменения назначения
`install` берёт существующий start-lock и держит его до конца
`os.replace`. Lock занят — немедленный отказ с подсказкой «сначала
stop». При взятом lock допускаются только отсутствующий state или
доказуемо устаревший (по классификации 03 — stale); живой, сиротский
или непроверяемый state — отказ.
- [x] Поведение при существующем назначении `~/.local/bin/xray`:
- отсутствует → установка;
- обычный исполняемый файл, отвечающий `26.3.27` → no-op без сети
(успех, сообщение «уже установлено»);
- `--replace` → принудительная переустановка с полной проверкой;
- иной файл (не отвечает 26.3.27, неисполняемый, битый) или симлинк →
отказ без `--replace`; с `--replace` заменяется сам путь, симлинк не
разыменовывается;
- каталог → всегда понятный отказ.
- [x] После установки: проверка `shutil.which("xray")` — резолвится ли в
установленный `~/.local/bin/xray`. Нет — ненулевой код с подсказкой
добавить `~/.local/bin` в начало `PATH`; готовая установка при этом НЕ
откатывается.
- [x] Тестовый шов: чистая функция «архитектура → имя ассета» и
подменяемый загрузчик (callable), который тесты заменяют in-process.
Пользовательского оверрайда базового URL нет.
- [x] Автоматические тесты (без root, без сети; временные HOME/PATH/state):
- маппинг обеих архитектур (patch значения архитектуры) и отказ на
неподдержанной;
- корректный `.dgst` → установка; повреждённый хэш и неожиданный формат
`.dgst` → отказ до распаковки, назначение не тронуто;
- фикстуры: локальный ZIP с фейковым исполняемым, печатающим
`Xray 26.3.27 ...`, и парные валидный/битый `.dgst`;
- существующая корректная установка → no-op без обращения к загрузчику;
`--replace` → переустановка; симлинк/каталог/битый файл — по таблице
выше;
- атомарность: инъецированная ошибка после скачивания / при распаковке /
при проверке `version` → назначение не изменено, staging убран;
- запись только в пределах staging/назначения и lock-файла: сверка
полного дерева временной ФС до/после;
- защита managed: при живом managed-экземпляре (реальная фикстура 03)
`install --replace` отказывает с подсказкой «сначала stop», бинарник
и state не тронуты;
- `shutil.which`-проверка: отсутствие каталога в PATH и конкурирующий
более ранний `xray` в PATH.
- [x] `proxy-client/README.md` обновлён: одна поддержанная версия 26.3.27
(упоминания 25.12.8 удалены), использование `install [--replace]`,
маппинг x86_64/aarch64, SHA-256-проверка, поведение PATH, отказ при
живом managed-экземпляре.
## Сохранить, не трогать
- Существующее поведение `resolve_xray`, ворот `VERSION_LINE`,
managed-режима и всех 49 зелёных тестов + 8 shell-проверок.
- Секреты: install не касается профилей и не выводит их; общее правило
«VLESS/UUID/ключи никогда в stdout/stderr/argv» остаётся.
- `bash/.bash_functions`, установщик dotfiles — вне объёма.
- Никаких коммитов.
## Отчёт исполнителя
1. Изменённые файлы со строкой «зачем» на каждый.
2. Команды тестов и счётчики (baseline: 49 OK / 8 PASS — новых станет
больше, указать числа).
3. Чекбоксы: закрыт / не закрыт и почему.
4. Принятые по ходу решения и сомнения, если есть.
## Резолюция (2026-07-20)
Конвейер пройден целиком: постановка → Codex spec-review (high, 5 MAJOR +
3 MINOR + 1 NIT, переписана) → реализация (Codex, medium — эксперимент) →
саморевью (found 6, fixed 6) → два слепых ревью (CODE: 2 MAJOR + 2 MINOR;
TASK: 3 NIT) → триаж (FIXED 7, REJECTED 0) → узкие перепроверки
(оба FIXES_CONFIRMED) → приёмка оркестратором.
Ключевое:
- Команда `install [--replace]`: пин 26.3.27, маппинг архитектур чистой
функцией, `urllib.request` + `zipfile`, staging внутри `~/.local/bin`,
один `os.replace`, `version`-ворота кандидата, guard по start-lock.
- CODE-1 (MAJOR) оказался дефектом постановки: spec-review зафиксировал
несуществующий однострочный формат `SHA256=`; реальные `.dgst` — четыре
записи. Протокол в тикете исправлен (см. выше), парсер строгий: ровно
одна `SHA2-256= <64 hex в нижнем регистре>`.
- Границы размеров: 64 MiB архив/бинарник, 4 KiB `.dgst`, счёт limit+1,
сверка Content-Length; offline-покрытие реального `download_file` через
подмену `urllib.request.urlopen`.
- `StartLockBusyError` вместо парсинга текста ошибки; снапшоты дерева ФС
в тестах отказных путей; PATH-тест конкурирующего бинарника.
- README дополнён (по просьбе пользователя): последняя версия с
`allowInsecure` — 25.12.8, опция удалена в 26.2.6; CLI их не ставит.
Тесты: 79 OK (baseline 49, +30) + 8 PASS shell-проверок. Затраты — в
`.scratch/proxy-client/token-ledger.md`.