docs(proxy-client): спецификация под упрощённый код, ссылка из README

Спецификация 603 → 220 строк: без описаний снятых защит и без пересказа
кода и README; остались задача, границы, решения с причинами и риски.
В README раздел «Изменение кода» ведёт к спецификации и тестам.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5.5 committed 2026-09-28 09:36:56 -04:00
1 parent a5db265f47
commit e709c585fa
2 files changed
+149 -525

No files matched your search

+134 -517
View File
@@ -4,6 +4,10 @@
Статус: реализовано; 2026-09-28 упрощено по принципу ниже
Точное поведение, форматы, сообщения и команды описаны в коде,
`proxy-client --help` и [`proxy-client/README.md`](../../proxy-client/README.md).
Здесь — то, чего по коду не видно: задача, границы, выбор и его причины, риски.
## Принцип: личный инструмент
`proxy-client` пользуются владелец и несколько друзей на своих машинах. Код
@@ -13,8 +17,9 @@
- секреты профиля не попадают в вывод, служебные файлы закрыты правами 600/700;
- локальный порт Xray слушает только loopback;
- чужие файлы не перезаписываются;
- сигнал уходит только процессу, у которого `/proc/PID/exe` совпадает с
запущенным Xray.
- сигнал уходит только процессам из `state.json`: Xray — если его
`/proc/PID/exe` совпадает с запущенным файлом, супервизору — если Xray
подтверждён и в командной строке супервизора есть `proxy-client`.
Пользователь и его собственные файлы — не противник. Сообщение об ошибке
называет файл полным путём и подсказывает готовую команду.
@@ -34,570 +39,182 @@
На удалённых Linux-серверах нет GUI-клиента Hiddify. Перед запуском Claude Code
или Codex приходится вручную запускать Xray и выставлять proxy-переменные.
Переменные легко забыть, после чего агент стартует без прокси.
Переменные легко забыть, и агент стартует без прокси.
Нужны привычные команды `pclaude` и `pcodex`, которые не запускают агента,
если явно настроенный прокси недоступен. Решение должно работать как часть этого
репозитория и отдельно от остальных dotfiles.
если явно настроенный прокси недоступен. Решение работает и в составе этого
репозитория, и отдельно от остальных dotfiles.
## Контекст
Существующий дизайн
Прежний дизайн
[`pclaude` / `pcodex`](../superpowers/specs/2026-05-19-proxy-cli-launchers-design.md)
задаёт важный контракт: proxy-переменные видны только дочернему процессу,
аргументы и код возврата сохраняются, а недоступный прокси приводит к отказу
до запуска агента.
задаёт договор: proxy-переменные видны только дочернему процессу, аргументы и
код возврата сохраняются, недоступный прокси — отказ до запуска агента. Эта
спецификация заменяет его Linux-часть; Windows-часть не меняется.
Эта спецификация заменяет 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 принимает конфигурационные файлы, а не `vless://`-ссылки. Его SOCKS-вход
принимает SOCKS и HTTP на одном порту, поэтому прежняя схема proxy-переменных
подходит локальному Xray без второго порта. См.
[командную строку Xray](https://xtls.github.io/en/document/command.html) и
[SOCKS-вход](https://xtls.github.io/en/config/inbounds/socks.html).
## Цели
- Дать headless Linux-серверу управляемый пользовательский Xray без systemd и
прав root.
- Сохранить явные `pclaude` и `pcodex`, которые нельзя случайно запустить через
другой прокси или без настроенного прокси.
- Принимать одну точечную VLESS-ссылку Hiddify и преобразовывать её в клиентский
JSON Xray.
- Сохранить совместимость Bash-функций с внешним Hiddify.
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
- Поддержать самостоятельную установку из отдельно скопированного каталога
`proxy-client/` без зависимости от общего установщика dotfiles и без
обязательного stow.
- Управляемый пользовательский Xray на Linux-сервере без systemd и root.
- Явные `pclaude` и `pcodex`, которые нельзя случайно запустить через другой
прокси или без прокси.
- Одна VLESS-ссылка Hiddify превращается в клиентский JSON Xray.
- Внешний Hiddify работает, как раньше.
- Машинный `NO_PROXY` настраивается без правки файлов репозитория.
- Каталог `proxy-client/` можно скопировать отдельно и поставить своим
установщиком, без остальных dotfiles и без Stow.
## Не входит в объём
- Windows, PowerShell, Windows Codex launcher и VS Code Server.
- systemd, автозапуск после перезагрузки, sudo, firewall, TUN и изменение
системной маршрутизации.
- Автоматический выбор между Xray и Hiddify или переход на запасной прокси.
- Одновременное управление несколькими профилями одного Unix-пользователя.
- Универсальный импорт подписок или полного JSON-экспорта Hiddify.
- Протоколы и сочетания за пределами двух явно поддержанных семейств.
- pip, venv и любые сторонние Python-библиотеки.
- Сетевая изоляция голых `claude` и `codex`: как и раньше, гарантию намерения
дают явные команды с префиксом `p`.
- systemd, автозапуск, sudo, firewall, TUN и системная маршрутизация.
- Автоматический выбор между Xray и Hiddify или запасной прокси.
- Несколько одновременных профилей у одного Unix-пользователя.
- Импорт подписок или полного JSON-экспорта Hiddify.
- Протоколы за пределами двух поддержанных семейств VLESS.
- pip, venv и сторонние Python-библиотеки.
- Сетевая изоляция голых `claude` и `codex`: намерение выражают команды с
префиксом `p`.
## Рассмотренные варианты
### Только Bash
- **Только Bash.** URL-кодирование, JSON, контрольные суммы и управление
процессом делают Bash хрупким и плохо тестируемым.
- **Bash-контроллер и Python-преобразователь.** Граница между двумя языками
усложняет ошибки, тесты и единый источник настроек.
- **Один Python CLI и тонкий Bash-модуль.** Выбран. Python отвечает за данные,
установку Xray, процессы и запуск дочерних команд. Bash нужен только там, где
меняется окружение текущей shell-сессии.
- **Без менеджера**: готовые JSON и Xray вручную в tmux. Почти нет кода, но не
решает главного — безопасного `pclaude`/`pcodex` из ссылки Hiddify.
Ближе всего к принесённому прототипу, но URL-кодирование, JSON, проверка схемы,
контрольные суммы и безопасное управление процессом делают Bash-реализацию
хрупкой и плохо тестируемой.
Для отдельно скопированного пакета рассматривались: только инструкция для
ручных ссылок (легко забыть шаги), копирование файлов в `$HOME` (сложно
обновлять), обязательный Stow (лишняя зависимость на серверах) и свой
установщик. Выбран свой установщик, который ставит прямые ссылки: один способ,
нет второй копии файлов.
### Bash-контроллер и отдельный Python-преобразователь
## Решение
Оставляет жизненный цикл в Bash, а разбор VLESS переносит в Python. Граница
между двумя реализациями усложняет ошибки, тесты и единый источник настроек.
### Компоненты
### Один Python CLI и тонкий Bash-модуль
`proxy-client` — один исполняемый файл на Python 3.8+ без внешних библиотек:
установка Xray, запуск и остановка, проверка доступности, разбор VLESS, запуск
дочерней команды через прокси. Команды: `install`, `start`, `stop`, `status`,
`env`, `run`. Без подкоманды ничего не делается.
Python отвечает за данные, установку Xray, процессы и запуск дочерних команд.
Bash нужен только там, где необходимо изменить окружение текущей shell-сессии.
Это выбранный вариант.
`shell.bash` — единственный владелец `proxy_on`, `proxy_off`, `pclaude`,
`pcodex` и `myip`; в `bash/.bash_functions` их нет, чтобы не зависеть от
порядка `source`. Модуль сам конфиг не читает:
### Не создавать менеджер
- `pclaude` и `pcodex` вызывают `proxy-client run -- COMMAND ARG...`;
- `proxy_on` берёт готовые переменные из `proxy-client env` и применяет их,
только если команда завершилась успешно; `eval` не используется;
- `proxy_off` очищает переменные в обоих регистрах.
Можно хранить готовые JSON и вручную запускать Xray в tmux. Вариант почти не
требует кода, но не решает основную боль: безопасный запуск `pclaude` и
`pcodex` из точечной ссылки Hiddify.
`proxy_on`/`proxy_off` больше не запрашивают внешний IP: это замедляло
переключение. Для проверки есть `myip` и `proxy-client status`.
### Доставка отдельного пакета
### Настройки
Рассмотрены четыре способа подключить отдельно скопированный каталог:
Два файла в `~/.config/proxy-client/`:
- оставить только инструкции для stow и ручных симлинков — просто, но сохраняет
легко забываемые шаги;
- копировать файлы в домашний каталог — не требует сохранять исходный каталог,
но усложняет обновления и расходится с моделью dotfiles;
- требовать stow — даёт привычное управление ссылками, но добавляет
необязательную зависимость на удалённых серверах;
- дать пакету собственный установщик, который использует stow при наличии и
создаёт те же симлинки напрямую без него.
- `config` — несекретный машинный INI: тип `xray` или `external`, имя, адрес,
порт, добавка к `NO_PROXY`. Если файла нет, используется прежний Hiddify на
`hproxy.local:12334`.
- `default.url` — секретная VLESS-ссылка с правами 600; `start -f` берёт другой
файл.
Выбран последний вариант. Он сохраняет один результат установки при разном
составе машины и не требует второй копии обновляемых файлов.
Они разделены, потому что первый описывает политику машины, а второй — секрет,
который меняется независимо. Выбран INI, а не TOML: `tomllib` появился только
в Python 3.11.
## Рекомендуемое решение
### Явный тип прокси
### Границы компонентов
`xray` значит: нужен Xray, запущенный через `proxy-client start`. Если его нет,
агент не запускается. `external` значит: проверяется только доступность адреса
и порта. Ни один тип не переходит на другой сам — предсказуемость канала важнее
удобства.
`proxy-client` — один исполняемый файл на Python 3.8+ без внешних библиотек. Он
отвечает за установку Xray, запуск и остановку процесса, диагностику,
преобразование VLESS и запуск произвольной дочерней команды через прокси.
Нейтральное публичное имя описывает клиентскую прокси-подсистему целиком:
внешний прокси и управляемый Xray. Название `xray` остаётся у конкретного
движка и значения `type = xray`, где оно действительно важно.
`start` работает только при `type=xray` и loopback-адресе в конфиге. Поэтому
нельзя поднять локальный Xray, оставив `pclaude` направленным во внешний
Hiddify. `stop` от типа не зависит: после переключения на `external` старый
Xray всё равно можно остановить.
Переносимый `shell.bash` — единственный владелец `proxy_on`, `proxy_off`,
`pclaude`, `pcodex`, `myip` и внутренних proxy-помощников. При реализации они
удаляются из `bash/.bash_functions`, чтобы результат не зависел от порядка
`source`. `update_hproxy` остаётся там как отдельная WSL-утилита управления
`/etc/hosts`, а не часть переносимого интерфейса прокси. Модуль не разбирает
машинный конфиг самостоятельно:
Один порт на HTTP и SOCKS, по умолчанию `12334`; порт настраивается, чтобы
разные пользователи одной машины не мешали друг другу.
- `pclaude` и `pcodex` делегируют запуск форме
`proxy-client run -- COMMAND ARG...`, где `--` отделяет параметры клиента;
- `proxy_on` получает от `proxy-client env` проверенный набор proxy-переменных и
применяет его к текущей Bash-сессии;
- `proxy_off` очищает верхний и нижний регистр proxy-переменных.
### VLESS-ссылки
Общий `bash/.bash_functions` и модуль клиента не зависят друг от друга. В составе
dotfiles `.bash_custom` подключает их двумя независимыми строками. Без
остальных dotfiles пользователь добавляет только один `source` модуля клиента в
`.bashrc`. Для неинтерактивных сценариев и других оболочек остаётся прямой
`proxy-client run`.
Поддержаны две семьи: TCP/RAW с REALITY и XHTTP с TLS. Неизвестный или
повторный query-параметр — отказ: он может менять смысл соединения, и его
нужно добавить в разбор явно. Остальное проверяет `xray run -test`.
`proxy-client env` сначала выполняет ту же проверку доступности, что и `run`, а
затем печатает восемь строк `KEY=value`: HTTP, HTTPS, ALL и NO_PROXY в верхнем
и нижнем регистре. `shell.bash` сначала целиком получает успешный ответ, затем
разбирает строки по первому `=`, принимает только известные ключи и лишь после
полной проверки экспортирует полученные значения через `printf -v`. `eval` не
используется, частичного изменения окружения при ошибке нет.
Hiddify Manager добавляет `allowInsecure`/`insecure` даже к профилям с
нормальным сертификатом
([Hiddify-Manager#5438](https://github.com/hiddify/Hiddify-Manager/issues/5438)),
а Xray [26.2.6](https://github.com/XTLS/Xray-core/releases/tag/v26.2.6) эту
опцию удалил. Поэтому поля принимаются и игнорируются с предупреждением;
TLS-проверку задают `pcs` и `vcn`.
У `env` stdout зарезервирован только для восьми машинно-читаемых строк.
Предупреждения и ошибки идут в stderr; обычная безопасная сводка этой командой
не печатается.
Совместимость существующих Bash-команд означает сохранение имён функций,
набора переменных, fail-fast перед `pclaude`/`pcodex` и изоляции окружения
дочернего процесса. Автоматический запрос внешнего IP из `proxy_on` и
`proxy_off` удаляется: он замедляет переключение и смешивает изменение
окружения с диагностикой. Для проверки остаются `myip` и `proxy-client status`.
### Два независимых файла настройки
Машинный файл `~/.config/proxy-client/config` хранит несекретное описание
активного прокси: явный тип `xray` или `external`, отображаемое имя, адрес, порт
и добавку к `NO_PROXY`. Формат — INI из стандартной библиотеки Python. Если
файла нет, прокси-подсистема сохраняет прежний внешний адрес
`hproxy.local:12334` ради обратной совместимости.
Секретный `~/.config/proxy-client/default.url` содержит одну VLESS-ссылку.
Команда запуска принимает другой файл через `-f`. Профили можно хранить в
нескольких файлах, но активным у пользователя бывает только один.
CLI следует за симлинком, но проверяет права конечного файла: доступ группы или
остальных пользователей считается ошибкой с подсказкой выполнить `chmod 600`.
Файл должен содержать ровно одну непустую строку без комментариев и служебного
текста.
Базовый `NO_PROXY` всегда включает loopback-адреса и узел самого прокси.
Машинная добавка расширяет его локальными IP и доменами. Документация должна
предупреждать, что поддержка CIDR в `NO_PROXY` различается между программами.
### Явная семантика прокси
Тип `xray` означает, что `pclaude` и `pcodex` требуют запущенный процесс,
которым управляет `proxy-client`. Если его нет, агент не запускается, а сообщение
предлагает выполнить `proxy-client 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](https://xtls.github.io/en/config/transports/reality.html),
а XHTTP с TLS — на
[схему TLS](https://xtls.github.io/en/config/transports/tls.html).
Для 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](https://github.com/XTLS/Xray-core/blob/v26.3.27/infra/conf/transport_internet.go#L3591-L3636)
нет полей Host или ALPN. Повтор одного query-параметра считается ошибкой.
Остальные правила соответствуют
[стандарту VLESS-ссылок Xray](https://github.com/XTLS/Xray-core/discussions/716).
Основной сценарий 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-ссылок](https://github.com/XTLS/Xray-core/discussions/716).
Перед запуском 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](https://github.com/hiddify/Hiddify-Manager/issues/5438).
Современный Xray больше не поддерживает `allowInsecure`; удаление зафиксировано
в [релизе 26.2.6](https://github.com/XTLS/Xray-core/releases/tag/v26.2.6).
Преобразователь принимает для обоих полей только точные строчные значения
`true` и `false`. Если присутствуют оба поля, их значения должны совпадать;
конфликт и любое другое написание отклоняются до создания JSON. Для Xray
26.3.27 поля не переносятся в JSON; истинное значение даёт предупреждение, что
сертификат будет проверен обычным способом, а ложное не требует предупреждения.
Соединение после этого может успешно заработать или завершиться ошибкой
сертификата; отдельного раннего отказа нет.
Сводка перед запуском не показывает UUID, ключи REALITY и содержимое XHTTP
`path` (там бывает токен). Ссылка не передаётся аргументом командной строки.
### Установка Xray
`proxy-client install` — отдельная явная операция. Она устанавливает Xray в
`~/.local/bin`, скачивает официальный архив и файл контрольной суммы, затем
проверяет SHA-256. Поддерживается только зафиксированный релиз 26.3.27;
параметра версии нет. Официальные релизы публикуют необходимые файлы `.dgst`:
[релиз 26.3.27](https://github.com/XTLS/Xray-core/releases/tag/v26.3.27).
Для Linux `x86_64`/`amd64` используется `Xray-linux-64.zip`, для
`aarch64`/`arm64` — `Xray-linux-arm64-v8a.zip`; остальные архитектуры
отклоняются. Официальный `.dgst` содержит записи `MD5`, `SHA1`, `SHA2-256` и
`SHA2-512`; принимается ровно один полный набор со строчными hex-значениями,
для проверки архива используется `SHA2-256`. Дубликаты, uppercase, отсутствие
записи и мусор отклоняются. Архив и извлечённый бинарник ограничены 64 MiB,
`.dgst` — 4 KiB; фактический размер считается с limit+1 и должен совпадать с
объявленным `Content-Length`. Архив распаковывается в закрытый staging-каталог
внутри `~/.local/bin`, проверенный бинарник запускается с `version`, после чего
одним `os.replace` атомарно переносится в `~/.local/bin/xray`.
Корректная существующая версия 26.3.27 даёт no-op без сети. Другой файл или
симлинк не заменяется без `--replace`, а каталог не заменяется никогда.
Start-lock берётся до проверки назначения и удерживается до завершения
`os.replace`; живой, сиротский или неподтверждённый managed-state запрещает
установку с подсказкой сначала выполнить `stop`.
После установки CLI проверяет, что первый `xray` из `PATH` разрешается в
установленный файл; иначе завершается с подсказкой поставить `~/.local/bin` в
начало `PATH`, не откатывая готовую установку. `start` всегда показывает
фактический путь и версию выбранного бинарника.
Перед созданием JSON CLI разрешает первый `xray` из `PATH` до одного пути и
запускает этим файлом `xray version`. Первая строка должна начинаться с точного
токена `Xray VERSION`, после полного токена версии допускается только конец
строки либо ASCII-пробел. Принимается только версия 26.3.27; ведущий
мусор, похожий более длинный токен, пустой, неверный, завершившийся ошибкой или
содержащий другую версию вывод запрещает запуск. Тот же разрешённый файл
используется для `run -test` и основного процесса. Версия никогда не выводится
из содержимого VLESS-ссылки.
`start` никогда не скачивает бинарник. Если `xray` отсутствует в `PATH`, команда
завершается с подсказкой выполнить `proxy-client install`.
`proxy-client install` ставит в `~/.local/bin/xray` официальный архив одного
зафиксированного релиза (сейчас 26.3.27), проверяет SHA-256 по `.dgst` и
заменяет файл атомарно. Официальный установщик Xray не подходит: он требует
root и создаёт systemd-службы. `start` принимает только ту же версию и никогда
ничего не скачивает.
### Жизненный цикл
CLI предоставляет `install`, `start`, `stop`, `status`, `env` и `run`. Запуск
без подкоманды не устанавливает и не стартует ничего.
`start` остаётся на переднем плане как небольшой Python-супервизор с одним
дочерним Xray; ожидаемое место — панель tmux. Отдельный журнал и команда `logs`
не нужны: хватает истории tmux. Второй `start` отсекает блокировка `flock`.
`state.json` хранит PID супервизора и Xray, чтобы `stop` работал из любого
терминала и мог добить Xray, если супервизор убит.
`start` остаётся на переднем плане как небольшой Python-супервизор и занимает
текущую консоль. Он запускает один дочерний Xray без демонизации и без новой
сессии процесса, ждёт его завершения и передаёт ему SIGINT, SIGTERM и SIGHUP.
Ожидаемый способ работы — отдельная панель tmux или другого мультиплексора.
Отсоединение от tmux не останавливает процесс, но закрытие панели или сессии
останавливает Xray. Отдельный постоянный журнал и команда `logs` не нужны:
достаточно истории tmux.
Перед запуском обязательны разбор ссылки, `xray run -test` и открытый порт
после старта. Проверка внешнего IP — только диагностика: её сбой даёт
предупреждение.
Супервизор на всё время работы удерживает неблокирующую эксклюзивную блокировку
`fcntl.flock` в служебном каталоге `~/.local/state/proxy-client/`. Второй `start`
отказывает до запуска процессов и изменения state или сгенерированного
конфига. `state.json` нужен для восстановления после сбоя, но не служит
блокировкой.
Файл состояния содержит ровно `supervisor_pid`, `xray_pid`, `xray_path`,
`config_path`, `address`, `port`, `supervisor_path`, `supervisor_starttime`,
`xray_starttime` и `boot_id`. Время старта — поле 22 `/proc/PID/stat`; поле
`comm` сначала отделяется по последней `)`. Для обоих процессов проверяются PID,
точное время старта и `boot_id`. У ребёнка дополнительно сверяется канонический
`/proc/PID/exe` с `xray_path`. У Python-супервизора операнд скрипта из
`/proc/PID/cmdline` разрешается относительно `/proc/PID/cwd` и сравнивается с
каноническим `supervisor_path`. Ошибка чтения или разбора означает несовпадение.
Непосредственно перед каждым сигналом идентичность читается повторно.
Проверка состояния различает три исхода. Для здоровой пары `stop` посылает
SIGTERM супервизору. Если супервизор исчез, но ребёнок подтверждён, `status`
сообщает о сироте с ненулевым кодом, `env` и `run` отказывают, а `stop` посылает
сигнал ребёнку напрямую без требования доступности порта и удаляет state и
конфиг. При живом, но неподтверждённом PID сигналы не посылаются, файлы
сохраняются, а диагностика требует ручной проверки и удаления. Если оба PID
мертвы или `boot_id` не совпадает, устаревшие state и конфиг можно безопасно
удалить без сигналов.
У пользователя может быть только один управляемый экземпляр. Повторный `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` не имеют скрытого автозапуска.
`run` никогда не запускает Xray, поэтому у `pclaude` и `pcodex` нет скрытого
автозапуска.
### Доставка
Компоненты образуют отдельный stow-совместимый каталог `proxy-client/`. Он
доставляет CLI, Bash-модуль и три шаблона: машинного INI для внешнего прокси,
машинного INI для Xray и файла VLESS-ссылки.
Реальные пользовательские файлы не управляются stow и не входят в Git.
CLI раскладывается как `~/.local/bin/proxy-client`. Машинный конфиг, профиль,
модуль `shell.bash` и каталог `examples/` находятся вместе в
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
файлы.
Каталог `proxy-client/` раскладывается в `$HOME` ссылками: CLI в
`~/.local/bin/`, `shell.bash` и `examples/` в `~/.config/proxy-client/`.
Машинный `config` и `default.url` пользователь создаёт сам; в Git они не
входят.
В корне этого каталога находятся самостоятельный `README.md` и пакетный
`install.sh`. Оба файла относятся к исходному каталогу и исключаются из
раскладки stow. README остаётся рядом с кодом при выборочном клонировании или
копировании каталога. Корневой README репозитория только кратко описывает
подсистему и ссылается на него.
- Отдельно: `proxy-client/install.sh` ставит эти три ссылки и по умолчанию
дописывает блок в `~/.bashrc`. Чужие файлы не трогает. Режим прокси, Xray и
сеть не настраивает — это делают отдельные команды по README.
- В составе dotfiles: корневой `install.sh` подключает тот же пакет через Stow,
а `.bash_custom` загружает `shell.bash`.
Пакетный установщик связывает отдельно скопированный каталог с домашним
каталогом и не зависит от положения или наличия остальных dotfiles. Если stow
доступен, установщик использует его; если нет — создаёт тот же набор симлинков
напрямую. Выбранный механизм явно показывается пользователю, но не меняет
результат. Исходный каталог должен оставаться на прежнем пути всё время
использования ссылок.
## Риски
Установщик управляет только тремя публичными артефактами: CLI, `shell.bash` и
каталогом шаблонов. Машинный `config` и закрытый `default.url` не входят в
раскладку stow, никогда не создаются, не заменяются и не связываются
установщиком. Их наличие в исходном каталоге также не должно расширять набор
управляемых путей.
Домашние `~/.local/bin` и `~/.config/proxy-client` остаются настоящими
каталогами. Stow не должен сворачивать их или их родителей в одну ссылку на
исходное дерево: ссылками становятся только три перечисленных артефакта. Это
одновременно сохраняет место для машинных файлов и делает результат stow
совпадающим с прямой раскладкой через `ln -s`.
Перед записью установщик проверяет весь набор назначений. Правильная ссылка на
текущий исходный каталог считается уже установленной; любой другой файл,
каталог или симлинк считается конфликтом. При конфликте установщик ничего не
меняет и сообщает все пути, которые пользователь должен разобрать вручную.
Принудительной замены и автоматических резервных копий нет.
По умолчанию установщик идемпотентно подключает Bash-модуль в `~/.bashrc` и при
необходимости добавляет `~/.local/bin` в `PATH`. Уже существующее подключение
через dotfiles не дублируется. Управляемый блок помечается, повторно не
добавляется и может быть полностью отключён параметром `--no-shell-setup`.
Установщик не выбирает режим прокси, не создаёт конфигурацию, не читает VLESS,
не устанавливает Xray и не проверяет сеть. После раскладки он проверяет только
локальные требования и исправность CLI и Bash-модуля, затем печатает краткую
подсказку для текущей shell-сессии и направляет пользователя в README за
настройкой режима. Недоступный прокси не делает установку неуспешной.
Полный корневой `install.sh` продолжает подключать тот же stow-пакет вместе с
остальными dotfiles и не получает отдельной реализации этой логики.
## Решения и отклонённые альтернативы
- Выбран Python 3.8+, потому что стандартной библиотеки достаточно для URL,
JSON, INI, ZIP, SHA-256, HTTP, процессов и тестов. Более новый синтаксис и
сторонние пакеты запрещены ради старых и непредсказуемых серверных систем.
- Выбран INI, а не TOML: модуль
[`tomllib`](https://docs.python.org/3/library/tomllib.html) добавлен только в
Python 3.11, что исключило бы штатный Python 3.10 в Ubuntu 22.04.
- Bash оставлен отдельным тонким модулем, потому что дочерний Python-процесс не
может изменить окружение родительской shell-сессии.
- Выбрано публичное имя `proxy-client`, потому что подсистема обслуживает и
внешний прокси, и управляемый Xray. `xray-proxy` отклонено как имя,
выпячивающее одну реализацию, а `proxyctl` — как менее понятное пользователю.
- Все пользовательские файлы собраны в `~/.config/proxy-client/`. Разделение
между общим машинным конфигом и Xray-профилем остаётся логическим, но не
требует помнить два каталога.
- Машинный прокси и VLESS-ссылка разделены: первый файл описывает локальную
политику shell, второй содержит секрет и меняется независимо.
- Выбран один порт, потому что SOCKS-вход Xray совместим с HTTP и не требует
второго локального слушателя.
- Выбран явный тип прокси вместо автообнаружения. Предсказуемость канала важнее
удобства автоматического перехода.
- Выбрана одна активная конфигурация на пользователя. Именованные процессы и
автоматическое распределение портов отложены до появления реальной нужды.
- Официальный системный установщик отклонён: он требует root и создаёт
systemd-службы. Пользовательская установка соответствует выбранной модели.
- Копирование файлов пакета отклонено: оно создаёт отдельные экземпляры,
усложняет обновление и нарушает общую с dotfiles модель симлинков.
- Обязательный stow отклонён как лишняя зависимость. Прямые симлинки дают тот же
результат на машине без stow, а автоматический выбор механизма не меняет
публичное поведение пакета.
- Автоматическая настройка режима, установка Xray и проверка сети отклонены для
установщика пакета. Эти операции зависят от машины, могут требовать секретов
или сети и уже имеют отдельные явные команды и инструкции.
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
VLESS-ссылка проще для пользователя и ограничивает область преобразования.
## Риски и меры
- Формат ссылок Hiddify может расширяться. Строгая проверка и явный отказ
безопаснее частично неверного JSON.
- Проверка открытого порта не доказывает работоспособность маршрута. CLI
разделяет обязательную проверку процесса и необязательную внешнюю диагностику.
- VLESS-ссылка содержит чувствительные данные. CLI сокращает секреты в выводе,
не журналирует их и использует закрытые пользовательские файлы.
- Устаревшие Hiddify-поля `allowInsecure` и `insecure` неоднозначны. Они не
переносятся в Xray 26.3.27, сертификат проверяется обычным способом, а
истинное значение сопровождается предупреждением.
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
основной работы и сообщает обнаруженную и требуемую версии.
- Симлинки зависят от постоянного пути исходного каталога. Установщик явно
сообщает эту зависимость и при повторном запуске принимает только ссылки на
текущий источник.
- Изменение `~/.bashrc` может конфликтовать с пользовательским способом загрузки
оболочки. Установщик распознаёт собственный блок и известное подключение из
dotfiles, не дублирует их и предоставляет `--no-shell-setup`.
- Частичная установка может оставить неоднозначное состояние. Все конфликты
назначений проверяются до изменений; проверки охватывают отсутствие новых
ссылок и правок shell при отказе.
- `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`.
- Проверки самостоятельной установки покрывают путь со stow и без него,
одинаковый набор итоговых ссылок, запуск не из каталога пакета и каталог с
пробелами в пути.
- В обоих путях домашние каталоги остаются обычными каталогами, а пакетные
`README.md` и `install.sh` не появляются в домашнем каталоге через stow.
- Повторный запуск не меняет уже правильную установку. Конфликт любого
назначения обнаруживается до записи; чужие файлы, каталоги и ссылки остаются
неизменными.
- Машинный `config` и закрытый `default.url` не управляются установщиком даже
при их наличии в исходном каталоге или домашнем каталоге.
- Настройка Bash проверяется для нового `~/.bashrc`, уже помеченного блока,
подключения из полного dotfiles и режима `--no-shell-setup`. Добавление
`~/.local/bin` не дублируется.
- Локальная проверка установки не обращается к сети и не требует доступного
прокси. Неисправный или отсутствующий машинный конфиг не отменяет успешную
раскладку пакета.
- Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
доступности: недоступная среда явно записывается как неподтверждённая, а
поддержка заявляется только после проверки версии Python и полного прогона.
## Влияние на документацию
Основная инструкция находится в `proxy-client/README.md` рядом с
реализацией. Она должна описывать:
- различие внешнего Hiddify и управляемого Xray;
- самостоятельный `install.sh`, его автоматический выбор stow или прямых
симлинков, требование сохранять исходный каталог и режим
`--no-shell-setup`;
- границу между управляемыми файлами пакета и создаваемыми вручную машинным
конфигом и закрытым профилем;
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
- команды управления, `proxy_on/off`, `pclaude/pcodex`;
- настройка `NO_PROXY`, ограничения CIDR и диагностика;
- поддерживаемые семейства VLESS и современную проверку сертификата;
- отсутствие systemd, автозапуска и автоматического перехода между прокси.
Корневой README получает только короткий обзор, пример подключения пакета и
ссылку на полную инструкцию клиента.
Шаблоны должны содержать только вымышленные значения. Настоящие VLESS-ссылки,
UUID и ключи никогда не входят в репозиторий.
- Формат ссылок Hiddify расширяется. Строгий отказ на неизвестном параметре
лучше частично неверного JSON, но новые ссылки могут потребовать правки
разбора.
- Открытый порт не доказывает, что маршрут работает. Проверка внешнего IP
отделена от обязательных проверок.
- Loopback без аутентификации открыт другим пользователям того же узла.
Пользователи машины считаются доверенными.
- `NO_PROXY` программы понимают по-разному, особенно CIDR. README советует
точные IP и домены.
- Ссылки установки зависят от пути исходного каталога: переносить его нельзя.
- Поддержка РЕД ОС, Astra Linux и Alt Linux заявляется только после проверки
версии Python и прогона `tests/test_proxy_client_full.sh` на них.
- Шаблоны в `examples/` содержат только вымышленные значения; настоящие ссылки,
UUID и ключи в репозиторий не попадают.
+15 -8
View File
@@ -76,12 +76,6 @@ source ~/.bash_custom
Если `~/.local/bin` отсутствует в `PATH` и в новых сессиях, добавьте ту же строку
`export` в свой Bash-конфиг: корневой установщик dotfiles его не изменяет.
Проверить пакет после правок (из корня dotfiles):
```bash
tests/test_proxy_client_full.sh
```
## Как выбирается прокси
Режим определяется только машинным файлом
@@ -407,5 +401,18 @@ CLI скачивает официальный архив и файл `.dgst`, с
Если файл профиля доступен другим пользователям, `start` не запустится и
покажет готовую команду `chmod 600` для конечного файла (у симлинка — для файла,
на который он указывает). Служебный каталог закрыт для других пользователей
(`700`, файлы в нём — `600`) и очищается после остановки Xray. Сбой определения внешнего IP показывается как предупреждение и
не останавливает уже готовый процесс.
(`700`, файлы в нём — `600`) и очищается после остановки Xray. Сбой
определения внешнего IP показывается как предупреждение и не останавливает уже
готовый процесс.
## Изменение кода
Перед правкой proxy-client прочитайте
[спецификацию](../docs/specs/2026-07-15-proxy-client-design.md): там сказано,
от чего клиент защищается, от чего нет и почему, и где живут тесты.
Все тесты пакета (из корня dotfiles):
```bash
tests/test_proxy_client_full.sh
```