- Зачем: - прежнее имя выпячивало Xray, хотя клиент поддерживает также внешний прокси, а два каталога конфигурации были неочевидны. - Что: - пакет, CLI, конфигурация, состояние, тесты и документация переименованы в proxy-client. - машинный конфиг, профиль и два явных примера собраны в одном пользовательском каталоге. - свежая установка защищена от сворачивания каталога конфигурации внутрь репозитория. - Проверка: - bash tests/test_proxy_client_full.sh. - bash tests/test_install.sh. - ручные start, status и xray run -test; незавершённая сетевая приёмка записана в handoff.
11 KiB
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+, переносимость).
Чекбоксы
- Взаимное исключение: супервизор на всё время жизни держит
неблокирующий эксклюзивный
fcntl.flockна lock-файле в служебном каталоге. Второй одновременныйstartзавершается понятным отказом, не запуская процессов и не трогая чужие state/конфиг.state.jsonостаётся данными для восстановления после сбоя, а не механизмом блокировки. Последовательные повторныйstartи смена профиля при живом экземпляре по-прежнему отвергаются. - Схема state (права 0600, каталоги 0700) — ровно эти ключи:
supervisor_pid,xray_pid,xray_path,config_path,address,port(существующие) +supervisor_path,supervisor_starttime,xray_starttime,boot_id(новые). Ссылка VLESS и её секреты в state не попадают. statusиstopприменяют модель идентичности выше; чужой, устаревший или переиспользованный PID не признаётся управляемым процессом (в пределах принятой остаточной гонки).- Три исхода проверки состояния, у каждого — своё поведение:
- Здоровая пара (супервизор и ребёнок живы, идентичность
подтверждена):
stopсигналит супервизору, как сейчас. - Сирота (супервизор исчез, ребёнок жив и подтверждён по
PID + starttime + boot_id + exe):
stopсигналит ребёнку напрямую и удаляет state и сгенерированный конфиг; доступность порта для этого НЕ требуется.status— отдельная ненулевая диагностика «сирота»;envиrun— отказ. - Непроверяемое/устаревшее состояние: если записанный PID жив, но идентичность не подтверждена — никаких сигналов, state сохраняется, диагностика с инструкцией ручного удаления; если записанные PID мертвы (или boot_id не совпал) — безопасная очистка устаревших файлов разрешена.
- Здоровая пара (супервизор и ребёнок живы, идентичность
подтверждена):
- Конфликт локального порта:
startпри уже занятом порте завершается ненулевым кодом, диагностика называет порт занятым/недоступным, чужой слушатель не затронут, свои сгенерированные файлы убраны. Инспекция владения слушателем (/proc/net/tcp) запрещена. - Секреты: полная VLESS-ссылка никуда не персистится; UUID, ключи,
секретоподобные значения отсутствуют в stdout, stderr, argv,
state.jsonи lock-файле. Единственное допустимое место —client.jsonс правами 0600 внутри каталога 0700; он удаляется при штатном завершении и приstopсироты. - Автоматические тесты (без root, на обычной dev-машине):
- блокировка: удержанный извне flock →
startотказывает, ничего не запустив и не изменив; - переиспользованный PID моделируется правкой сохранённого
starttimeу живого процесса (реальную гонку PID не воспроизводим); отдельно — мёртвый PID, неверный исполняемый файл, несовпавшийboot_id; - SIGKILL супервизора →
stopдобивает подтверждённого сироту и чистит state/конфиг; - занятый порт (заранее открытый сокет) → контракт из чекбокса выше;
- юнит-тест парсера
/proc/PID/statна синтетическихcommс пробелами и); - фикстура фейкового Xray адаптирована под проверку
/proc/PID/exe(ребёнок должен быть настоящим исполняемым файлом — например,sys.executableкак «бинарник xray» со скриптами-операндами; детали на усмотрение исполнителя); - все сохраняемые ниже поведения остаются зелёными.
- блокировка: удержанный извне flock →
Сохранить, не переписывать
Уже реализовано и покрыто тестами в 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 — вне объёма.- Никаких коммитов.
Отчёт исполнителя
- Изменённые файлы со строкой «зачем» на каждый.
- Команды тестов и счётчики (
python3 -m unittest discover -s tests,bash tests/test_proxy_launchers.sh; baseline: 34 OK / 8 PASS — новых станет больше, указать числа). - Чекбоксы: закрыт / не закрыт и почему.
- Принятые по ходу решения и сомнения, если есть.