Загрузка данных
#!/usr/bin/env bash
# ============================================================================
# o3-claude-setup — настройка доступа к Claude Code из РФ через свой SSH-сервер.
# Только для macOS (нужен Homebrew).
#
# Зачем это нужно:
# Anthropic не пускает Claude Code с российских IP. Скрипт заворачивает
# трафик claude через твой зарубежный VPS — для Anthropic запросы выглядят
# приходящими из страны сервера. Проксируется ТОЛЬКО claude, остальные
# программы (git, brew, браузер) ходят напрямую.
#
# Как работает (путь одного запроса claude):
# claude
# -> по HTTPS_PROXY идёт на локальный HTTP-прокси privoxy (127.0.0.1:8118)
# -> privoxy перекладывает запрос в локальный SOCKS5 (127.0.0.1:1080)
# -> SOCKS5 = вход в SSH-туннель (ssh -D) до твоего VPS
# -> из VPS запрос выходит в интернет уже с IP сервера
# -> и доходит до api.anthropic.com
# Зачем два прокси: claude понимает только HTTP-прокси, а туннель даёт
# SOCKS5 — privoxy служит переходником HTTP -> SOCKS5.
#
# Что делает скрипт (по шагам):
# 1. Ставит privoxy и autossh через Homebrew.
# 2. Генерит отдельный SSH-ключ ~/.ssh/o3-claude под этот туннель.
# 3. Авторизует ключ на сервере: сам через ssh-copy-id (если знаешь пароль),
# либо печатает готовую строку для ручного добавления в authorized_keys.
# 4. Настраивает privoxy как переходник SOCKS5 -> HTTP.
# 5. Ставит туннель под launchd: автозапуск при логине и авто-переподключение
# при обрыве или смене сети (autossh).
# 6. Кладёт команду o3-claude-health для проверки всей цепочки.
# 7. Добавляет в ~/.zshrc алиас o3-claude (claude с прокси-переменными).
# 8. Ставит сам Claude Code через туннель, если он ещё не установлен.
# Все артефакты помечены именем o3-claude — легко найти и удалить.
#
# После установки:
# Запуск Claude: o3-claude
# Проверка цепочки: o3-claude-health
# Перезапуск прокси: o3-claude-setup --restart
#
# Смена VPS / полное удаление:
# o3-claude-setup --uninstall # снести всю настройку и откатить
# o3-claude-setup new-user@new-host # затем чистая установка на новый VPS
#
# Использование:
# o3-claude-setup user@host
# o3-claude-setup --host 1.2.3.4 --user root --port 22
# o3-claude-setup # спросит сервер интерактивно
# o3-claude-setup --restart # перезапустить прокси (:1080 и :8118)
#
# Опции:
# -H, --host HOST адрес сервера
# -u, --user USER ssh-пользователь (по умолчанию root)
# -p, --port PORT ssh-порт (по умолчанию 22)
# -k, --key PATH путь к ключу (по умолчанию ~/.ssh/o3-claude)
# --socks PORT локальный SOCKS5-порт (по умолчанию 1080)
# --http PORT локальный HTTP-порт privoxy (по умолчанию 8118)
# -r, --restart перезапустить SOCKS-туннель и privoxy (без переустановки)
# --uninstall снести настройку и откатить все изменения
# -h, --help
# ============================================================================
set -euo pipefail
info() { printf "\033[36m==>\033[0m %s\n" "$1"; }
ok() { printf "\033[32mOK\033[0m %s\n" "$1"; }
warn() { printf "\033[33m!!\033[0m %s\n" "$1"; }
die() { printf "\033[31mОШИБКА:\033[0m %s\n" "$1" >&2; exit 1; }
usage() { awk 'NR==1{next} /^#/{sub(/^# ?/,"");print;next} {exit}' "$0"; }
# Откат: удаляет все артефакты (историч. имена autossh-proxy / cc-proxy / o3-claude).
# Не трогает личный ключ ~/.ssh/id_*, пакеты privoxy/autossh и сам Claude Code.
uninstall() {
[[ "$(uname)" == "Darwin" ]] || die "откат рассчитан на macOS."
info "Откат настройки o3-claude — удаляю артефакты..."
# launchd-агенты
for l in autossh-proxy cc-proxy o3-claude; do
launchctl unload "$HOME/Library/LaunchAgents/com.user.$l.plist" 2>/dev/null || true
rm -f "$HOME/Library/LaunchAgents/com.user.$l.plist"
done
# живые туннели
pkill -f 'autossh' 2>/dev/null || true
pkill -f 'ssh .*-D ' 2>/dev/null || true
# сгенерированные команды и логи
rm -f "$HOME/.local/bin/proxy-health" "$HOME/.local/bin/cc-proxy-health" \
"$HOME/.local/bin/o3-claude-health" "$HOME/.local/bin/cc-install"
rm -f /tmp/autossh-proxy.err /tmp/autossh-proxy.out \
/tmp/cc-proxy.err /tmp/cc-proxy.out /tmp/o3-claude.err /tmp/o3-claude.out
# ключи, созданные скриптом (личные ~/.ssh/id_* НЕ трогаем)
rm -f "$HOME/.ssh/cc-proxy" "$HOME/.ssh/cc-proxy.pub" \
"$HOME/.ssh/o3-claude" "$HOME/.ssh/o3-claude.pub"
# блоки алиасов в ~/.zshrc
if [[ -f "$HOME/.zshrc" ]]; then
for m in cc-proxy o3-claude; do
sed -i '' "/# >>> $m alias >>>/,/# <<< $m alias <<</d" "$HOME/.zshrc"
done
fi
# privoxy: убрать наши блоки, вернуть исходный socket-timeout, выключить сервис
local CFG; CFG="$(brew --prefix 2>/dev/null)/etc/privoxy/config"
if [[ -f "$CFG" ]]; then
for m in cc-proxy o3-claude; do
sed -i '' "/# >>> $m >>>/,/# <<< $m <<</d" "$CFG"
done
sed -i '' '/^forward-socks5t/d' "$CFG"
sed -i '' '/Перенаправление всего трафика/d' "$CFG"
sed -i '' -E 's/^# (socket-timeout[[:space:]]+.*)$/\1/' "$CFG"
brew services stop privoxy >/dev/null 2>&1 || true
fi
ok "Откат завершён."
echo " Не тронуты: личный ключ ~/.ssh/id_*, пакеты privoxy/autossh, сам Claude Code."
echo " Старый VPS можно забыть — его authorized_keys больше не используется."
echo " Чистая установка на новый сервер: o3-claude-setup new-user@new-host"
echo " (после отката перезапусти терминал или: source ~/.zshrc)"
}
# Ждёт, пока порт начнёт слушать (до tries*0.5 сек).
wait_port() {
local port="$1" tries="${2:-24}" i
for ((i = 0; i < tries; i++)); do
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
sleep 0.5
done
return 1
}
# Перезапуск обеих половин цепочки без переустановки:
# SOCKS-туннель (autossh под launchd) и privoxy. Конфиги не трогает.
restart_proxies() {
[[ "$(uname)" == "Darwin" ]] || die "перезапуск рассчитан на macOS."
command -v brew >/dev/null 2>&1 || die "нет Homebrew. Поставь с https://brew.sh и запусти снова."
local PLIST="$HOME/Library/LaunchAgents/com.user.o3-claude.plist"
local FAILED=0
info "Перезапускаю SOCKS-туннель (:$SOCKS_PORT)..."
if [[ -f "$PLIST" ]]; then
launchctl unload "$PLIST" 2>/dev/null || true
pkill -f "autossh.*-D $SOCKS_PORT" 2>/dev/null || true
pkill -f "ssh .*-D $SOCKS_PORT" 2>/dev/null || true
sleep 1
launchctl load "$PLIST" || die "не удалось загрузить $PLIST"
if wait_port "$SOCKS_PORT"; then
ok "Туннель слушает :$SOCKS_PORT"
else
FAILED=1
warn "порт :$SOCKS_PORT не поднялся — смотри /tmp/o3-claude.err (ключ авторизован на сервере?)"
fi
else
FAILED=1
warn "нет $PLIST — туннель ещё не настроен. Установка: o3-claude-setup user@host"
fi
info "Перезапускаю privoxy (:$HTTP_PORT)..."
if brew list privoxy >/dev/null 2>&1; then
brew services restart privoxy >/dev/null || die "не удалось перезапустить privoxy"
if wait_port "$HTTP_PORT"; then
ok "Privoxy слушает :$HTTP_PORT"
else
FAILED=1
warn "порт :$HTTP_PORT не поднялся — проверь $(brew --prefix)/etc/privoxy/config"
fi
else
FAILED=1
warn "privoxy не установлен. Установка: o3-claude-setup user@host"
fi
if [[ -x "$HOME/.local/bin/o3-claude-health" ]]; then
echo
SOCKS_PORT="$SOCKS_PORT" HTTP_PORT="$HTTP_PORT" "$HOME/.local/bin/o3-claude-health" || true
fi
return "$FAILED"
}
# --- параметры --------------------------------------------------------------
SSH_HOST=""; SSH_USER="root"; SSH_PORT="22"; DO_UNINSTALL=0; DO_RESTART=0
KEY="$HOME/.ssh/o3-claude"; SOCKS_PORT="1080"; HTTP_PORT="8118"
NO_PROXY_VAL="localhost,127.0.0.1,::1,.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.ru,.ozon.ru,.o3.ru,.o3t.ru"
while [[ $# -gt 0 ]]; do
case "$1" in
-H|--host) SSH_HOST="$2"; shift 2;;
-u|--user) SSH_USER="$2"; shift 2;;
-p|--port) SSH_PORT="$2"; shift 2;;
-k|--key) KEY="$2"; shift 2;;
--socks) SOCKS_PORT="$2"; shift 2;;
--http) HTTP_PORT="$2"; shift 2;;
-r|--restart|--reload) DO_RESTART=1; shift;;
--uninstall|--rollback) DO_UNINSTALL=1; shift;;
-h|--help) usage; exit 0;;
-*) die "неизвестный флаг: $1 (см. --help)";;
*) if [[ "$1" == *@* ]]; then SSH_USER="${1%@*}"; SSH_HOST="${1#*@}"; else SSH_HOST="$1"; fi; shift;;
esac
done
if [[ "$DO_UNINSTALL" == 1 ]]; then uninstall; exit 0; fi
if [[ "$DO_RESTART" == 1 ]]; then restart_proxies || exit $?; exit 0; fi
if [[ -z "$SSH_HOST" ]]; then
[[ -t 0 ]] || die "не задан сервер (user@host). См. --help"
read -r -p "SSH-сервер (host или user@host): " ans
[[ -n "$ans" ]] || die "сервер не указан"
if [[ "$ans" == *@* ]]; then SSH_USER="${ans%@*}"; SSH_HOST="${ans#*@}"; else SSH_HOST="$ans"; fi
fi
TARGET="$SSH_USER@$SSH_HOST"
# --- окружение --------------------------------------------------------------
[[ "$(uname)" == "Darwin" ]] || die "скрипт рассчитан на macOS."
command -v brew >/dev/null 2>&1 || die "нет Homebrew. Поставь с https://brew.sh и запусти снова."
BREW_PREFIX="$(brew --prefix)"
info "Сервер: $TARGET:$SSH_PORT | SOCKS :$SOCKS_PORT | HTTP :$HTTP_PORT"
# --- 1. зависимости ---------------------------------------------------------
info "Ставлю privoxy и autossh (если ещё нет)..."
brew list privoxy >/dev/null 2>&1 || brew install privoxy
brew list autossh >/dev/null 2>&1 || brew install autossh
ok "Зависимости на месте"
# --- 2. SSH-ключ ------------------------------------------------------------
if [[ -f "$KEY" ]]; then
ok "Ключ уже есть: $KEY"
else
info "Генерирую SSH-ключ..."
ssh-keygen -t ed25519 -f "$KEY" -N "" -C "o3-claude-$(whoami)@$(hostname -s)"
ok "Ключ создан: $KEY"
fi
# --- 3. авторизация ключа на сервере ---------------------------------------
key_ok() { ssh -i "$KEY" -p "$SSH_PORT" -o BatchMode=yes -o ConnectTimeout=8 \
-o StrictHostKeyChecking=accept-new "$TARGET" 'true' 2>/dev/null; }
if key_ok; then
ok "Вход по ключу уже работает"
else
warn "Ключ ещё не авторизован на сервере."
if [[ -t 0 ]]; then
read -r -p "Скопировать ключ на $TARGET сейчас? (нужен пароль от сервера) [Y/n]: " a
if [[ ! "$a" =~ ^[Nn] ]]; then
ssh-copy-id -i "$KEY.pub" -p "$SSH_PORT" "$TARGET" || warn "ssh-copy-id не удался"
fi
fi
if ! key_ok; then
echo
echo "============================================================"
printf "\033[1m ДОБАВЬ ЭТОТ ПУБЛИЧНЫЙ КЛЮЧ НА СЕРВЕР (в ~/.ssh/authorized_keys юзера %s):\033[0m\n" "$SSH_USER"
echo "============================================================"
echo "restrict,port-forwarding $(cat "$KEY.pub")"
echo "============================================================"
echo " (префикс restrict,port-forwarding = только туннель, без шелла)"
echo " Как только ключ добавят — туннель поднимется сам (~минута)."
echo
fi
fi
# --- 4. Privoxy (SOCKS5 -> HTTP) -------------------------------------------
CFG="$BREW_PREFIX/etc/privoxy/config"
info "Настраиваю Privoxy: $CFG"
if grep -q '# >>> o3-claude >>>' "$CFG"; then
sed -i '' '/# >>> o3-claude >>>/,/# <<< o3-claude <<</d' "$CFG"
fi
sed -i '' -E 's/^([[:space:]]*socket-timeout[[:space:]]+.*)$/# \1/' "$CFG"
LISTEN_LINE=""
if ! grep -qE "^[[:space:]]*listen-address[[:space:]]+127\.0\.0\.1:$HTTP_PORT([[:space:]]|\$)" "$CFG"; then
LISTEN_LINE="listen-address 127.0.0.1:$HTTP_PORT"
fi
cat >> "$CFG" <<EOF
# >>> o3-claude >>>
$LISTEN_LINE
socket-timeout 900
forward-socks5t / 127.0.0.1:$SOCKS_PORT .
# <<< o3-claude <<<
EOF
brew services restart privoxy >/dev/null
ok "Privoxy настроен и запущен (:$HTTP_PORT)"
# --- 5. автозапуск туннеля (launchd) ---------------------------------------
PLIST="$HOME/Library/LaunchAgents/com.user.o3-claude.plist"
info "Настраиваю автозапуск туннеля..."
launchctl unload "$PLIST" 2>/dev/null || true
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.user.o3-claude</string>
<key>ProgramArguments</key>
<array>
<string>$BREW_PREFIX/bin/autossh</string>
<string>-M</string><string>0</string>
<string>-N</string>
<string>-D</string><string>$SOCKS_PORT</string>
<string>-i</string><string>$KEY</string>
<string>-p</string><string>$SSH_PORT</string>
<string>-o</string><string>ServerAliveInterval=15</string>
<string>-o</string><string>ServerAliveCountMax=2</string>
<string>-o</string><string>ExitOnForwardFailure=yes</string>
<string>-o</string><string>StrictHostKeyChecking=accept-new</string>
<string>$TARGET</string>
</array>
<key>EnvironmentVariables</key>
<dict><key>AUTOSSH_GATETIME</key><string>0</string></dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardErrorPath</key><string>/tmp/o3-claude.err</string>
<key>StandardOutPath</key><string>/tmp/o3-claude.out</string>
</dict>
</plist>
EOF
launchctl load "$PLIST"
ok "Туннель под автозапуском (сам переподнимется при обрыве/смене сети)"
# --- 6. хелсчек ------------------------------------------------------------
BIN="$HOME/.local/bin"; mkdir -p "$BIN"
cat > "$BIN/o3-claude-health" <<'HEALTHEOF'
#!/bin/bash
SOCKS_PORT="${SOCKS_PORT:-__SOCKS__}"; HTTP_PORT="${HTTP_PORT:-__HTTP__}"
g(){ printf "\033[32m%s\033[0m" "$1"; }; r(){ printf "\033[31m%s\033[0m" "$1"; }
echo "=== Проверка o3-claude ==="
launchctl list 2>/dev/null | grep -q o3-claude && echo "autossh (launchd) : $(g OK)" || echo "autossh (launchd) : $(r FAIL)"
lsof -nP -iTCP:$SOCKS_PORT -sTCP:LISTEN >/dev/null 2>&1 && echo "туннель :$SOCKS_PORT : $(g OK)" || echo "туннель :$SOCKS_PORT : $(r FAIL — ключ авторизован на сервере?)"
lsof -nP -iTCP:$HTTP_PORT -sTCP:LISTEN >/dev/null 2>&1 && echo "privoxy :$HTTP_PORT : $(g OK)" || echo "privoxy :$HTTP_PORT : $(r FAIL)"
DIRECT=$(curl -s --max-time 8 https://api.ipify.org)
SOCKS_IP=$(curl -s --max-time 8 --socks5-hostname 127.0.0.1:$SOCKS_PORT https://api.ipify.org)
HTTP_IP=$(HTTPS_PROXY="http://127.0.0.1:$HTTP_PORT" curl -s --max-time 8 https://api.ipify.org)
echo "прямой IP : ${DIRECT:-нет ответа}"
{ [ -n "$SOCKS_IP" ] && [ "$SOCKS_IP" != "$DIRECT" ]; } && echo "SOCKS egress : $(g OK) ($SOCKS_IP)" || echo "SOCKS egress : $(r FAIL) (${SOCKS_IP:-пусто})"
{ [ -n "$HTTP_IP" ] && [ "$HTTP_IP" = "$SOCKS_IP" ]; } && echo "Privoxy egress : $(g OK) ($HTTP_IP)" || echo "Privoxy egress : $(r FAIL) (${HTTP_IP:-пусто})"
echo "========================="
{ [ -n "$SOCKS_IP" ] && [ "$SOCKS_IP" != "$DIRECT" ] && [ "$HTTP_IP" = "$SOCKS_IP" ]; } \
&& { g "Цепочка работает."; echo; } || { r "Ещё не готово — смотри FAIL и /tmp/o3-claude.err"; echo; }
HEALTHEOF
sed -i '' "s/__SOCKS__/$SOCKS_PORT/; s/__HTTP__/$HTTP_PORT/" "$BIN/o3-claude-health"
chmod +x "$BIN/o3-claude-health"
ok "Команда o3-claude-health установлена"
# --- 7. PATH + алиас -------------------------------------------------------
ALIAS="o3-claude"
ZRC="$HOME/.zshrc"; touch "$ZRC"
if grep -q '# >>> o3-claude alias >>>' "$ZRC"; then
sed -i '' '/# >>> o3-claude alias >>>/,/# <<< o3-claude alias <<</d' "$ZRC"
fi
cat >> "$ZRC" <<EOF
# >>> o3-claude alias >>>
export PATH="\$HOME/.local/bin:\$PATH"
alias $ALIAS='HTTPS_PROXY="http://127.0.0.1:$HTTP_PORT" HTTP_PROXY="http://127.0.0.1:$HTTP_PORT" NO_PROXY="$NO_PROXY_VAL" claude'
# <<< o3-claude alias <<<
EOF
ok "Алиас '$ALIAS' добавлен в ~/.zshrc"
# --- 8. финал: проверка + claude -------------------------------------------
sleep 4
echo; "$BIN/o3-claude-health" || true; echo
CLAUDE_INSTALL="ALL_PROXY=\"socks5h://127.0.0.1:$SOCKS_PORT\" curl -fsSL https://claude.ai/install.sh | bash"
if command -v claude >/dev/null 2>&1; then
ok "Claude Code уже установлен"
elif curl -s --max-time 6 --socks5-hostname "127.0.0.1:$SOCKS_PORT" https://api.ipify.org >/dev/null 2>&1; then
info "Ставлю Claude Code через туннель..."
ALL_PROXY="socks5h://127.0.0.1:$SOCKS_PORT" curl -fsSL https://claude.ai/install.sh | bash \
|| warn "не удалось сейчас — поставь позже: $CLAUDE_INSTALL"
else
warn "Туннель ещё не поднят — Claude Code поставишь позже: $CLAUDE_INSTALL"
fi
echo
ok "Готово."
echo " Проверка цепочки: o3-claude-health"
echo " Запуск: $ALIAS (новый терминал или: source ~/.zshrc)"