- Зачем: - прежнее имя выпячивало Xray, хотя клиент поддерживает также внешний прокси, а два каталога конфигурации были неочевидны. - Что: - пакет, CLI, конфигурация, состояние, тесты и документация переименованы в proxy-client. - машинный конфиг, профиль и два явных примера собраны в одном пользовательском каталоге. - свежая установка защищена от сворачивания каталога конфигурации внутрь репозитория. - Проверка: - bash tests/test_proxy_client_full.sh. - bash tests/test_install.sh. - ручные start, status и xray run -test; незавершённая сетевая приёмка записана в handoff.
145 lines
11 KiB
Markdown
145 lines
11 KiB
Markdown
# 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`.
|