From e709c585fab353f3831a2240abdb95b3762e2824 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Mon, 28 Sep 2026 09:36:56 -0400 Subject: [PATCH] =?UTF-8?q?docs(proxy-client):=20=D1=81=D0=BF=D0=B5=D1=86?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=20=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BA=D0=BE=D0=B4,=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B8=D0=B7=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Спецификация 603 → 220 строк: без описаний снятых защит и без пересказа кода и README; остались задача, границы, решения с причинами и риски. В README раздел «Изменение кода» ведёт к спецификации и тестам. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specs/2026-07-15-proxy-client-design.md | 651 ++++--------------- proxy-client/README.md | 23 +- 2 files changed, 149 insertions(+), 525 deletions(-) diff --git a/docs/specs/2026-07-15-proxy-client-design.md b/docs/specs/2026-07-15-proxy-client-design.md index 6a06f3c..4f4c823 100644 --- a/docs/specs/2026-07-15-proxy-client-design.md +++ b/docs/specs/2026-07-15-proxy-client-design.md @@ -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 и ключи в репозиторий не попадают. diff --git a/proxy-client/README.md b/proxy-client/README.md index 844a20d..a47c510 100644 --- a/proxy-client/README.md +++ b/proxy-client/README.md @@ -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 +```