Files
dotfiles/docs/specs/2026-07-15-headless-xray-proxy-design.md
T
ddadminandClaude Fable 5 98bfd6338a refactor(xray-proxy): срезана accidental-сложность CLI, тестов и спеки
- Зачем:
  - CLI и спека были переусложнены относительно принятой модели угроз
    (один пользователь, доверенная машина): лишняя ось версий Xray и
    защита от входов, которые не возникают при ручном редактировании.
- Что:
  - убрана поддержка Xray 25.12.8 — поддерживается только 26.3.27;
    allowInsecure валидируется, но никогда не эмитится в JSON.
  - удалена проверка владения сокетом через /proc/net/tcp; идентичность
    процесса и сверка адрес/порт со state сохранены.
  - валидация INI/профиля/адреса/query сведена к отказам на реальные
    опечатки; scoped IPv6 отклоняется одинаково на Python 3.8 и 3.9+.
  - удалены fsync/O_EXCL-церемония, SIGALRM-таймер, zombie-инспекция,
    мёртвый отпечаток профиля и двойной разбор extra/alpn.
  - shell.bash принимает вывод env без ре-валидации, сохранив атомарность;
    спека и issue 01–05 приведены к новому объёму (тикет 06, леджер).
- Проверка:
  - python3 -m unittest discover -s tests — 34 теста OK.
  - bash tests/test_proxy_launchers.sh — 8 проверок PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:13:54 -04:00

38 KiB
Raw Blame History

Headless Xray-прокси для агентских CLI

Дата: 2026-07-15

Статус: дизайн утверждён, реализация не начата

Проблема

На удалённых Linux-серверах нет GUI-клиента Hiddify. Перед запуском Claude Code или Codex приходится вручную запускать Xray и выставлять proxy-переменные. Переменные легко забыть, после чего агент стартует без прокси.

Нужны привычные команды pclaude и pcodex, которые не запускают агента, если явно настроенный прокси недоступен. Решение должно работать как часть этого репозитория и отдельно от остальных dotfiles.

Контекст

Существующий дизайн pclaude / pcodex задаёт важный контракт: proxy-переменные видны только дочернему процессу, аргументы и код возврата сохраняются, а недоступный прокси приводит к отказу до запуска агента.

Эта спецификация заменяет Linux-часть прежнего дизайна в вопросах расположения функций, источника настроек и вывода proxy_on/proxy_off. Windows-часть прежнего дизайна не меняется.

Сейчас Bash-функции жёстко используют внешний Hiddify на hproxy.local:12334. Принесённый прототип scripts/setup-xray-proxy.sh умеет установить и запустить Xray из готового JSON, но смешивает установку и запуск, не разбирает ссылки Hiddify и небезопасно ищет чужие процессы через pkill.

Xray принимает конфигурационные файлы, а не vless://-ссылки. При этом его SOCKS-вход принимает SOCKS и HTTP на одном порту, поэтому существующая схема proxy-переменных совместима с локальным Xray без второго порта. См. командную строку Xray и SOCKS-вход.

Цели

  • Дать headless Linux-серверу управляемый пользовательский Xray без systemd и прав root.
  • Сохранить явные pclaude и pcodex, которые нельзя случайно запустить через другой прокси или без настроенного прокси.
  • Принимать одну точечную VLESS-ссылку Hiddify и преобразовывать её в клиентский JSON Xray.
  • Сохранить совместимость Bash-функций с внешним Hiddify.
  • Позволить настраивать машинный NO_PROXY без правки файлов репозитория.
  • Поддержать выборочную установку после git clone: через stow, ручные симлинки или копирование.

Не входит в объём

  • Windows, PowerShell, Windows Codex launcher и VS Code Server.
  • systemd, автозапуск после перезагрузки, sudo, firewall, TUN и изменение системной маршрутизации.
  • Автоматический выбор между Xray и Hiddify или переход на запасной прокси.
  • Одновременное управление несколькими профилями одного Unix-пользователя.
  • Универсальный импорт подписок или полного JSON-экспорта Hiddify.
  • Протоколы и сочетания за пределами двух явно поддержанных семейств.
  • pip, venv и любые сторонние Python-библиотеки.
  • Сетевая изоляция голых claude и codex: как и раньше, гарантию намерения дают явные команды с префиксом p.

Рассмотренные варианты

Только Bash

Ближе всего к принесённому прототипу, но URL-кодирование, JSON, проверка схемы, контрольные суммы и безопасное управление процессом делают Bash-реализацию хрупкой и плохо тестируемой.

Bash-контроллер и отдельный Python-преобразователь

Оставляет жизненный цикл в Bash, а разбор VLESS переносит в Python. Граница между двумя реализациями усложняет ошибки, тесты и единый источник настроек.

Один Python CLI и тонкий Bash-модуль

Python отвечает за данные, установку Xray, процессы и запуск дочерних команд. Bash нужен только там, где необходимо изменить окружение текущей shell-сессии. Это выбранный вариант.

Не создавать менеджер

Можно хранить готовые JSON и вручную запускать Xray в tmux. Вариант почти не требует кода, но не решает основную боль: безопасный запуск pclaude и pcodex из точечной ссылки Hiddify.

Рекомендуемое решение

Границы компонентов

xray-proxy — один исполняемый файл на Python 3.8+ без внешних библиотек. Он отвечает за установку Xray, запуск и остановку процесса, диагностику, преобразование VLESS и запуск произвольной дочерней команды через прокси.

Переносимый shell.bash — единственный владелец proxy_on, proxy_off, pclaude, pcodex, myip и внутренних proxy-помощников. При реализации они удаляются из bash/.bash_functions, чтобы результат не зависел от порядка source. update_hproxy остаётся там как отдельная WSL-утилита управления /etc/hosts, а не часть переносимого интерфейса прокси. Модуль не разбирает машинный конфиг самостоятельно:

  • pclaude и pcodex делегируют запуск форме xray-proxy run -- COMMAND ARG..., где -- отделяет параметры клиента;
  • proxy_on получает от xray-proxy env проверенный набор proxy-переменных и применяет его к текущей Bash-сессии;
  • proxy_off очищает верхний и нижний регистр proxy-переменных.

Общий bash/.bash_functions и модуль Xray не зависят друг от друга. В составе dotfiles .bash_custom подключает их двумя независимыми строками. Без остальных dotfiles пользователь добавляет только один source модуля Xray в .bashrc. Для неинтерактивных сценариев и других оболочек остаётся прямой xray-proxy run.

xray-proxy env сначала выполняет ту же проверку доступности, что и run, а затем печатает восемь строк KEY=value: HTTP, HTTPS, ALL и NO_PROXY в верхнем и нижнем регистре. shell.bash сначала целиком получает успешный ответ, затем разбирает строки по первому =, принимает только известные ключи и лишь после полной проверки экспортирует полученные значения через printf -v. eval не используется, частичного изменения окружения при ошибке нет.

У env stdout зарезервирован только для восьми машинно-читаемых строк. Предупреждения и ошибки идут в stderr; обычная безопасная сводка этой командой не печатается.

Совместимость существующих Bash-команд означает сохранение имён функций, набора переменных, fail-fast перед pclaude/pcodex и изоляции окружения дочернего процесса. Автоматический запрос внешнего IP из proxy_on и proxy_off удаляется: он замедляет переключение и смешивает изменение окружения с диагностикой. Для проверки остаются myip и xray-proxy status.

Два независимых файла настройки

Машинный файл ~/.config/proxy/config хранит несекретное описание активного прокси: явный тип xray или external, отображаемое имя, адрес, порт и добавку к NO_PROXY. Формат — INI из стандартной библиотеки Python. Если файла нет, прокси-подсистема сохраняет прежний внешний адрес hproxy.local:12334 ради обратной совместимости.

Секретный ~/.config/xray-proxy/default.url содержит одну VLESS-ссылку. Команда запуска принимает другой файл через -f. Профили можно хранить в нескольких файлах, но активным у пользователя бывает только один. CLI следует за симлинком, но проверяет права конечного файла: доступ группы или остальных пользователей считается ошибкой с подсказкой выполнить chmod 600. Файл должен содержать ровно одну непустую строку без комментариев и служебного текста.

Базовый NO_PROXY всегда включает loopback-адреса и узел самого прокси. Машинная добавка расширяет его локальными IP и доменами. Документация должна предупреждать, что поддержка CIDR в NO_PROXY различается между программами.

Явная семантика прокси

Тип xray означает, что pclaude и pcodex требуют запущенный процесс, которым управляет xray-proxy. Если его нет, агент не запускается, а сообщение предлагает выполнить xray-proxy start.

Тип external означает внешний процесс, например Hiddify. Проверяется только доступность явно заданного адреса и порта. Ни один тип не переходит на другой автоматически.

start разрешён только при type=xray; при type=external он завершается с подсказкой изменить машинный конфиг. Для Xray адрес машинного конфига обязан быть loopback-адресом, а порт обязан совпадать с создаваемым входом. Так нельзя случайно поднять локальный Xray, оставив pclaude направленным во внешний Hiddify.

status описывает именно активный тип из машинного конфига: состояние процесса и порта для Xray либо доступность адреса для external. stop от машинного типа не зависит и завершает записанный управляемый Xray, если он существует. Поэтому после явного переключения на external старый локальный процесс можно штатно остановить.

HTTP, HTTPS и SOCKS используют один локальный порт, по умолчанию 12334. Порт остаётся настраиваемым, чтобы разные Unix-пользователи одной машины могли запускать свои экземпляры без конфликта.

Локальный вход Xray слушает только loopback: 127.0.0.1 и, если поддержано выбранной конфигурацией, ::1. Он никогда не привязывается к 0.0.0.0, адресу сетевого интерфейса или имени узла. Это закрывает вход от удалённой сети, но не изолирует его от других пользователей того же Linux-узла. Первая версия не добавляет локальную аутентификацию и предполагает доверенных пользователей машины.

Поддерживаемые VLESS-ссылки

Первая версия поддерживает две семьи:

  • VLESS с TCP/RAW и REALITY;
  • VLESS с XHTTP и TLS, включая HTTP/1.1, h2 и h3 через ALPN.

Преобразователь сохраняет значимые параметры ссылки, проверяет обязательные поля и отклоняет неизвестное сочетание целиком. Он не должен молча пропускать неизвестные или несовместимые параметры. Поддержка REALITY опирается на официальную схему REALITY, а XHTTP с TLS — на схему TLS.

Для REALITY преобразователь явно сопоставляет UUID с VLESS id, узел и порт с адресом назначения, type=tcp с RAW, flow с режимом VLESS Vision, sni с serverName, fp с fingerprint, pbk с password, а sid с shortId. headerType=none принимается как нейтральное значение, другое значение отклоняется. hiddify, host и alpn для TCP/REALITY считаются метаданными и не меняют Xray JSON; безопасная сводка явно помечает host и alpn как неиспользованные. Это не скрытая потеря параметра: в целевой схеме REALITY Xray 26.3.27 нет полей Host или ALPN. Повтор одного query-параметра считается ошибкой. Остальные правила соответствуют стандарту VLESS-ссылок Xray.

Основной сценарий XHTTP/TLS — одиночная ссылка Hiddify с type=xhttp, security=tls, sni, alpn, path, host, encryption, fp и, возможно, extra. UUID, узел и порт преобразуются так же, как для REALITY. Остальные поля сопоставляются явно:

  • type=xhttp задаёт транспорт XHTTP;
  • security=tls включает TLS;
  • sni, alpn и fp задают serverName, список ALPN и fingerprint;
  • path, host и необязательный mode переходят в настройки XHTTP;
  • encryption задаёт шифрование VLESS и в первой версии должно быть none;
  • extra URL-декодируется, разбирается как JSON-объект без повторяющихся ключей и целиком передаётся в одноимённую секцию XHTTP;
  • pcs и vcn для Xray 26.3.27 переходят в pinnedPeerCertSha256 и verifyPeerCertByName;
  • hiddify=1 и core=xray принимаются как метаданные источника и не меняют JSON;
  • headerType=none принимается как устаревшее нейтральное поле Hiddify, другое значение отклоняется;
  • fragment после # используется только как отображаемое имя профиля.

Если mode отсутствует, преобразователь не придумывает его и оставляет выбор Xray. Параметры query не зависят от порядка, но не могут повторяться. После перечисленных полей неизвестный параметр считается ошибкой: он может менять смысл соединения и требует явного добавления в преобразователь. path, host, mode и extra входят в официальный стандарт ссылок Xray; параметры TLS pcs/vcn заменяют удалённый allowInsecure: стандарт VLESS-ссылок.

Перед запуском CLI печатает компактную безопасную сводку: сервер, порт, транспорт, защиту, SNI, ALPN, Host, наличие и длину пути, fingerprint и локальный порт. XHTTP path может содержать tenant ID или bearer-подобный токен, поэтому его содержимое никогда не печатается. Обычный режим не выводит подробный журнал Xray. UUID, REALITY password/public key, short ID и другие чувствительные значения обозначаются без фрагментов исходного значения. В создаваемом JSON журнал доступа отключён, а уровень ошибок по умолчанию — warning.

Hiddify Manager может добавлять allowInsecure=true и insecure=true даже к профилю с действительным публичным сертификатом — этот дефект описан в hiddify/Hiddify-Manager#5438. Современный Xray больше не поддерживает allowInsecure; удаление зафиксировано в релизе 26.2.6. Преобразователь принимает для обоих полей только точные строчные значения true и false. Если присутствуют оба поля, их значения должны совпадать; конфликт и любое другое написание отклоняются до создания JSON. Для Xray 26.3.27 поля не переносятся в JSON; истинное значение даёт предупреждение, что сертификат будет проверен обычным способом, а ложное не требует предупреждения. Соединение после этого может успешно заработать или завершиться ошибкой сертификата; отдельного раннего отказа нет.

Установка Xray

xray-proxy install — отдельная явная операция. Она устанавливает Xray в ~/.local/bin, скачивает официальный архив и файл контрольной суммы, затем проверяет SHA-256. Версия по умолчанию зафиксирована в коде клиента на текущем на момент реализации стабильном релизе 26.3.27. Пользователь может явно указать другую версию через --version; произвольная версия не становится заявленной как поддерживаемая только из-за успешной установки. Официальные релизы публикуют необходимые файлы .dgst: релиз 26.3.27.

Первая версия установщика поддерживает Linux x86_64 и aarch64. Архив распаковывается во временный пользовательский каталог, проверенный бинарник сначала запускается с version, а затем атомарно переносится в ~/.local/bin/xray. Существующий файл другой версии не перезаписывается без явного --replace. После установки CLI проверяет, что command -v xray указывает именно на установленный файл; иначе завершается с подсказкой исправить PATH. start всегда показывает фактический путь и версию выбранного бинарника.

Перед созданием JSON CLI разрешает первый xray из PATH до одного пути и запускает этим файлом xray version. Первая строка должна начинаться с точного токена Xray VERSION, после полного токена версии допускается только конец строки либо ASCII-пробел. Принимается только версия 26.3.27; ведущий мусор, похожий более длинный токен, пустой, неверный, завершившийся ошибкой или содержащий другую версию вывод запрещает запуск. Тот же разрешённый файл используется для run -test и основного процесса. Версия никогда не выводится из содержимого VLESS-ссылки.

start никогда не скачивает бинарник. Если xray отсутствует в PATH, команда завершается с подсказкой выполнить xray-proxy install.

Жизненный цикл

CLI предоставляет install, start, stop, status, env и run. Запуск без подкоманды не устанавливает и не стартует ничего.

start остаётся на переднем плане как небольшой Python-супервизор и занимает текущую консоль. Он запускает один дочерний Xray без демонизации и без новой сессии процесса, ждёт его завершения и передаёт ему SIGINT, SIGTERM и SIGHUP. Ожидаемый способ работы — отдельная панель tmux или другого мультиплексора. Отсоединение от tmux не останавливает процесс, но закрытие панели или сессии останавливает Xray. Отдельный постоянный журнал и команда logs не нужны: достаточно истории tmux.

Супервизор владеет блокировкой, дочерним процессом и служебным каталогом ~/.local/state/xray-proxy/. Файл состояния содержит PID и время старта обоих процессов, путь к бинарнику и порт, но не VLESS-ссылку и не её секреты. status и stop сверяют PID со временем старта и ожидаемым исполняемым файлом, поэтому повторное использование PID не считается управляемым процессом. stop посылает SIGTERM супервизору, а тот завершает Xray и очищает состояние. Если супервизор аварийно исчез, stop может завершить только дочерний Xray, идентичность которого подтверждена теми же данными.

У пользователя может быть только один управляемый экземпляр. Повторный start не заменяет его другим профилем. Для смены профиля нужны явные stop и start.

Перед успешным запуском должны пройти разбор ссылки, проверка поддерживаемого сочетания и xray run -test. После запуска процесс должен оставаться живым, а локальный порт — принимать соединения. Проверка внешнего IP через прокси носит диагностический характер: её сбой даёт предупреждение, но не останавливает уже прошедший обязательные проверки Xray.

Сгенерированный JSON и временные файлы создаются внутри служебного каталога и удаляются супервизором после завершения Xray. Каталоги создаются с правами 0700, файлы с конфигурацией и состоянием — 0600. Исходная VLESS-ссылка не попадает в stdout или stderr при успехе, ошибках разбора и сбое run -test и никогда не передаётся аргументом командной строки. Те же поверхности не содержат UUID, ключи, short ID и другие уникальные секретные маркеры профиля.

run никогда не запускает Xray. Для типа xray он проверяет PID управляемого процесса и локальный порт; для типа external — только явно настроенный адрес и порт. Лишь после успешной проверки он выставляет proxy-переменные для дочерней команды. Поэтому pclaude и pcodex не имеют скрытого автозапуска.

Доставка

Компоненты образуют отдельный stow-совместимый каталог xray-proxy/. Он доставляет CLI, Bash-модуль и два шаблона: машинного INI и файла VLESS-ссылки. Реальные пользовательские файлы не управляются stow и не входят в Git. CLI раскладывается как ~/.local/bin/xray-proxy, а модуль — как ~/.config/xray-proxy/shell.bash. Шаблоны лежат рядом с модулем в подкаталоге examples/ и не подменяют настоящие пользовательские файлы.

В корне этого каталога находится самостоятельный README.md. Он является основной инструкцией клиента и исключается из раскладки stow. README остаётся рядом с кодом при выборочном клонировании или копировании каталога. Корневой README репозитория только кратко описывает подсистему и ссылается на него.

После клонирования репозитория пакет можно подключить отдельно от общего install.sh. README также должен описывать эквивалентные ручные симлинки и копирование файлов. Полный install.sh подключает этот же stow-пакет вместе с остальными dotfiles и не имеет отдельной реализации.

Решения и отклонённые альтернативы

  • Выбран Python 3.8+, потому что стандартной библиотеки достаточно для URL, JSON, INI, ZIP, SHA-256, HTTP, процессов и тестов. Более новый синтаксис и сторонние пакеты запрещены ради старых и непредсказуемых серверных систем.
  • Выбран INI, а не TOML: модуль tomllib добавлен только в Python 3.11, что исключило бы штатный Python 3.10 в Ubuntu 22.04.
  • Bash оставлен отдельным тонким модулем, потому что дочерний Python-процесс не может изменить окружение родительской shell-сессии.
  • Машинный прокси и VLESS-ссылка разделены: первый файл описывает локальную политику shell, второй содержит секрет и меняется независимо.
  • Выбран один порт, потому что SOCKS-вход Xray совместим с HTTP и не требует второго локального слушателя.
  • Выбран явный тип прокси вместо автообнаружения. Предсказуемость канала важнее удобства автоматического перехода.
  • Выбрана одна активная конфигурация на пользователя. Именованные процессы и автоматическое распределение портов отложены до появления реальной нужды.
  • Официальный системный установщик отклонён: он требует root и создаёт systemd-службы. Пользовательская установка соответствует выбранной модели.
  • Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная VLESS-ссылка проще для пользователя и ограничивает область преобразования.

Риски и меры

  • Формат ссылок Hiddify может расширяться. Строгая проверка и явный отказ безопаснее частично неверного JSON.
  • Проверка открытого порта не доказывает работоспособность маршрута. CLI разделяет обязательную проверку процесса и необязательную внешнюю диагностику.
  • VLESS-ссылка содержит чувствительные данные. CLI сокращает секреты в выводе, не журналирует их и использует закрытые пользовательские файлы.
  • Устаревшие Hiddify-поля allowInsecure и insecure неоднозначны. Они не переносятся в Xray 26.3.27, сертификат проверяется обычным способом, а истинное значение сопровождается предупреждением.
  • Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до основной работы и сообщает обнаруженную и требуемую версии.
  • NO_PROXY реализуется программами неодинаково. README рекомендует точные IP и домены, а CIDR описывает как совместимость best-effort.
  • Разные пользователи делят сетевое пространство имён. Настраиваемый порт и явная ошибка конфликта предотвращают случайный запуск на занятом порту, но loopback без аутентификации не запрещает намеренное подключение другого локального пользователя. Это принятое ограничение первой версии.

Проверка реализации

  • Модульные тесты разбирают обезличенные ссылки обеих поддерживаемых семей и все показанные варианты ALPN. Отдельный регрессионный пример повторяет форму типичной ссылки Hiddify XHTTP/TLS с hiddify, core, extra, headerType=none, allowInsecure и insecure.
  • Неподдерживаемые транспорт, защита, отсутствующие обязательные поля и любой неизвестный query-параметр дают понятный отказ до запуска Xray.
  • Сгенерированный JSON проходит xray run -test на Xray 26.3.27 для заявленных сочетаний. Интеграционная проверка подтверждает, что один локальный вход принимает HTTP и SOCKS.
  • Тесты отдельно проверяют удаление устаревших insecure-полей.
  • Тесты подтверждают сокращение секретов и отсутствие полной VLESS-ссылки в stdout, stderr, служебных файлах и аргументах дочернего процесса.
  • Установка проверяется на корректной и повреждённой SHA-256 без записи вне пользовательского каталога.
  • Управление процессом покрывает повторный запуск, устаревший PID, чужой PID, повторно использованный PID, передачу сигналов, аварийно исчезнувший супервизор, очистку файлов, смену профиля, остановку и конфликт порта.
  • Сбой внешней проверки IP не отменяет успешный запуск, но отображается отдельно от состояния процесса и порта.
  • run сохраняет аргументы и код возврата, не оставляет proxy-переменные в родительской Bash-сессии и не запускает команду при недоступном прокси.
  • env не печатает частичный результат при ошибке, а shell.bash отклоняет неизвестные строки без eval и без частичного экспорта.
  • Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии машинного конфига, оба регистра proxy-переменных, настраиваемый NO_PROXY и отсутствие автоматического запроса внешнего IP в proxy_on/proxy_off.
  • Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере доступности: недоступная среда явно записывается как неподтверждённая, а поддержка заявляется только после проверки версии Python и полного прогона.

Влияние на документацию

Основная инструкция создаётся как xray-proxy/README.md одновременно с реализацией. Она должна описывать:

  • различие внешнего Hiddify и управляемого Xray;
  • выборочная установка через stow, ручные симлинки и копирование;
  • создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
  • команды управления, proxy_on/off, pclaude/pcodex;
  • настройка NO_PROXY, ограничения CIDR и диагностика;
  • поддерживаемые семейства VLESS и современную проверку сертификата;
  • отсутствие systemd, автозапуска и автоматического перехода между прокси.

Корневой README получает только короткий обзор, пример подключения пакета и ссылку на полную инструкцию клиента.

Шаблоны должны содержать только вымышленные значения. Настоящие VLESS-ссылки, UUID и ключи никогда не входят в репозиторий.