Files
dotfiles/.scratch/proxy-client/issues/03-safe-process-lifecycle.md
T
ddadmin 6e431d875e 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.
2026-07-21 09:31:21 -04:00

11 KiB
Raw Blame History

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 не признаётся управляемым процессом (в пределах принятой остаточной гонки).
  • Три исхода проверки состояния, у каждого — своё поведение:
    1. Здоровая пара (супервизор и ребёнок живы, идентичность подтверждена): stop сигналит супервизору, как сейчас.
    2. Сирота (супервизор исчез, ребёнок жив и подтверждён по PID + starttime + boot_id + exe): stop сигналит ребёнку напрямую и удаляет state и сгенерированный конфиг; доступность порта для этого НЕ требуется. status — отдельная ненулевая диагностика «сирота»; env и run — отказ.
    3. Непроверяемое/устаревшее состояние: если записанный 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» со скриптами-операндами; детали на усмотрение исполнителя);
    • все сохраняемые ниже поведения остаются зелёными.

Сохранить, не переписывать

Уже реализовано и покрыто тестами в 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. Принятые по ходу решения и сомнения, если есть.