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:
1 parent
e5323284a8
commit
6e431d875e
25 files changed
+310
-227
No files matched your search
@@ -0,0 +1,132 @@
|
||||
# 03 — Безопасный жизненный цикл управляемого процесса
|
||||
|
||||
**What to build:** Управляемый Xray предсказуемо переживает повторные команды,
|
||||
сигналы, устаревшее состояние и аварийное исчезновение супервизора, не
|
||||
затрагивая чужие процессы и не оставляя секретных данных вне закрытого
|
||||
служебного каталога.
|
||||
|
||||
**Blocked by:** 02 — Управляемый Xray: полный штатный путь; 06 — Упрощение.
|
||||
|
||||
**Status:** done
|
||||
|
||||
**Резолюция (2026-07-20):** реализовано и принято. Дельта: CLI 976→1235,
|
||||
тесты 437→521 + 799→1112; сьюты: 49 unittest OK + 8 shell PASS (baseline
|
||||
34/8). Конвейер: spec-ревью постановки (NEEDS_REWORK, 7 MAJOR + 3 MINOR —
|
||||
постановка переписана до реализации) → реализация на medium → саморевью
|
||||
(3 находки, починены) → слепые ревью: TASK 1 MINOR + 2 NIT, CODE 4 MAJOR +
|
||||
1 MINOR (гонки идентичности/очистки) → триаж: FIXED 8, REJECTED 0 → обе
|
||||
перепроверки FIXES_CONFIRMED.
|
||||
|
||||
## Модель идентичности процессов
|
||||
|
||||
Идентичность определяется отдельно для ребёнка-Xray и для супервизора;
|
||||
«обход argv в поисках любого совпадения» (`verify_managed_state`) заменяется
|
||||
этим механизмом.
|
||||
|
||||
- **Общее для обоих:** PID + время старта. Время старта — поле 22
|
||||
(`starttime`) из `/proc/PID/stat`, целое число тиков с момента загрузки;
|
||||
хранится в state как есть и сравнивается на точное равенство. Парсер
|
||||
обязан сначала изолировать поле `comm` по последней `)` (comm может
|
||||
содержать пробелы и скобки) и только потом индексировать поля; любая
|
||||
ошибка чтения/разбора трактуется как несовпадение (fail-closed).
|
||||
- **Ребёнок Xray:** канонический путь `/proc/PID/exe` (realpath) совпадает
|
||||
с сохранённым `xray_path`.
|
||||
- **Супервизор:** его exe — интерпретатор Python, поэтому проверяется
|
||||
иначе: в state сохраняется канонический путь скрипта CLI
|
||||
(`supervisor_path`, realpath на момент старта); при проверке операнд
|
||||
скрипта из `/proc/PID/cmdline` резолвится (относительные пути — через
|
||||
`/proc/PID/cwd`) и сравнивается с `supervisor_path`. Поддерживаемые формы
|
||||
запуска (относительный путь, PATH, симлинк) не ломаются — сравнение
|
||||
идёт по realpath.
|
||||
- **Границы загрузки:** в state сохраняется `boot_id`
|
||||
(`/proc/sys/kernel/random/boot_id`). Несовпадение с текущим означает
|
||||
устаревшее состояние: записанные PID трактуются как мёртвые, сигналы не
|
||||
посылаются.
|
||||
- **Гонка check-then-kill:** непосредственно перед каждым `os.kill`
|
||||
идентичность перечитывается заново; несовпадение — отказ без сигнала.
|
||||
Остаточная гонка «процесс умер между перечиткой и kill» принята как риск
|
||||
модели доверенных пользователей (pidfd не используем: Python 3.8+,
|
||||
переносимость).
|
||||
|
||||
## Чекбоксы
|
||||
|
||||
- [x] Взаимное исключение: супервизор на всё время жизни держит
|
||||
неблокирующий эксклюзивный `fcntl.flock` на lock-файле в служебном
|
||||
каталоге. Второй одновременный `start` завершается понятным отказом, не
|
||||
запуская процессов и не трогая чужие state/конфиг. `state.json` остаётся
|
||||
данными для восстановления после сбоя, а не механизмом блокировки.
|
||||
Последовательные повторный `start` и смена профиля при живом экземпляре
|
||||
по-прежнему отвергаются.
|
||||
- [x] Схема state (права 0600, каталоги 0700) — ровно эти ключи:
|
||||
`supervisor_pid`, `xray_pid`, `xray_path`, `config_path`, `address`,
|
||||
`port` (существующие) + `supervisor_path`, `supervisor_starttime`,
|
||||
`xray_starttime`, `boot_id` (новые). Ссылка VLESS и её секреты в state
|
||||
не попадают.
|
||||
- [x] `status` и `stop` применяют модель идентичности выше; чужой,
|
||||
устаревший или переиспользованный PID не признаётся управляемым
|
||||
процессом (в пределах принятой остаточной гонки).
|
||||
- [x] Три исхода проверки состояния, у каждого — своё поведение:
|
||||
1. **Здоровая пара** (супервизор и ребёнок живы, идентичность
|
||||
подтверждена): `stop` сигналит супервизору, как сейчас.
|
||||
2. **Сирота** (супервизор исчез, ребёнок жив и подтверждён по
|
||||
PID + starttime + boot_id + exe): `stop` сигналит ребёнку напрямую и
|
||||
удаляет state и сгенерированный конфиг; доступность порта для этого
|
||||
НЕ требуется. `status` — отдельная ненулевая диагностика «сирота»;
|
||||
`env` и `run` — отказ.
|
||||
3. **Непроверяемое/устаревшее состояние:** если записанный PID жив, но
|
||||
идентичность не подтверждена — никаких сигналов, state сохраняется,
|
||||
диагностика с инструкцией ручного удаления; если записанные PID
|
||||
мертвы (или boot_id не совпал) — безопасная очистка устаревших
|
||||
файлов разрешена.
|
||||
- [x] Конфликт локального порта: `start` при уже занятом порте завершается
|
||||
ненулевым кодом, диагностика называет порт занятым/недоступным, чужой
|
||||
слушатель не затронут, свои сгенерированные файлы убраны. Инспекция
|
||||
владения слушателем (/proc/net/tcp) запрещена.
|
||||
- [x] Секреты: полная VLESS-ссылка никуда не персистится; UUID, ключи,
|
||||
секретоподобные значения отсутствуют в stdout, stderr, argv,
|
||||
`state.json` и lock-файле. Единственное допустимое место — `client.json`
|
||||
с правами 0600 внутри каталога 0700; он удаляется при штатном завершении
|
||||
и при `stop` сироты.
|
||||
- [x] Автоматические тесты (без root, на обычной dev-машине):
|
||||
- блокировка: удержанный извне flock → `start` отказывает, ничего не
|
||||
запустив и не изменив;
|
||||
- переиспользованный PID моделируется правкой сохранённого `starttime`
|
||||
у живого процесса (реальную гонку PID не воспроизводим); отдельно —
|
||||
мёртвый PID, неверный исполняемый файл, несовпавший `boot_id`;
|
||||
- SIGKILL супервизора → `stop` добивает подтверждённого сироту и чистит
|
||||
state/конфиг;
|
||||
- занятый порт (заранее открытый сокет) → контракт из чекбокса выше;
|
||||
- юнит-тест парсера `/proc/PID/stat` на синтетических `comm` с
|
||||
пробелами и `)`;
|
||||
- фикстура фейкового Xray адаптирована под проверку `/proc/PID/exe`
|
||||
(ребёнок должен быть настоящим исполняемым файлом — например,
|
||||
`sys.executable` как «бинарник xray» со скриптами-операндами; детали
|
||||
на усмотрение исполнителя);
|
||||
- все сохраняемые ниже поведения остаются зелёными.
|
||||
|
||||
## Сохранить, не переписывать
|
||||
|
||||
Уже реализовано и покрыто тестами в 02/06 — не переделывать, только не
|
||||
сломать: проброс SIGINT/SIGTERM/SIGHUP ребёнку; очистка state и конфига при
|
||||
штатном завершении; права 0600/0700; последовательные отказы (повторный
|
||||
start, смена профиля, несовпадение машинного адреса/порта со state);
|
||||
loopback-only; секреты вне stdout/stderr/argv; foreground-супервизор;
|
||||
fail-fast `pclaude`/`pcodex`; внешний путь без машинного конфига.
|
||||
|
||||
Запрещено восстанавливать срезанное в 06: владение слушателем через
|
||||
`/proc/net/tcp`, `profile_fingerprint`, инспекцию zombie-состояний,
|
||||
SIGALRM-машинерию, поддержку второй версии Xray.
|
||||
|
||||
## Не трогать
|
||||
|
||||
- `bash/.bash_functions`, установщик, README — вне объёма.
|
||||
- Никаких коммитов.
|
||||
|
||||
## Отчёт исполнителя
|
||||
|
||||
1. Изменённые файлы со строкой «зачем» на каждый.
|
||||
2. Команды тестов и счётчики (`python3 -m unittest discover -s tests`,
|
||||
`bash tests/test_proxy_launchers.sh`; baseline: 34 OK / 8 PASS — новых
|
||||
станет больше, указать числа).
|
||||
3. Чекбоксы: закрыт / не закрыт и почему.
|
||||
4. Принятые по ходу решения и сомнения, если есть.
|
||||
Reference in new issue
Block a user