- Зачем: - нужен зафиксированный контракт переносимого Xray-клиента для удалённых Linux-систем. - Что: - описаны Python CLI, отдельный Bash-модуль и совместимость с внешним Hiddify. - закреплены преобразование VLESS-ссылок, версии Xray и обработка TLS. - определены жизненный цикл в tmux, безопасность файлов и проверки реализации. - Проверка: - выполнена проверка `git diff --cached --check`.
39 KiB
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 в верхнем
и нижнем регистре. Переносы строк и NUL в значениях запрещены. 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;extraURL-декодируется, разбирается как 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 и локальный порт. Обычный
режим не выводит подробный журнал Xray: -v добавляет информационные сообщения,
а --debug включает подробную диагностику. UUID, REALITY password/public key,
short ID и другие чувствительные значения выводятся только сокращённо.
В создаваемом JSON журнал доступа отключён, а уровень ошибок по умолчанию —
warning.
Hiddify Manager может добавлять allowInsecure=true и insecure=true даже к
профилю с действительным публичным сертификатом — этот дефект описан в
hiddify/Hiddify-Manager#5438.
Современный Xray больше не поддерживает allowInsecure; удаление зафиксировано
в релизе 26.2.6.
Поэтому преобразователь принимает эти поля, но для Xray 26.3.27 не переносит
их в JSON и предупреждает, что сертификат будет проверен обычным способом.
Соединение после этого может успешно заработать или завершиться ошибкой
сертификата; отдельного раннего отказа нет.
При явно выбранном Xray 25.12.8 преобразователь сохраняет
allowInsecure=true и предупреждает одновременно об отключённой проверке
сертификата и об использовании старой совместимой версии. Автоматический откат
с 26.x на 25.12.8 запрещён. Новые pcs/vcn в режиме 25.12.8 отклоняются как
несовместимые вместо попытки угадать их старое представление.
Установка 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 version и выбирает
только совместимое с ней представление TLS-полей. Версия никогда не выводится
из содержимого VLESS-ссылки.
Xray 25.12.8 — документированный запасной вариант только для старых Hiddify-
профилей, которым действительно требуется allowInsecure=true. Это последняя
версия ветки 25.x, где поле применяется без календарного запрета:
релиз 25.12.8 и
реализация TLS 25.12.8.
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-ссылка не
попадает в обычный вывод и никогда не передаётся аргументом командной строки.
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.x игнорирует их на уровне преобразователя и проверяет сертификат; Xray 25.12.8 сохраняет их только после явного выбора версии. Оба пути сопровождаются предупреждением. - Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до основной работы и сообщает обнаруженную и требуемую версии.
NO_PROXYреализуется программами неодинаково. README рекомендует точные IP и домены, а CIDR описывает как совместимость best-effort.- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и явная ошибка конфликта предотвращают случайный запуск на занятом порту, но loopback без аутентификации не запрещает намеренное подключение другого локального пользователя. Это принятое ограничение первой версии.
Проверка реализации
- Модульные тесты разбирают обезличенные ссылки обеих поддерживаемых семей и
все показанные варианты ALPN. Отдельный регрессионный пример повторяет форму
типичной ссылки Hiddify XHTTP/TLS с
hiddify,core,extra,headerType=none,allowInsecureиinsecure. - Неподдерживаемые транспорт, защита, отсутствующие обязательные поля и неизвестные критичные параметры дают понятный отказ до запуска Xray.
- Сгенерированный JSON проходит
xray run -testотдельно на Xray 26.3.27 и 25.12.8 для заявленных совместимыми сочетаний. Интеграционная проверка каждой версии подтверждает, что один локальный вход принимает HTTP и SOCKS. - Тесты отдельно проверяют современное удаление устаревших insecure-полей и их сохранение для явно выбранного Xray 25.12.8.
- Тесты подтверждают сокращение секретов и отсутствие полной 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 и на одной целевой удалённой системе; отдельным пунктом проверяется доступная версия Python в РЕД ОС перед заявлением её поддержки.
Влияние на документацию
Основная инструкция создаётся как xray-proxy/README.md одновременно с
реализацией. Она должна описывать:
- различие внешнего Hiddify и управляемого Xray;
- выборочная установка через stow, ручные симлинки и копирование;
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
- команды управления,
proxy_on/off,pclaude/pcodex; - настройка
NO_PROXY, ограничения CIDR и диагностика; - поддерживаемые семейства VLESS, современную проверку сертификата и явный
запасной вариант Xray 25.12.8 для
allowInsecure; - отсутствие systemd, автозапуска и автоматического перехода между прокси.
Корневой README получает только короткий обзор, пример подключения пакета и ссылку на полную инструкцию клиента.
Шаблоны должны содержать только вымышленные значения. Настоящие VLESS-ссылки, UUID и ключи никогда не входят в репозиторий.