#!/usr/bin/env bash # Переключение режима выхода в интернет через SSH SOCKS (macOS / Linux). # См. docs/connectivity/vpn/SSH_EXIT_MODES.md # # Типичный кейс из‑за рубежа (RU‑сервисы режут иностранный IP): # ./scripts/vpn/exit-via-ru.sh # SOCKS + системный прокси macOS → IP RU # ./scripts/vpn/exit-via-local.sh # стоп туннелей + системный SOCKS off set -e SSH_EXIT_MAIN_PORT="${SSH_EXIT_MAIN_PORT:-10809}" SSH_EXIT_STOP_PORTS="${SSH_EXIT_STOP_PORTS:-10809 1081 1082}" SSH_EXIT_RU_HOST="${SSH_EXIT_RU_HOST:-hsites-ahau}" # На macOS по умолчанию включаем системный SOCKS (Safari/Chrome и т.п.). # Отключить: SSH_EXIT_SYSTEM_PROXY=0 if [ -z "${SSH_EXIT_SYSTEM_PROXY+x}" ]; then if [ "$(uname -s)" = "Darwin" ]; then SSH_EXIT_SYSTEM_PROXY=1 else SSH_EXIT_SYSTEM_PROXY=0 fi fi SSH_EXIT_NET_SERVICE="${SSH_EXIT_NET_SERVICE:-Wi-Fi}" SSH_SSH_STABLE_OPTS="-o TCPKeepAlive=yes -o IPQoS=throughput -o ServerAliveInterval=60 -o ServerAliveCountMax=4" SSH_SSH_RU_OPTS="-o TCPKeepAlive=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=6" usage() { cat </dev/null 2>&1 } enable_system_socks() { system_proxy_wanted || return 0 # PAC поверх SOCKS часто ломает выход — гасим. networksetup -setautoproxystate "$SSH_EXIT_NET_SERVICE" off 2>/dev/null || true networksetup -setsocksfirewallproxy "$SSH_EXIT_NET_SERVICE" 127.0.0.1 "$SSH_EXIT_MAIN_PORT" networksetup -setsocksfirewallproxystate "$SSH_EXIT_NET_SERVICE" on echo "Системный SOCKS ($SSH_EXIT_NET_SERVICE): 127.0.0.1:$SSH_EXIT_MAIN_PORT ON" } disable_system_socks() { system_proxy_wanted || return 0 networksetup -setsocksfirewallproxystate "$SSH_EXIT_NET_SERVICE" off 2>/dev/null || true echo "Системный SOCKS ($SSH_EXIT_NET_SERVICE): OFF" } print_system_socks() { if ! is_darwin || ! command -v networksetup >/dev/null 2>&1; then return 0 fi echo "=== Системный SOCKS ($SSH_EXIT_NET_SERVICE) ===" networksetup -getsocksfirewallproxy "$SSH_EXIT_NET_SERVICE" 2>/dev/null || true } stop_dynamic_tunnels() { local p for p in $SSH_EXIT_STOP_PORTS; do pkill -f "ssh.*-D ${p} " 2>/dev/null || true pkill -f "ssh.*-D ${p}\$" 2>/dev/null || true done sleep 0.4 for p in $SSH_EXIT_STOP_PORTS; do if command -v lsof >/dev/null 2>&1; then # shellcheck disable=SC2046 lsof -ti ":$p" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true fi done } cmd_local() { # Сначала снимаем системный SOCKS — иначе при мёртвом туннеле сеть «висит». disable_system_socks stop_dynamic_tunnels echo "SSH SOCKS на портах ($SSH_EXIT_STOP_PORTS) остановлены. Выход — с локального публичного IP." } ensure_port_free() { if command -v lsof >/dev/null 2>&1; then if lsof -i :"$SSH_EXIT_MAIN_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then echo "Порт $SSH_EXIT_MAIN_PORT занят — освобождаю..." pkill -f "ssh.*-D ${SSH_EXIT_MAIN_PORT} " 2>/dev/null || true sleep 0.4 # shellcheck disable=SC2046 lsof -ti ":$SSH_EXIT_MAIN_PORT" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true sleep 0.3 fi fi if command -v lsof >/dev/null 2>&1 && lsof -i :"$SSH_EXIT_MAIN_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then echo "Ошибка: порт $SSH_EXIT_MAIN_PORT всё ещё занят. Закройте процесс вручную или: $0 local" exit 1 fi } wait_socks_ready() { local i for i in 1 2 3 4 5 6 7 8 9 10; do if command -v lsof >/dev/null 2>&1; then if lsof -i :"$SSH_EXIT_MAIN_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then return 0 fi else sleep 0.5 return 0 fi sleep 0.3 done echo "Ошибка: SOCKS на :$SSH_EXIT_MAIN_PORT не поднялся." >&2 exit 1 } cmd_ru() { ensure_port_free # shellcheck disable=SC2086 ssh -D "$SSH_EXIT_MAIN_PORT" -f -N $SSH_SSH_RU_OPTS "$SSH_EXIT_RU_HOST" wait_socks_ready enable_system_socks echo "SOCKS 127.0.0.1:$SSH_EXIT_MAIN_PORT → $SSH_EXIT_RU_HOST (выход с IP RU-прокси)." echo "Проверка: $0 verify | в браузере: https://ipinfo.io (ожидается RU)" } cmd_ru_do() { ensure_port_free if ! ssh -o BatchMode=yes -o ConnectTimeout=6 -o ConnectionAttempts=1 "$SSH_EXIT_RU_HOST" "exit 0" 2>/dev/null; then echo "Ошибка: $SSH_EXIT_RU_HOST недоступен. Режим ru-do только Mac→RU→DO; прямой Mac→DO не используется." >&2 exit 1 fi # shellcheck disable=SC2086 ssh -D "$SSH_EXIT_MAIN_PORT" -f -N $SSH_SSH_STABLE_OPTS telegram-socks-via-do wait_socks_ready enable_system_socks echo "SOCKS 127.0.0.1:$SSH_EXIT_MAIN_PORT → $SSH_EXIT_RU_HOST → DO (telegram-socks-via-do). Снаружи виден IP DO." } cmd_status() { local p any=0 if ! command -v lsof >/dev/null 2>&1; then echo "lsof не найден — статус портов недоступен." else for p in $SSH_EXIT_STOP_PORTS; do if lsof -i :"$p" -sTCP:LISTEN -t >/dev/null 2>&1; then echo "--- :$p ---" lsof -i :"$p" -sTCP:LISTEN any=1 fi done if [ "$any" = 0 ]; then echo "Ни один из портов не слушает: $SSH_EXIT_STOP_PORTS" fi fi echo "" print_system_socks } cmd_verify() { echo "Без прокси (локальный выход / curl игнорирует системный SOCKS):" curl -sS --max-time 12 https://ipinfo.io/ip 2>/dev/null || echo "(ошибка curl)" echo "" echo "Через SOCKS socks5h://127.0.0.1:$SSH_EXIT_MAIN_PORT:" curl -sS --max-time 12 -x "socks5h://127.0.0.1:$SSH_EXIT_MAIN_PORT" https://ipinfo.io/ip 2>/dev/null || echo "(нет ответа — туннель не поднят или порт другой)" echo "" echo "Кратко через SOCKS (ip + страна):" curl -sS --max-time 12 -x "socks5h://127.0.0.1:$SSH_EXIT_MAIN_PORT" https://ipinfo.io/json 2>/dev/null \ | sed -n 's/.*"ip": *"\([^"]*\)".*/IP: \1/p; s/.*"country": *"\([^"]*\)".*/country: \1/p' 2>/dev/null | head -4 || true echo "" print_system_socks } case "${1:-}" in local|off|stop) cmd_local ;; ru) cmd_ru ;; ru-do|ru_do) cmd_ru_do ;; status) cmd_status ;; verify) cmd_verify ;; -h|--help|help) usage ;; "") usage exit 1 ;; *) echo "Неизвестная команда: $1" usage exit 1 ;; esac