feat(xray-proxy): добавлен управляемый прокси для CLI

Зачем:
- отделить переносимый прокси-интерфейс от общих Bash-функций
- запускать агентские CLI через явный внешний или управляемый прокси

Что:
- добавлен stow-пакет xray-proxy с CLI, Bash-модулем и примерами
- реализованы строгий разбор VLESS и управляемый запуск Xray
- добавлены проверки процесса, порта, сигналов и отсутствия утечек
- сохранены спецификация и очередь следующих тикетов

Проверка:
- python3 tests/test_xray_proxy.py — 22/22
- python3 tests/test_xray_proxy_managed.py — 16/16
- bash tests/test_proxy_launchers.sh — 9/9
- bash tests/test_install.sh
- bash -n, Python 3.8 grammar, git diff --check
This commit is contained in:
ddadmin committed 2026-07-20 00:00:59 +03:00
1 parent fdf744e578
commit e69a4bb7df
20 files changed
+3224 -170

No files matched your search

@@ -0,0 +1,6 @@
[proxy]
type = external
name = Hiddify
address = hproxy.local
port = 12334
no_proxy_extra = .example.internal,git.example.internal
@@ -0,0 +1 @@
vless://11111111-2222-4333-8444-555555555555@example.invalid:443?type=tcp&security=reality&encryption=none&flow=xtls-rprx-vision&sni=front.example.invalid&fp=chrome&pbk=SYNTHETIC_PUBLIC_KEY&sid=SYNTHETIC_SHORT_ID&headerType=none#Synthetic-example
+116
View File
@@ -0,0 +1,116 @@
# Переносимый Bash-интерфейс xray-proxy. Файл можно подключать отдельно от
# остальных dotfiles: source ~/.config/xray-proxy/shell.bash
_proxy_cli() {
"${XRAY_PROXY_BIN:-xray-proxy}" "$@"
}
_proxy_apply_env_file() {
local output_file="$1" line key value nul_prefix
local -A proxy_values=()
local -A proxy_seen=()
local -a allowed_keys=(
HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
http_proxy https_proxy all_proxy no_proxy
)
if IFS= read -r -d '' nul_prefix < "$output_file"; then
echo "Ошибка: xray-proxy env вернул запрещённый NUL" >&2
return 1
fi
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" == *$'\r'* ]]; then
echo "Ошибка: xray-proxy env вернул запрещённый CR" >&2
return 1
fi
if [[ "$line" != *=* ]]; then
echo "Ошибка: xray-proxy env вернул строку без '='" >&2
return 1
fi
key="${line%%=*}"
value="${line#*=}"
case "$key" in
HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|http_proxy|https_proxy|all_proxy|no_proxy) ;;
*)
echo "Ошибка: xray-proxy env вернул неизвестный ключ: $key" >&2
return 1
;;
esac
if [[ -n "${proxy_seen[$key]+set}" ]]; then
echo "Ошибка: xray-proxy env повторил ключ: $key" >&2
return 1
fi
proxy_seen["$key"]=1
proxy_values["$key"]="$value"
done < "$output_file"
for key in "${allowed_keys[@]}"; do
if [[ -z "${proxy_seen[$key]+set}" ]]; then
echo "Ошибка: xray-proxy env не вернул обязательный ключ: $key" >&2
return 1
fi
done
if [ "${#proxy_seen[@]}" -ne 8 ]; then
echo "Ошибка: xray-proxy env должен вернуть ровно восемь ключей" >&2
return 1
fi
for key in "${allowed_keys[@]}"; do
printf -v "$key" '%s' "${proxy_values[$key]}"
done
export HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
export http_proxy https_proxy all_proxy no_proxy
}
proxy_on() {
local output_file old_umask
old_umask="$(umask)"
umask 077
if ! output_file="$(mktemp "${TMPDIR:-/tmp}/xray-proxy-env.XXXXXX")"; then
umask "$old_umask"
echo "Ошибка: не удалось создать временный файл для proxy-env" >&2
return 1
fi
umask "$old_umask"
if ! _proxy_cli env > "$output_file"; then
rm -f "$output_file"
echo "Ошибка: прокси не включён" >&2
return 1
fi
if ! _proxy_apply_env_file "$output_file"; then
rm -f "$output_file"
echo "Ошибка: прокси не включён" >&2
return 1
fi
rm -f "$output_file"
echo "Прокси включён"
}
proxy_off() {
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
unset http_proxy https_proxy all_proxy no_proxy
echo "Прокси выключен"
}
_proxy_run() {
local command_name="${1:-}"
if [ -z "$command_name" ]; then
echo "Ошибка: не указана команда" >&2
return 2
fi
shift
_proxy_cli run -- "$command_name" "$@"
}
pclaude() { _proxy_run claude "$@"; }
pcodex() { _proxy_run codex "$@"; }
myip() {
local address
if ! address="$(curl -fsS --max-time 3 https://ifconfig.me)"; then
echo "Ошибка: не удалось определить внешний IP" >&2
return 1
fi
printf 'IP: %s\n' "$address"
}
+1123
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
^/README\.md$
+147
View File
@@ -0,0 +1,147 @@
# xray-proxy
Переносимый Linux/Bash-интерфейс для безопасного запуска команд через явно
настроенный внешний прокси или через управляемый пользовательский Xray.
CLI требует Python 3.8+ и использует только стандартную библиотеку. Он ничего не
устанавливает и не запускает без явной подкоманды. `start` использует первый
`xray` из `PATH`; скачивания и установки при запуске нет.
## Подключение
В составе dotfiles:
```bash
cd ~/dotfiles
stow xray-proxy
source ~/.config/xray-proxy/shell.bash
```
Полный `./install.sh` выполняет тот же `stow` вместе с остальными пакетами, а
`.bash_custom` подключает модуль автоматически.
Без остальных dotfiles можно создать две ссылки:
```bash
mkdir -p ~/.local/bin ~/.config/xray-proxy
ln -s /path/to/xray-proxy/.local/bin/xray-proxy ~/.local/bin/xray-proxy
ln -s /path/to/xray-proxy/.config/xray-proxy/shell.bash \
~/.config/xray-proxy/shell.bash
printf '%s\n' 'source ~/.config/xray-proxy/shell.bash' >> ~/.bashrc
```
Вместо ссылок файлы можно скопировать в те же пути. Исполняемому файлу нужен
режим `chmod 755`. Каталог `~/.local/bin` должен входить в `PATH`.
## Машинный конфиг
Скопируйте пример и отредактируйте его:
```bash
mkdir -p ~/.config/proxy
cp ~/.config/xray-proxy/examples/config ~/.config/proxy/config
```
Формат строгий: одна секция `[proxy]`, пять обязательных ключей без повторов и
неизвестных полей.
```ini
[proxy]
type = external
name = Hiddify
address = hproxy.local
port = 12334
no_proxy_extra = .example.internal,git.example.internal
```
`address` — IPv4, IPv6 без квадратных скобок или ASCII DNS-имя без схемы и
порта. `no_proxy_extra` — список через запятую; значение может быть пустым.
Поддержка CIDR в `NO_PROXY` различается между программами, поэтому надёжнее
указывать точные IP и домены.
Если файла нет, используется прежний внешний Hiddify на
`hproxy.local:12334`, включая `.concept-software.ru` в `NO_PROXY`. Это не
переключает тип прокси автоматически.
Для управляемого Xray задайте явный тип, loopback IP и нужный локальный порт:
```ini
[proxy]
type = xray
name = Local Xray
address = 127.0.0.1
port = 12334
no_proxy_extra = .example.internal,git.example.internal
```
Секретный профиль хранится отдельно. Скопируйте синтетический пример, замените
его одной настоящей VLESS-ссылкой и закройте права конечного файла:
```bash
cp ~/.config/xray-proxy/examples/default.url ~/.config/xray-proxy/default.url
chmod 600 ~/.config/xray-proxy/default.url
```
Файл должен содержать ровно одну непустую строку. Можно передать другой файл
через `xray-proxy start -f /path/to/profile.url`. Если это симлинк, проверяются
права конечного файла.
## Команды
```bash
xray-proxy status
xray-proxy start [-f PROFILE]
xray-proxy stop
xray-proxy env
xray-proxy run -- COMMAND ARG...
proxy_on
proxy_off
pclaude [ARG...]
pcodex [ARG...]
myip
```
Для `external` команды `status`, `env` и `run` проверяют настроенный адрес и
порт. Для `xray` они дополнительно требуют закрытое состояние именно того
супервизора и дочернего Xray, который запустил CLI. Один открытый порт или чужой
PID не принимается. `run` никогда не запускает Xray скрыто.
`start` поддерживает Xray `25.12.8` и `26.3.27`, сначала выполняет
`xray version` и `xray run -test`, затем остаётся на переднем плане. Используйте
отдельную панель tmux. После готовности один loopback-порт принимает HTTP и
SOCKS. `stop` ограниченно ждёт завершения супервизора и Xray, очищает служебные
файлы и возвращает исходный `start` в терминал. Команда `stop` работает и после
явного переключения машинного типа на `external`.
`run` передаёт восемь proxy-переменных только дочернему процессу и сохраняет
аргументы и код возврата. `proxy_on` атомарно применяет те же восемь переменных
к текущей Bash-сессии; `proxy_off` удаляет их.
Таймаут проверки задаётся положительным числом секунд:
```bash
export PROXY_PREFLIGHT_TIMEOUT=2
```
Значение по умолчанию — 2 секунды. `env` при успехе печатает только восемь строк
`KEY=value`; для обычной диагностики используйте `status` или `myip`.
## Поддерживаемые профили
Поддерживаются только VLESS TCP/RAW с REALITY и VLESS XHTTP с TLS. Неизвестные,
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом без повторных ключей.
Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allowInsecure` и
`insecure` не переносит. Истинное legacy-значение даёт предупреждение, но
обычная проверка сертификата остаётся включённой. Xray 25.12.8 предназначен
только для явной старой совместимости: согласованное `true` переносится как
`allowInsecure=true` с предупреждением об отключённой проверке сертификата;
`pcs` и `vcn` для этой версии запрещены. Автоматического перехода между
версиями или типами прокси нет.
Сводка скрывает UUID и ключи. Содержимое XHTTP `path` не показывается — только
его длина. Сгенерированный JSON и состояние лежат в закрытом каталоге
`$XDG_STATE_HOME/xray-proxy` либо `~/.local/state/xray-proxy` и удаляются после
штатной остановки. Сбой диагностики внешнего IP через локальный HTTP-прокси
показывается как предупреждение и не останавливает уже готовый Xray.