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:
1 parent
fdf744e578
commit
e69a4bb7df
20 files changed
+3224
-170
No files matched your search
+184
-79
@@ -3,6 +3,8 @@ set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FUNCTIONS_FILE="$REPO_ROOT/bash/.bash_functions"
|
||||
CUSTOM_FILE="$REPO_ROOT/bash/.bash_custom"
|
||||
SHELL_MODULE="$REPO_ROOT/xray-proxy/.config/xray-proxy/shell.bash"
|
||||
|
||||
pass_count=0
|
||||
|
||||
@@ -17,101 +19,204 @@ ok() {
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1" actual="$2" msg="$3"
|
||||
[ "$expected" = "$actual" ] || fail "$msg (ожидалось '$expected', получено '$actual')"
|
||||
local expected="$1" actual="$2" message="$3"
|
||||
[ "$expected" = "$actual" ] || fail "$message (ожидалось '$expected', получено '$actual')"
|
||||
}
|
||||
|
||||
# Изолированный fake-CLI на PATH: печатает env и аргументы, отдаёт заданный код.
|
||||
make_fake_bin() {
|
||||
local dir
|
||||
dir="$(mktemp -d /tmp/proxy-launchers-test.XXXXXX)"
|
||||
cat >"$dir/fakecli" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
echo "HTTP_PROXY=${HTTP_PROXY:-}"
|
||||
echo "NO_PROXY=${NO_PROXY:-}"
|
||||
for a in "$@"; do echo "ARG=$a"; done
|
||||
exit "${FAKE_EXIT:-0}"
|
||||
valid_env() {
|
||||
cat <<'EOF'
|
||||
HTTP_PROXY=http://proxy.test:1234
|
||||
HTTPS_PROXY=http://proxy.test:1234
|
||||
ALL_PROXY=socks5://proxy.test:1234
|
||||
NO_PROXY=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
||||
http_proxy=http://proxy.test:1234
|
||||
https_proxy=http://proxy.test:1234
|
||||
all_proxy=socks5://proxy.test:1234
|
||||
no_proxy=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
||||
EOF
|
||||
chmod +x "$dir/fakecli"
|
||||
echo "$dir"
|
||||
}
|
||||
|
||||
# --- Каждый кейс в отдельном subshell, чтобы env/функции не протекали ---
|
||||
assert_five_proxy_commands() {
|
||||
local command_name
|
||||
for command_name in proxy_on proxy_off pclaude pcodex myip; do
|
||||
declare -F "$command_name" >/dev/null || fail "нет функции $command_name"
|
||||
done
|
||||
}
|
||||
|
||||
# 1. Прокси недоступен → бинарник НЕ вызывается, ненулевой код.
|
||||
# Самостоятельный модуль предоставляет весь публичный Bash-интерфейс.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
_proxy_reachable() { return 1; }
|
||||
bindir="$(make_fake_bin)"; export PATH="$bindir:$PATH"
|
||||
out="$(_proxy_run fakecli 2>&1)"; rc=$?
|
||||
[ "$rc" -ne 0 ] || fail "недоступный прокси: ожидался ненулевой код"
|
||||
case "$out" in *ARG=*) fail "недоступный прокси: бинарник не должен запускаться";; esac
|
||||
case "$out" in *"недоступен"*) ;; *) fail "недоступный прокси: нет понятного сообщения";; esac
|
||||
) && ok "недоступный прокси → fail-fast без запуска бинарника"
|
||||
source "$SHELL_MODULE"
|
||||
assert_five_proxy_commands
|
||||
) && ok "самостоятельное подключение предоставляет пять proxy-команд"
|
||||
|
||||
# 2. proxy-env виден дочернему процессу и НЕ протекает в родительский shell.
|
||||
# Обычная .bash_custom независимо подключает общие функции и proxy-модуль.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
_proxy_reachable() { return 0; }
|
||||
bindir="$(make_fake_bin)"; export PATH="$bindir:$PATH"
|
||||
unset HTTP_PROXY NO_PROXY || true
|
||||
out="$(_proxy_run fakecli)"
|
||||
case "$out" in *"HTTP_PROXY=http://hproxy.local:12334"*) ;; *) fail "child: HTTP_PROXY не выставлен";; esac
|
||||
case "$out" in *"NO_PROXY=hproxy.local,localhost,127.0.0.1,::1,[::1],.concept-software.ru"*) ;; *) fail "child: NO_PROXY неверен";; esac
|
||||
[ -z "${HTTP_PROXY:-}" ] || fail "leak: HTTP_PROXY протёк в родительский shell"
|
||||
[ -z "${NO_PROXY:-}" ] || fail "leak: NO_PROXY протёк в родительский shell"
|
||||
) && ok "proxy-env в дочернем процессе, без протечки в shell"
|
||||
fixture="$(mktemp -d /tmp/proxy-bash-custom-test.XXXXXX)"
|
||||
mkdir -p "$fixture/.config/xray-proxy"
|
||||
ln -s "$CUSTOM_FILE" "$fixture/.bash_custom"
|
||||
ln -s "$FUNCTIONS_FILE" "$fixture/.bash_functions"
|
||||
ln -s "$SHELL_MODULE" "$fixture/.config/xray-proxy/shell.bash"
|
||||
output="$(
|
||||
HOME="$fixture" bash --noprofile --norc -ic '
|
||||
source "$HOME/.bash_custom"
|
||||
for name in proxy_on proxy_off pclaude pcodex myip update_hproxy; do
|
||||
declare -F "$name" >/dev/null || exit 1
|
||||
done
|
||||
printf ready
|
||||
' 2>/dev/null
|
||||
)"
|
||||
assert_eq ready "$output" ".bash_custom не подключил оба независимых модуля"
|
||||
) && ok "обычное подключение предоставляет proxy-команды и update_hproxy"
|
||||
|
||||
# 3. Проброс кода возврата дочернего процесса.
|
||||
# proxy_on атомарно применяет ровно восемь переменных и не запрашивает IP.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
_proxy_reachable() { return 0; }
|
||||
bindir="$(make_fake_bin)"; export PATH="$bindir:$PATH"
|
||||
FAKE_EXIT=42 _proxy_run fakecli >/dev/null; rc=$?
|
||||
assert_eq 42 "$rc" "код возврата дочернего процесса не проброшен"
|
||||
) && ok "проброс кода возврата дочернего процесса"
|
||||
|
||||
# 4. Аргументы с пробелами и `--` доходят без искажений.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
_proxy_reachable() { return 0; }
|
||||
bindir="$(make_fake_bin)"; export PATH="$bindir:$PATH"
|
||||
out="$(_proxy_run fakecli "a b" -- --flag)"
|
||||
case "$out" in *$'ARG=a b\n'*) ;; *) fail "arg с пробелом потерян";; esac
|
||||
case "$out" in *"ARG=--"*) ;; *) fail "разделитель -- потерян";; esac
|
||||
case "$out" in *"ARG=--flag"*) ;; *) fail "флаг после -- потерян";; esac
|
||||
) && ok "аргументы с пробелами и -- проброшены без искажений"
|
||||
|
||||
# 5. Команда не в PATH → отдельное сообщение, код 127.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
_proxy_reachable() { return 0; }
|
||||
out="$(_proxy_run definitely-no-such-cli-xyz 2>&1)"; rc=$?
|
||||
assert_eq 127 "$rc" "отсутствующая команда: ожидался код 127"
|
||||
case "$out" in *"не найдена"*) ;; *) fail "отсутствующая команда: нет понятного сообщения";; esac
|
||||
) && ok "команда не в PATH → код 127 с сообщением"
|
||||
|
||||
# 6. Регресс proxy_on: точный контракт экспортируемых переменных.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
curl() { echo "1.2.3.4"; } # без сети
|
||||
source "$SHELL_MODULE"
|
||||
_proxy_cli() {
|
||||
[ "$1" = env ] || return 99
|
||||
valid_env
|
||||
}
|
||||
curl() { fail "proxy_on не должен вызывать curl"; }
|
||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
||||
http_proxy https_proxy all_proxy no_proxy || true
|
||||
proxy_on >/dev/null
|
||||
np="hproxy.local,localhost,127.0.0.1,::1,[::1],.concept-software.ru"
|
||||
assert_eq "http://hproxy.local:12334" "${HTTP_PROXY}" "proxy_on HTTP_PROXY"
|
||||
assert_eq "http://hproxy.local:12334" "${HTTPS_PROXY}" "proxy_on HTTPS_PROXY"
|
||||
assert_eq "socks5://hproxy.local:12334" "${ALL_PROXY}" "proxy_on ALL_PROXY"
|
||||
assert_eq "$np" "${NO_PROXY}" "proxy_on NO_PROXY"
|
||||
assert_eq "${HTTP_PROXY}" "${http_proxy}" "proxy_on http_proxy дубль"
|
||||
assert_eq "${HTTPS_PROXY}" "${https_proxy}" "proxy_on https_proxy дубль"
|
||||
assert_eq "${ALL_PROXY}" "${all_proxy}" "proxy_on all_proxy дубль"
|
||||
assert_eq "$np" "${no_proxy}" "proxy_on no_proxy дубль"
|
||||
) && ok "регресс proxy_on: контракт переменных не изменился"
|
||||
assert_eq "http://proxy.test:1234" "$HTTP_PROXY" "HTTP_PROXY"
|
||||
assert_eq "$HTTP_PROXY" "$http_proxy" "http_proxy"
|
||||
assert_eq "http://proxy.test:1234" "$HTTPS_PROXY" "HTTPS_PROXY"
|
||||
assert_eq "$HTTPS_PROXY" "$https_proxy" "https_proxy"
|
||||
assert_eq "socks5://proxy.test:1234" "$ALL_PROXY" "ALL_PROXY"
|
||||
assert_eq "$ALL_PROXY" "$all_proxy" "all_proxy"
|
||||
assert_eq "proxy.test,localhost,127.0.0.1,::1,[::1],.example.test" "$NO_PROXY" "NO_PROXY"
|
||||
assert_eq "$NO_PROXY" "$no_proxy" "no_proxy"
|
||||
) && ok "proxy_on применяет полный восьмиключевой вывод без запроса IP"
|
||||
|
||||
# Любой неверный машинный вывод оставляет прежнее окружение целиком.
|
||||
missing_output="$(valid_env | sed '/^no_proxy=/d')"
|
||||
unknown_output="$(valid_env; printf 'EXTRA_PROXY=bad\n')"
|
||||
duplicate_output="$(valid_env; printf 'HTTP_PROXY=duplicate\n')"
|
||||
for bad_output in \
|
||||
"$missing_output" \
|
||||
"$unknown_output" \
|
||||
"$duplicate_output" \
|
||||
"BROKEN-LINE"; do
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
export HTTP_PROXY=old-http HTTPS_PROXY=old-https ALL_PROXY=old-all NO_PROXY=old-no
|
||||
export http_proxy=old-http-lower https_proxy=old-https-lower
|
||||
export all_proxy=old-all-lower no_proxy=old-no-lower
|
||||
_proxy_cli() { printf '%s\n' "$bad_output"; }
|
||||
if proxy_on >/dev/null 2>&1; then
|
||||
fail "proxy_on принял неверный вывод"
|
||||
fi
|
||||
assert_eq old-http "$HTTP_PROXY" "HTTP_PROXY изменился при ошибке"
|
||||
assert_eq old-https "$HTTPS_PROXY" "HTTPS_PROXY изменился при ошибке"
|
||||
assert_eq old-all "$ALL_PROXY" "ALL_PROXY изменился при ошибке"
|
||||
assert_eq old-no "$NO_PROXY" "NO_PROXY изменился при ошибке"
|
||||
assert_eq old-http-lower "$http_proxy" "http_proxy изменился при ошибке"
|
||||
assert_eq old-https-lower "$https_proxy" "https_proxy изменился при ошибке"
|
||||
assert_eq old-all-lower "$all_proxy" "all_proxy изменился при ошибке"
|
||||
assert_eq old-no-lower "$no_proxy" "no_proxy изменился при ошибке"
|
||||
) || fail "атомарность нарушена для неверного вывода"
|
||||
done
|
||||
ok "неверный env-вывод отклоняется без частичного изменения окружения"
|
||||
|
||||
# Даже полный stdout не применяется, если CLI завершился с ошибкой.
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
export HTTP_PROXY=old HTTPS_PROXY=old ALL_PROXY=old NO_PROXY=old
|
||||
export http_proxy=old https_proxy=old all_proxy=old no_proxy=old
|
||||
_proxy_cli() {
|
||||
valid_env
|
||||
return 1
|
||||
}
|
||||
if proxy_on >/dev/null 2>&1; then
|
||||
fail "proxy_on принял вывод неуспешного CLI"
|
||||
fi
|
||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
||||
http_proxy https_proxy all_proxy no_proxy; do
|
||||
assert_eq old "${!key}" "$key изменился после ошибки CLI"
|
||||
done
|
||||
) && ok "stdout неуспешного env не изменяет окружение"
|
||||
|
||||
# CRLF и NUL считаются повреждённым машинным выводом и не могут быть молча
|
||||
# нормализованы перед атомарной проверкой.
|
||||
for malformed_kind in crlf nul; do
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
export HTTP_PROXY=old HTTPS_PROXY=old ALL_PROXY=old NO_PROXY=old
|
||||
export http_proxy=old https_proxy=old all_proxy=old no_proxy=old
|
||||
_proxy_cli() {
|
||||
case "$malformed_kind" in
|
||||
crlf)
|
||||
while IFS= read -r line; do
|
||||
printf '%s\r\n' "$line"
|
||||
done < <(valid_env)
|
||||
;;
|
||||
nul)
|
||||
printf 'HTTP_PROXY=http://proxy.test:12\00034\n'
|
||||
valid_env | sed '/^HTTP_PROXY=/d'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
if proxy_on >/dev/null 2>&1; then
|
||||
fail "proxy_on принял $malformed_kind в env-выводе"
|
||||
fi
|
||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
||||
http_proxy https_proxy all_proxy no_proxy; do
|
||||
assert_eq old "${!key}" "$key изменился после $malformed_kind"
|
||||
done
|
||||
) || fail "атомарность нарушена для $malformed_kind"
|
||||
done
|
||||
ok "CRLF и NUL отклоняются без изменения окружения"
|
||||
|
||||
# Обёртки передают CLI команду, аргументы и код возврата без искажений.
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
_proxy_cli() {
|
||||
printf '<%s>\n' "$@"
|
||||
return 42
|
||||
}
|
||||
set +e
|
||||
output="$(pclaude "a b" -- --flag)"
|
||||
rc=$?
|
||||
set -e
|
||||
assert_eq 42 "$rc" "pclaude не сохранил код возврата"
|
||||
assert_eq $'<run>\n<-->\n<claude>\n<a b>\n<-->\n<--flag>' "$output" "pclaude исказил аргументы"
|
||||
|
||||
set +e
|
||||
output="$(pcodex --search)"
|
||||
rc=$?
|
||||
set -e
|
||||
assert_eq 42 "$rc" "pcodex не сохранил код возврата"
|
||||
assert_eq $'<run>\n<-->\n<codex>\n<--search>' "$output" "pcodex исказил аргументы"
|
||||
) && ok "pclaude и pcodex делегируют run и сохраняют аргументы/код"
|
||||
|
||||
# proxy_off очищает оба регистра и не смешивает действие с диагностикой.
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
export HTTP_PROXY=x HTTPS_PROXY=x ALL_PROXY=x NO_PROXY=x
|
||||
export http_proxy=x https_proxy=x all_proxy=x no_proxy=x
|
||||
curl() { fail "proxy_off не должен вызывать curl"; }
|
||||
proxy_off >/dev/null
|
||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
||||
http_proxy https_proxy all_proxy no_proxy; do
|
||||
[ -z "${!key+x}" ] || fail "$key не очищен"
|
||||
done
|
||||
) && ok "proxy_off очищает восемь переменных без запроса IP"
|
||||
|
||||
# Общий файл больше не владеет proxy-интерфейсом, но сохраняет update_hproxy.
|
||||
(
|
||||
source "$FUNCTIONS_FILE"
|
||||
declare -F update_hproxy >/dev/null || fail "update_hproxy потерян"
|
||||
for name in proxy_on proxy_off pclaude pcodex myip _proxy_run _proxy_cli; do
|
||||
if declare -F "$name" >/dev/null; then
|
||||
fail "$name всё ещё определён в bash/.bash_functions"
|
||||
fi
|
||||
done
|
||||
) && ok "владение proxy-функциями перенесено, update_hproxy сохранён"
|
||||
|
||||
# 7. shellcheck (если доступен).
|
||||
if command -v shellcheck >/dev/null 2>&1; then
|
||||
shellcheck -s bash "$FUNCTIONS_FILE" || fail "shellcheck нашёл проблемы в bash/.bash_functions"
|
||||
shellcheck -s bash "$FUNCTIONS_FILE" "$CUSTOM_FILE" "$SHELL_MODULE" "$0" \
|
||||
|| fail "shellcheck нашёл проблемы"
|
||||
ok "shellcheck чист"
|
||||
else
|
||||
echo "skip - shellcheck не установлен"
|
||||
|
||||
Reference in new issue
Block a user