#!/usr/bin/env bash set -Eeuo pipefail readonly XRAY_CONFIG="/usr/local/etc/xray/config.json" readonly XRAY_DIR="/usr/local/etc/xray" readonly STATE_DIR="/etc/ochenstarik-xray" readonly NFT_FILE="${STATE_DIR}/routing.nft" readonly ROUTE_HELPER="/usr/local/sbin/ochenstarik-xray-routing" readonly ROUTE_SERVICE="/etc/systemd/system/ochenstarik-xray-routing.service" readonly TPROXY_PORT="12345" readonly SOCKS_PORT="10808" log() { printf '[+] %s\n' "$*"; } warn() { printf '[!] %s\n' "$*" >&2; } die() { printf '[x] %s\n' "$*" >&2; exit 1; } TMP_DIR="" cleanup() { [[ -z "$TMP_DIR" ]] || rm -rf -- "$TMP_DIR" } trap cleanup EXIT public_ipv4() { local ip ip="$(curl -fsS -4 --max-time 12 https://api.ipify.org 2>/dev/null \ || curl -fsS -4 --max-time 12 https://ipv4.icanhazip.com 2>/dev/null \ || true)" printf '%s' "$ip" | tr -d '\r\n' } valid_ipv4() { local ip="$1" octet local -a octets [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 IFS=. read -r -a octets <<< "$ip" for octet in "${octets[@]}"; do (( 10#$octet <= 255 )) || return 1 done } stop_rollback_timer() { systemctl stop ochenstarik-xray-rollback.timer >/dev/null 2>&1 || true systemctl reset-failed ochenstarik-xray-rollback.timer \ ochenstarik-xray-rollback.service >/dev/null 2>&1 || true } vpn_is_configured() { [[ -f "$XRAY_CONFIG" && ! -L "$XRAY_CONFIG" \ && -x "$ROUTE_HELPER" && ! -L "$ROUTE_HELPER" \ && -f "$ROUTE_SERVICE" && ! -L "$ROUTE_SERVICE" ]] } select_existing_action() { local choice printf '\nОбнаружена существующая конфигурация системного VPN.\n' printf ' 1) Подключить VPN с сохранёнными настройками\n' printf ' 2) Отключить VPN, сохранив настройки\n' printf ' 3) Переустановить VPN или заменить подписку\n' printf ' 4) Показать состояние VPN\n' while :; do read -rp 'Выберите действие [1]: ' choice || die "Ввод прерван" choice="${choice:-1}" case "$choice" in 1) ACTION=enable; return 0 ;; 2) ACTION=disable; return 0 ;; 3) ACTION=reconfigure; return 0 ;; 4) ACTION=status; return 0 ;; *) warn "Введите номер от 1 до 4" ;; esac done } disable_vpn() { stop_rollback_timer systemctl disable --now ochenstarik-xray-routing.service >/dev/null 2>&1 || true [[ ! -x "$ROUTE_HELPER" || -L "$ROUTE_HELPER" ]] || "$ROUTE_HELPER" stop log "Системная маршрутизация через Xray отключена; конфигурация сохранена" if command -v curl >/dev/null 2>&1; then printf 'Текущий внешний IP: %s\n' "$(public_ipv4)" fi } enable_vpn() { local direct_ip vpn_ip vpn_is_configured || die "Сохранённая конфигурация VPN не найдена; выполните установку" command -v xray >/dev/null 2>&1 || die "Xray не установлен" xray run -test -config "$XRAY_CONFIG" || die "Xray отклонил сохранённую конфигурацию" systemctl stop ochenstarik-xray-routing.service >/dev/null 2>&1 || true "$ROUTE_HELPER" stop direct_ip="$(public_ipv4)" valid_ipv4 "$direct_ip" || die "Не удалось определить прямой внешний IPv4 перед подключением" systemctl daemon-reload systemctl enable --now xray.service systemctl restart xray.service systemctl enable --now ochenstarik-xray-routing.service systemctl restart ochenstarik-xray-routing.service systemctl is-active --quiet xray.service || die "Служба Xray не запущена" systemctl is-active --quiet ochenstarik-xray-routing.service \ || die "Системная маршрутизация через VPN не запущена" vpn_ip="$(public_ipv4)" if ! valid_ipv4 "$vpn_ip" || [[ "$vpn_ip" == "$direct_ip" ]]; then systemctl stop ochenstarik-xray-routing.service >/dev/null 2>&1 || true "$ROUTE_HELPER" stop die "Проверка внешнего IP не пройдена; VPN автоматически отключён" fi log "Системный VPN подключён с сохранёнными настройками" printf 'Прямой внешний IP: %s\n' "$direct_ip" printf 'Внешний IP через VPN: %s\n' "$vpn_ip" } show_vpn_status() { if systemctl is-active --quiet ochenstarik-xray-routing.service; then printf 'Системный VPN: подключён\n' else printf 'Системный VPN: отключён\n' fi printf 'Xray: %s\n' "$(systemctl is-active xray.service 2>/dev/null || true)" printf 'Маршрутизация: %s\n' \ "$(systemctl is-active ochenstarik-xray-routing.service 2>/dev/null || true)" if [[ -x "$ROUTE_HELPER" && ! -L "$ROUTE_HELPER" ]] \ && systemctl is-active --quiet ochenstarik-xray-routing.service; then "$ROUTE_HELPER" status || warn "Не удалось полностью прочитать правила маршрутизации" fi if command -v curl >/dev/null 2>&1; then printf 'Текущий внешний IP: %s\n' "$(public_ipv4)" fi } [[ "$EUID" -eq 0 ]] || die "Запустите этот скрипт от имени root" ACTION="" case "${1:-}" in "") vpn_is_configured && select_existing_action || ACTION=reconfigure ;; --enable) ACTION=enable ;; --disable) ACTION=disable ;; --reconfigure) ACTION=reconfigure ;; --status) ACTION=status ;; *) die "Использование: $0 [--enable|--disable|--reconfigure|--status]" ;; esac case "$ACTION" in enable) enable_vpn; exit 0 ;; disable) disable_vpn; exit 0 ;; status) show_vpn_status; exit 0 ;; reconfigure) ;; *) die "Некорректное действие VPN: $ACTION" ;; esac export DEBIAN_FRONTEND=noninteractive log "Установка системных зависимостей" apt-get update apt-get install -y curl ca-certificates jq nftables iproute2 python3 if systemctl is-active --quiet ochenstarik-xray-routing.service; then warn "Обнаружена действующая маршрутизация Xray; временно отключаю её для проверки прямого IP" systemctl disable --now ochenstarik-xray-routing.service elif systemctl is-enabled --quiet ochenstarik-xray-routing.service 2>/dev/null; then systemctl disable ochenstarik-xray-routing.service fi BEFORE_IP="$(public_ipv4)" valid_ipv4 "$BEFORE_IP" || die "Не удалось определить исходный внешний IPv4" printf 'Внешний IP до подключения VPN: %s\n' "$BEFORE_IP" printf 'Вставьте прямую ссылку vless:// или HTTPS-ссылку подписки 3x-ui. Ввод будет скрыт.\n' read -rsp 'Ссылка VLESS/подписки: ' SUBSCRIPTION_INPUT printf '\n' [[ "$SUBSCRIPTION_INPUT" == vless://* || "$SUBSCRIPTION_INPUT" == https://* ]] \ || die "Поддерживаются только ссылки vless:// и https://" TMP_DIR="$(mktemp -d)" chmod 700 "$TMP_DIR" RAW_FILE="${TMP_DIR}/subscription.txt" LINKS_FILE="${TMP_DIR}/vless-links.txt" LINK_FILE="${TMP_DIR}/vless-link.txt" PORTS_FILE="${TMP_DIR}/vless-ports.txt" GENERATED_CONFIG="${TMP_DIR}/config.json" INSTALLER_FILE="${TMP_DIR}/install-release.sh" if [[ "$SUBSCRIPTION_INPUT" == vless://* ]]; then printf '%s' "$SUBSCRIPTION_INPUT" > "$RAW_FILE" else { printf 'url = "%s"\n' "$SUBSCRIPTION_INPUT" printf 'fail\nsilent\nshow-error\nlocation\n' printf 'proto = "=https"\ntlsv1.2\nmax-time = 30\n' } | curl --config - > "$RAW_FILE" \ || die "Не удалось загрузить подписку" fi unset SUBSCRIPTION_INPUT chmod 600 "$RAW_FILE" python3 - "$RAW_FILE" "$LINKS_FILE" "$PORTS_FILE" <<'PY' import base64 import pathlib import sys import urllib.parse raw = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").strip() def find_vless(text: str): result = [] for line in text.replace("\r", "").split("\n"): line = line.strip() if line.startswith("vless://"): result.append(line) return result links = find_vless(raw) if not links: try: padded = raw.replace("-", "+").replace("_", "/") padded += "=" * (-len(padded) % 4) decoded = base64.b64decode(padded, validate=False).decode("utf-8") except Exception as exc: raise SystemExit(f"Подписка не является Base64/VLESS: {exc}") links = find_vless(decoded) links = list(dict.fromkeys(links)) if not links: raise SystemExit("В подписке не найдена ссылка vless://") ports = [] for link in links: try: port = urllib.parse.urlsplit(link).port except ValueError as exc: raise SystemExit(f"Некорректный порт в VLESS-ссылке: {exc}") if port is None: raise SystemExit("В одной из VLESS-ссылок отсутствует порт") if port not in ports: ports.append(port) pathlib.Path(sys.argv[2]).write_text("\n".join(links) + "\n", encoding="utf-8") pathlib.Path(sys.argv[3]).write_text( "\n".join(str(port) for port in ports) + "\n", encoding="utf-8" ) PY chmod 600 "$LINKS_FILE" "$PORTS_FILE" mapfile -t SUBSCRIPTION_PORTS < "$PORTS_FILE" (( ${#SUBSCRIPTION_PORTS[@]} > 0 )) || die "В подписке не найдены доступные порты" printf 'Доступные порты в подписке: %s\n' "${SUBSCRIPTION_PORTS[*]}" while :; do read -rp "Выберите порт подписки [${SUBSCRIPTION_PORTS[0]}]: " SUBSCRIPTION_PORT SUBSCRIPTION_PORT="${SUBSCRIPTION_PORT:-${SUBSCRIPTION_PORTS[0]}}" [[ "$SUBSCRIPTION_PORT" =~ ^[0-9]{1,5}$ ]] \ && (( 10#$SUBSCRIPTION_PORT >= 1 && 10#$SUBSCRIPTION_PORT <= 65535 )) \ || { warn "Порт должен быть числом от 1 до 65535"; continue; } SUBSCRIPTION_PORT="$((10#$SUBSCRIPTION_PORT))" port_found="no" for available_port in "${SUBSCRIPTION_PORTS[@]}"; do if [[ "$SUBSCRIPTION_PORT" == "$available_port" ]]; then port_found="yes" break fi done [[ "$port_found" == yes ]] && break warn "Порт ${SUBSCRIPTION_PORT} отсутствует в подписке" done python3 - "$LINKS_FILE" "$LINK_FILE" "$SUBSCRIPTION_PORT" <<'PY' import pathlib import sys import urllib.parse links = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").splitlines() selected_port = int(sys.argv[3]) for link in links: if urllib.parse.urlsplit(link).port == selected_port: pathlib.Path(sys.argv[2]).write_text(link, encoding="utf-8") break else: raise SystemExit(f"VLESS-ссылка для порта {selected_port} не найдена") PY chmod 600 "$LINK_FILE" log "Выбран порт подписки ${SUBSCRIPTION_PORT}" VPN_IP="$(python3 - "$LINK_FILE" "$GENERATED_CONFIG" "$TPROXY_PORT" "$SOCKS_PORT" <<'PY' import json import pathlib import socket import sys import urllib.parse link = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").strip() output = pathlib.Path(sys.argv[2]) tproxy_port = int(sys.argv[3]) socks_port = int(sys.argv[4]) uri = urllib.parse.urlsplit(link) if uri.scheme.lower() != "vless": raise SystemExit("Ожидалась ссылка vless://") client_id = urllib.parse.unquote(uri.username or "") host = uri.hostname or "" port = uri.port query = {key: values[-1] for key, values in urllib.parse.parse_qs( uri.query, keep_blank_values=True ).items()} if not client_id or not host or not port: raise SystemExit("В ссылке отсутствует UUID, адрес или порт") transport = query.get("type", "tcp").lower() if transport not in {"tcp", "raw"}: raise SystemExit(f"Поддерживается только TCP/RAW, получено: {transport}") security = query.get("security", "").lower() if security != "reality": raise SystemExit(f"Поддерживается только REALITY, получено: {security or 'пусто'}") server_name = query.get("sni", "") password = query.get("pbk", "") fingerprint = query.get("fp", "chrome") or "chrome" short_id = query.get("sid", "") spider_x = query.get("spx", "/") or "/" flow = query.get("flow", "") encryption = query.get("encryption", "none") or "none" if not server_name or not password: raise SystemExit("В REALITY-ссылке отсутствуют параметры sni или pbk") try: addresses = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM) except socket.gaierror as exc: raise SystemExit(f"Не удалось определить IPv4 VPN-сервера: {exc}") vpn_ip = addresses[0][4][0] settings = { "address": vpn_ip, "port": port, "id": client_id, "encryption": encryption, } if flow: settings["flow"] = flow config = { "log": { "loglevel": "warning", "access": "/var/log/xray/access.log", "error": "/var/log/xray/error.log", }, "inbounds": [ { "tag": "transparent-in", "port": tproxy_port, "protocol": "tunnel", "settings": { "allowedNetwork": "tcp,udp", "followRedirect": True, }, "sniffing": { "enabled": True, "destOverride": ["http", "tls", "quic"], }, "streamSettings": { "sockopt": {"tproxy": "tproxy"}, }, }, { "tag": "socks-in", "listen": "127.0.0.1", "port": socks_port, "protocol": "socks", "settings": {"udp": True}, }, ], "outbounds": [ { "tag": "vpn-out", "protocol": "vless", "settings": settings, "streamSettings": { "network": "raw", "security": "reality", "realitySettings": { "serverName": server_name, "fingerprint": fingerprint, "password": password, "shortId": short_id, "spiderX": spider_x, }, "sockopt": {"mark": 2}, }, } ], } output.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(vpn_ip) PY )" || die "Не удалось разобрать VLESS-ссылку" valid_ipv4 "$VPN_IP" || die "Получен некорректный IPv4 VPN-сервера" chmod 600 "$GENERATED_CONFIG" log "Подписка разобрана: VLESS + TCP/RAW + REALITY" log "Установка или обновление Xray из официального репозитория XTLS" curl --fail --silent --show-error --location \ --proto '=https' --tlsv1.2 \ https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh \ -o "$INSTALLER_FILE" chmod 700 "$INSTALLER_FILE" bash "$INSTALLER_FILE" install command -v xray >/dev/null 2>&1 || die "Xray не установлен" install -d -m 755 "$XRAY_DIR" /var/log/xray touch /var/log/xray/access.log /var/log/xray/error.log log "Проверка конфигурации Xray" xray run -test -config "$GENERATED_CONFIG" \ || die "Xray отклонил сгенерированную конфигурацию" XRAY_BACKUP="" if [[ -f "$XRAY_CONFIG" ]]; then XRAY_BACKUP="${XRAY_CONFIG}.bak.$(date +%F-%H%M%S-%N)" cp -a -- "$XRAY_CONFIG" "$XRAY_BACKUP" fi XRAY_GROUP="$(id -gn nobody 2>/dev/null || printf 'nogroup')" install -o root -g "$XRAY_GROUP" -m 640 "$GENERATED_CONFIG" "$XRAY_CONFIG" chown nobody:"$XRAY_GROUP" /var/log/xray/access.log /var/log/xray/error.log chmod 600 /var/log/xray/access.log /var/log/xray/error.log systemctl daemon-reload systemctl enable --now xray.service systemctl restart xray.service sleep 2 if ! systemctl is-active --quiet xray.service; then [[ -z "$XRAY_BACKUP" ]] || cp -a -- "$XRAY_BACKUP" "$XRAY_CONFIG" systemctl restart xray.service >/dev/null 2>&1 || true die "Служба Xray не запустилась; предыдущая конфигурация восстановлена" fi log "Проверка VLESS через локальный SOCKS-порт ${SOCKS_PORT}" PROXY_IP="$(curl -fsS -4 --max-time 20 \ --socks5-hostname "127.0.0.1:${SOCKS_PORT}" \ https://api.ipify.org 2>/dev/null || true)" PROXY_IP="$(printf '%s' "$PROXY_IP" | tr -d '\r\n')" if ! valid_ipv4 "$PROXY_IP"; then [[ -z "$XRAY_BACKUP" ]] || cp -a -- "$XRAY_BACKUP" "$XRAY_CONFIG" systemctl restart xray.service >/dev/null 2>&1 || true die "VLESS не прошёл предварительную проверку через SOCKS" fi printf 'IP через локальный прокси Xray: %s\n' "$PROXY_IP" [[ "$PROXY_IP" != "$BEFORE_IP" ]] \ || die "IP через Xray совпадает с исходным; системная маршрутизация не включена" SSHD_EFFECTIVE_CONFIG="$(sshd -T 2>/dev/null || true)" SSH_PORT="$(awk '$1 == "port" { print $2; exit }' <<< "$SSHD_EFFECTIVE_CONFIG")" [[ "$SSH_PORT" =~ ^[0-9]+$ ]] || SSH_PORT="20202" install -d -m 700 -o root -g root "$STATE_DIR" cat > "$NFT_FILE" < "$ROUTE_HELPER" </dev/null 2>&1 \ && nft delete table ip ochenstarik_xray || true nft list table ip6 ochenstarik_xray6 >/dev/null 2>&1 \ && nft delete table ip6 ochenstarik_xray6 || true while ip rule del priority 100 fwmark 0x1/0x1 table 100 2>/dev/null; do :; done ip route flush table 100 2>/dev/null || true } case "\${1:-}" in start) stop_routes ip route add local 0.0.0.0/0 dev lo table 100 ip rule add priority 100 fwmark 0x1/0x1 table 100 nft -f "\$NFT_FILE" ;; stop) stop_routes ;; restart) stop_routes "\$0" start ;; status) ip rule show ip route show table 100 nft list table ip ochenstarik_xray ;; *) echo "Использование: \$0 {start|stop|restart|status}" >&2 exit 2 ;; esac EOF chmod 700 "$ROUTE_HELPER" cat > "$ROUTE_SERVICE" </dev/null 2>&1 || true stop_rollback_timer die "Итоговая проверка IP не пройдена; маршрутизация автоматически отключена" fi systemctl enable ochenstarik-xray-routing.service >/dev/null stop_rollback_timer printf '\nНастройка завершена успешно.\n' printf 'IP до VPN: %s\n' "$BEFORE_IP" printf 'IP через Xray: %s\n' "$PROXY_IP" printf 'IP после VPN: %s\n' "$AFTER_IP" printf 'SSH-порт %s и ответы сервисов 443/63636 оставлены напрямую.\n' "$SSH_PORT" printf 'Публичный IPv6 заблокирован, чтобы исключить обход VPN.\n' printf '\nПодключить VPN повторно:\n %s --enable\n' "$0" printf 'Отключить VPN с сохранением настроек:\n %s --disable\n' "$0" printf 'Заменить подписку:\n %s --reconfigure\n' "$0" printf 'Показать состояние VPN:\n %s --status\n' "$0" printf 'Проверить маршрутизацию:\n %s status\n' "$ROUTE_HELPER"