feat(proxy): добавлены pclaude/pcodex для явного запуска через прокси
- Зачем: - claude/codex должны ходить через hproxy.local, а ручной proxy_on легко забыть; нужны явные команды, которые не стартуют молча при недоступном прокси. - Что: - bash: добавлены _proxy_export/_proxy_reachable/_proxy_run и pclaude/pcodex; proxy_on рефакторен на единый источник правды без смены поведения. - powershell: добавлены Get-ProxyEndpoint/Set-ProxyEnv/Test-ProxyReachable/Invoke-WithProxy и pclaude/pcodex; proxy_on рефакторен аналогично. - таймаут pre-flight вынесен в PROXY_PREFLIGHT_TIMEOUT (дефолт 2 c). - добавлен tests/test_proxy_launchers.sh (fail-fast, изоляция env, проброс кода, аргументы, регресс proxy_on). - Проверка: - bash tests/test_proxy_launchers.sh.
This commit is contained in:
1 parent
84e577bc97
commit
d5559867bd
3 files changed
+249
-18
No files matched your search
+49
-11
@@ -23,20 +23,58 @@ update_hproxy() {
|
||||
echo "✅ hproxy.local -> $(getent hosts hproxy.local 2>/dev/null | awk '{print $1}')"
|
||||
}
|
||||
|
||||
# Удобные функции-переключатели для терминала
|
||||
# NO_PROXY: ::1 и [::1] нужны, т.к. localhost в dual-stack Linux резолвится в IPv6,
|
||||
# и без них локальные MCP/loopback-соединения (chroma-mcp, claude-mem worker и т.п.)
|
||||
# уходят в SOCKS5 через hproxy.local. Дублируем в lower-case no_proxy для Python-стека.
|
||||
proxy_on() {
|
||||
local no_proxy_list="hproxy.local,localhost,127.0.0.1,::1,[::1],.concept-software.ru"
|
||||
export HTTP_PROXY="http://hproxy.local:12334"
|
||||
export HTTPS_PROXY="http://hproxy.local:12334"
|
||||
export ALL_PROXY="socks5://hproxy.local:12334"
|
||||
export NO_PROXY="$no_proxy_list"
|
||||
# Единый источник правды для прокси-эндпоинта (не экспортируется; идемпотентно
|
||||
# при повторном source). NO_PROXY: ::1 и [::1] нужны, т.к. localhost в dual-stack
|
||||
# Linux резолвится в IPv6, и без них локальные MCP/loopback-соединения
|
||||
# (chroma-mcp, claude-mem worker и т.п.) уходят в SOCKS5 через hproxy.local.
|
||||
: "${_PROXY_HOST:=hproxy.local}"
|
||||
: "${_PROXY_PORT:=12334}"
|
||||
: "${_PROXY_NO_PROXY:=hproxy.local,localhost,127.0.0.1,::1,[::1],.concept-software.ru}"
|
||||
|
||||
# Внутренний: экспортирует proxy-env (+ lower-case дубли для Python-стека).
|
||||
# Единственное место, где формируются значения переменных.
|
||||
_proxy_export() {
|
||||
export HTTP_PROXY="http://${_PROXY_HOST}:${_PROXY_PORT}"
|
||||
export HTTPS_PROXY="http://${_PROXY_HOST}:${_PROXY_PORT}"
|
||||
export ALL_PROXY="socks5://${_PROXY_HOST}:${_PROXY_PORT}"
|
||||
export NO_PROXY="$_PROXY_NO_PROXY"
|
||||
export http_proxy="$HTTP_PROXY"
|
||||
export https_proxy="$HTTPS_PROXY"
|
||||
export all_proxy="$ALL_PROXY"
|
||||
export no_proxy="$no_proxy_list"
|
||||
export no_proxy="$_PROXY_NO_PROXY"
|
||||
}
|
||||
|
||||
# Внутренний: fail-fast проверка доступности прокси (только reachability —
|
||||
# не доказательство, что трафик реально проксируется). Таймаут покрывает и
|
||||
# медленный DNS-резолв; при зависании — отказ запуска (безопасный отказ).
|
||||
_proxy_reachable() {
|
||||
timeout "${PROXY_PREFLIGHT_TIMEOUT:-2}" \
|
||||
bash -c "exec 3<>/dev/tcp/${_PROXY_HOST}/${_PROXY_PORT}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Внутренний: запуск команды с proxy-env. Pre-flight, затем subshell+exec —
|
||||
# env виден только дочернему процессу, не протекает в родительский shell,
|
||||
# код возврата пробрасывается.
|
||||
_proxy_run() {
|
||||
local cmd="$1"; shift
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "❌ Команда не найдена в PATH: $cmd" >&2
|
||||
return 127
|
||||
fi
|
||||
if ! _proxy_reachable; then
|
||||
echo "❌ Прокси ${_PROXY_HOST}:${_PROXY_PORT} недоступен — запуск '$cmd' отменён." >&2
|
||||
return 1
|
||||
fi
|
||||
# exec и так резолвит через PATH в обход функций/алиасов.
|
||||
( _proxy_export; exec "$cmd" "$@" )
|
||||
}
|
||||
|
||||
pclaude() { _proxy_run claude "$@"; }
|
||||
pcodex() { _proxy_run codex "$@"; }
|
||||
|
||||
# Удобные функции-переключатели для терминала.
|
||||
proxy_on() {
|
||||
_proxy_export
|
||||
echo "✅ Proxy enabled. Checking route..."
|
||||
# Подставляем результат выполнения curl прямо в строку
|
||||
echo "🌍 Current IP: $(curl -s --max-time 3 https://ifconfig.me)"
|
||||
|
||||
@@ -75,15 +75,46 @@ function Get-PublicIp {
|
||||
catch { return $null }
|
||||
}
|
||||
|
||||
# Единый источник правды для прокси-эндпоинта.
|
||||
function Get-ProxyEndpoint {
|
||||
@{
|
||||
ProxyHost = 'hproxy.local'
|
||||
Port = 12334
|
||||
NoProxy = 'hproxy.local,localhost,127.0.0.1,.concept-software.ru'
|
||||
}
|
||||
}
|
||||
|
||||
# Внутренний: выставить proxy-env в текущей области. Единственное место,
|
||||
# где формируются значения переменных.
|
||||
function Set-ProxyEnv {
|
||||
$e = Get-ProxyEndpoint
|
||||
$env:HTTP_PROXY = "http://$($e.ProxyHost):$($e.Port)"
|
||||
$env:HTTPS_PROXY = "http://$($e.ProxyHost):$($e.Port)"
|
||||
$env:ALL_PROXY = "socks5://$($e.ProxyHost):$($e.Port)"
|
||||
$env:NO_PROXY = $e.NoProxy
|
||||
}
|
||||
|
||||
# Внутренний: fail-fast проверка доступности прокси (только reachability).
|
||||
# Таймаут — $env:PROXY_PREFLIGHT_TIMEOUT (дефолт 2 c), покрывает и медленный DNS.
|
||||
function Test-ProxyReachable {
|
||||
$e = Get-ProxyEndpoint
|
||||
$timeout = if ($env:PROXY_PREFLIGHT_TIMEOUT) { [int]$env:PROXY_PREFLIGHT_TIMEOUT } else { 2 }
|
||||
$client = [System.Net.Sockets.TcpClient]::new()
|
||||
try {
|
||||
$iar = $client.BeginConnect($e.ProxyHost, [int]$e.Port, $null, $null)
|
||||
if ($iar.AsyncWaitHandle.WaitOne([TimeSpan]::FromSeconds($timeout))) {
|
||||
$client.EndConnect($iar)
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
catch { return $false }
|
||||
finally { $client.Dispose() }
|
||||
}
|
||||
|
||||
# Включить прокси для текущей сессии (hproxy.local).
|
||||
function proxy_on {
|
||||
$Host_ = 'hproxy.local'; $Port = 12334
|
||||
|
||||
$env:HTTP_PROXY = "http://${Host_}:${Port}"
|
||||
$env:HTTPS_PROXY = "http://${Host_}:${Port}"
|
||||
$env:ALL_PROXY = "socks5://${Host_}:${Port}"
|
||||
$env:NO_PROXY = "${Host_},localhost,127.0.0.1,.concept-software.ru"
|
||||
|
||||
Set-ProxyEnv
|
||||
$Ip = Get-PublicIp
|
||||
"Proxy ON. IP: $(if ($Ip) { $Ip } else { 'unavailable' })"
|
||||
}
|
||||
@@ -103,6 +134,48 @@ function myip {
|
||||
"IP: $(if ($Ip) { $Ip } else { 'unavailable' })"
|
||||
}
|
||||
|
||||
# Внутренний: запуск команды через прокси. Pre-flight, затем env только на
|
||||
# время дочернего процесса (в finally восстанавливаем прежние значения —
|
||||
# сессия не остаётся с включённым прокси).
|
||||
function Invoke-WithProxy {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Exe,
|
||||
[Parameter(ValueFromRemainingArguments)][string[]]$CmdArgs
|
||||
)
|
||||
|
||||
if (-not (Get-Command $Exe -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "Команда не найдена в PATH: $Exe"
|
||||
return
|
||||
}
|
||||
if (-not (Test-ProxyReachable)) {
|
||||
$e = Get-ProxyEndpoint
|
||||
Write-Error "Прокси $($e.ProxyHost):$($e.Port) недоступен — запуск '$Exe' отменён."
|
||||
return
|
||||
}
|
||||
|
||||
$names = 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'
|
||||
$saved = @{}
|
||||
foreach ($n in $names) { $saved[$n] = [Environment]::GetEnvironmentVariable($n) }
|
||||
try {
|
||||
Set-ProxyEnv
|
||||
& $Exe @CmdArgs
|
||||
$global:LASTEXITCODE = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
foreach ($n in $names) {
|
||||
if ($null -eq $saved[$n]) {
|
||||
Remove-Item -Path "Env:$n" -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Set-Item -Path "Env:$n" -Value $saved[$n]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Явный запуск claude/codex гарантированно через прокси.
|
||||
function pclaude { Invoke-WithProxy claude @args }
|
||||
function pcodex { Invoke-WithProxy codex @args }
|
||||
|
||||
# ── Шорткаты Windows Terminal ────────────────────────────────────────
|
||||
|
||||
if (Get-Command wt -ErrorAction SilentlyContinue) {
|
||||
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FUNCTIONS_FILE="$REPO_ROOT/bash/.bash_functions"
|
||||
|
||||
pass_count=0
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ok() {
|
||||
pass_count=$((pass_count + 1))
|
||||
echo "ok - $*"
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1" actual="$2" msg="$3"
|
||||
[ "$expected" = "$actual" ] || fail "$msg (ожидалось '$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}"
|
||||
EOF
|
||||
chmod +x "$dir/fakecli"
|
||||
echo "$dir"
|
||||
}
|
||||
|
||||
# --- Каждый кейс в отдельном subshell, чтобы env/функции не протекали ---
|
||||
|
||||
# 1. Прокси недоступен → бинарник НЕ вызывается, ненулевой код.
|
||||
(
|
||||
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 без запуска бинарника"
|
||||
|
||||
# 2. proxy-env виден дочернему процессу и НЕ протекает в родительский shell.
|
||||
(
|
||||
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"
|
||||
|
||||
# 3. Проброс кода возврата дочернего процесса.
|
||||
(
|
||||
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"; } # без сети
|
||||
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: контракт переменных не изменился"
|
||||
|
||||
# 7. shellcheck (если доступен).
|
||||
if command -v shellcheck >/dev/null 2>&1; then
|
||||
shellcheck -s bash "$FUNCTIONS_FILE" || fail "shellcheck нашёл проблемы в bash/.bash_functions"
|
||||
ok "shellcheck чист"
|
||||
else
|
||||
echo "skip - shellcheck не установлен"
|
||||
fi
|
||||
|
||||
echo "PASS: $pass_count проверок пройдено"
|
||||
Reference in new issue
Block a user