#!/bin/bash # ============================================================================= # setup.sh — Единый установщик: # • Telegram MTProto прокси (nginx stream + telemt + fail2ban) # • VPN обход блокировок (nginx stream + xray VLESS+Reality) # # Схема: клиент → nginx:443 (SNI-роутинг) → telemt | xray (127.0.0.1) # # Адаптировано под «сибирскую» схему DPI (июнь 2026, разбор @hyperion_cs): # заморозка включается при совпадении трёх сигналов (логическое И): # 1) подозрительная подсеть сервера — рвём выбором провайдера/CDN (вне скрипта); # 2) массовый TLS-фингерпринт — random_fp() даёт только «лояльные» # пресеты uTLS (firefox/edge/360/qq/android/random), без chrome/safari/ios # и без синтетического randomized; # 3) лавина TLS-соединений к одному SNI — лечим XHTTP + XMUX (mux в outbound). # Достаточно стабильно нарушать ЛЮБОЕ одно звено — правило не срабатывает. # # Использование: # bash setup.sh — интерактивное меню # bash setup.sh telemt — telemt direct: 3 инстанса (443/5223/8530) + UFW rate-limit # bash setup.sh telemt-rf — telemt РФ-нода: те же инстансы, но выход через SOCKS5 # (бэкенд systemd|docker спрашивается, или env TELEMT_BACKEND) # bash setup.sh update [mss] — обновить telemt и задать client_mss во всех инстансах # (mss: tspu|extreme-low|2in8|88..4096|"", по умолч. tspu) # bash setup.sh xray-euro — установить xray евро-ноду (Reality direct, порт 443→853→8443) # bash setup.sh xray-rf — установить xray РФ-ноду (Reality direct, порт 443→853→8443, мост → евро) # bash setup.sh status — статус всех сервисов # bash setup.sh purge — полное удаление всего # ============================================================================= set -euo pipefail # ══════════════════════════════════════════════════════════════════════════════ # КОНСТАНТЫ # ══════════════════════════════════════════════════════════════════════════════ NGINX_CONF_MAIN="/etc/nginx/nginx.conf" NGINX_STREAM_DIR="/etc/nginx/stream.d" NGINX_STREAM_LOG="/var/log/nginx/stream-access.log" TELEMT_STREAM_CONF="${NGINX_STREAM_DIR}/telemt.conf" XRAY_STREAM_CONF="${NGINX_STREAM_DIR}/xray.conf" F2B_FILTER="/etc/fail2ban/filter.d/nginx-stream-empty-sni.conf" F2B_JAIL="/etc/fail2ban/jail.d/nginx-stream-empty-sni.local" XRAY_DIR="/usr/local/etc/xray" XRAY_BIN="/usr/local/bin/xray" XRAY_STATE_FILE="/root/euro-node.conf" NGINX_EXT_PORT=443 # внешний порт nginx (всегда 443) # ══════════════════════════════════════════════════════════════════════════════ # ЦВЕТА И ВЫВОД # ══════════════════════════════════════════════════════════════════════════════ RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m' CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; } die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } section() { echo -e "\n${BOLD}${CYAN}══════════ $* ══════════${NC}"; } banner() { echo -e "${CYAN}${BOLD}" echo "╔══════════════════════════════════════════════════════════╗" echo "║ nginx + telemt + xray — Установщик ║" echo "╚══════════════════════════════════════════════════════════╝" echo -e "${NC}" } # ══════════════════════════════════════════════════════════════════════════════ # ПРАВА И ОС # ══════════════════════════════════════════════════════════════════════════════ [[ $EUID -eq 0 ]] || die "Запустите от root или через sudo" detect_os() { OS_FAMILY="" if [[ -f /etc/os-release ]]; then source /etc/os-release case "${ID:-}" in debian|ubuntu|linuxmint|raspbian) OS_FAMILY="debian" ;; rhel|centos|fedora|rocky|almalinux) OS_FAMILY="rhel" ;; alpine) OS_FAMILY="alpine" ;; *) case "${ID_LIKE:-}" in *debian*) OS_FAMILY="debian" ;; *rhel*|*fedora*) OS_FAMILY="rhel" ;; esac ;; esac fi [[ -f /etc/alpine-release ]] && OS_FAMILY="alpine" [[ -z "$OS_FAMILY" ]] && die "Неподдерживаемый дистрибутив" ok "ОС: ${OS_FAMILY} (${ID:-?} ${VERSION_ID:-})" } pkg_update() { case "$OS_FAMILY" in debian) DEBIAN_FRONTEND=noninteractive apt-get update -qq ;; rhel) dnf makecache -q 2>/dev/null || yum makecache -q ;; alpine) apk update -q ;; esac } pkg_install() { case "$OS_FAMILY" in debian) DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" ;; rhel) dnf install -y -q "$@" 2>/dev/null || yum install -y -q "$@" ;; alpine) apk add --no-cache "$@" ;; esac } svc_enable() { systemctl enable "$1" 2>/dev/null || rc-update add "$1" default 2>/dev/null || true; } svc_start() { systemctl start "$1" 2>/dev/null || rc-service "$1" start 2>/dev/null || true; } svc_restart() { systemctl restart "$1" 2>/dev/null || rc-service "$1" restart 2>/dev/null || true; } svc_stop() { systemctl stop "$1" 2>/dev/null || rc-service "$1" stop 2>/dev/null || true; } svc_disable() { systemctl disable "$1" 2>/dev/null || rc-update del "$1" 2>/dev/null || true; } svc_active() { systemctl is-active --quiet "$1" 2>/dev/null; } get_svc_mgr() { if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then echo "systemd" elif command -v rc-service &>/dev/null; then echo "openrc" else echo "none"; fi } # ══════════════════════════════════════════════════════════════════════════════ # ПОРТЫ # ══════════════════════════════════════════════════════════════════════════════ port_in_use() { local p="$1" ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE ":${p}$" && return 0 netstat -tlnH 2>/dev/null | awk '{print $4}' | grep -qE ":${p}$" && return 0 local hex; hex="$(printf '%04X' "$p")" grep -qi ":${hex} " /proc/net/tcp /proc/net/tcp6 2>/dev/null && return 0 return 1 } find_free_port() { local candidates=("$@") for p in "${candidates[@]}"; do port_in_use "$p" || { echo "$p"; return; } done echo "${candidates[-1]}" } show_ports() { section "Занятые порты" local watch="22 80 443 8080 8443 9090 9091 18443 19443 28443" local raw; raw="$(ss -tlnpH 2>/dev/null || netstat -tlnpH 2>/dev/null || true)" printf " ${BOLD}%-8s %-22s %-20s %s${NC}\n" "ПОРТ" "АДРЕС" "СЕРВИС" "ПРОЦЕСС" printf " %s\n" "──────────────────────────────────────────────────────────" local found=0 for p in $watch; do local line; line="$(echo "$raw" | awk -v pt=":${p}" '$4 ~ pt"$" {print; exit}')" [[ -z "$line" ]] && continue found=1 local addr proc svc col addr="$(echo "$line" | awk '{print $4}')" proc="$(echo "$line" | grep -oP 'users:\(\("([^"]+)"' | head -1 | grep -oP '"[^"]+"' | tr -d '"' || echo "?")" case "$p" in 22) svc="SSH"; col="$GREEN" ;; 443|8443) svc="HTTPS"; col="$CYAN" ;; 9091) svc="telemt API"; col="$CYAN" ;; 18443|19443|28443) svc="внутр.порт"; col="$CYAN" ;; *) svc="—"; col="$YELLOW" ;; esac printf " ${col}%-8s %-22s %-20s %s${NC}\n" "$p" "$addr" "$svc" "${proc:-?}" done [[ $found -eq 0 ]] && echo " Ни один из отслеживаемых портов не занят" printf " %s\n\n" "──────────────────────────────────────────────────────────" } get_public_ip() { local ip="" ip="$(curl -s4 -m 5 ifconfig.me 2>/dev/null || curl -s4 -m 5 api.ipify.org 2>/dev/null || true)" echo "${ip:-}" } # ══════════════════════════════════════════════════════════════════════════════ # NGINX — общие функции (используются обоими модулями) # ══════════════════════════════════════════════════════════════════════════════ # Проверка: установлен ли nginx с поддержкой stream nginx_stream_ok() { command -v nginx &>/dev/null || return 1 local tmpd; tmpd="$(mktemp -d)" cat > "$tmpd/nginx.conf" << 'NCEOF' include /etc/nginx/modules-enabled/*.conf; events {} stream {} NCEOF local out; out="$(nginx -t -c "$tmpd/nginx.conf" 2>&1)" rm -rf "$tmpd" echo "$out" | grep -q 'unknown directive "stream"' && return 1 return 0 } # Устанавливаем nginx + stream модуль если нужно nginx_ensure() { section "nginx" if nginx_stream_ok; then ok "nginx со stream модулем уже установлен" return fi info "Устанавливаю nginx..." case "$OS_FAMILY" in debian) pkg_install nginx pkg_install libnginx-mod-stream 2>/dev/null || pkg_install nginx-full 2>/dev/null || true ;; rhel) pkg_install nginx 2>/dev/null || true pkg_install nginx-mod-stream 2>/dev/null || true ;; alpine) pkg_install nginx nginx-mod-stream ;; esac nginx_stream_ok || die "nginx stream модуль недоступен после установки. Попробуйте вручную: apt install libnginx-mod-stream" ok "nginx установлен" } # Восстанавливаем nginx.conf если он повреждён (нет events{} или http{}) nginx_restore_if_broken() { # Считаем повреждённым если нет блока events{} if grep -q 'events[[:space:]]*{' "$NGINX_CONF_MAIN" 2>/dev/null; then return 0 # файл в порядке fi warn "nginx.conf повреждён (отсутствует блок events{}) — восстанавливаю..." cat > "$NGINX_CONF_MAIN" << 'NGINX_DEFAULT' user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 768; } http { sendfile on; tcp_nopush on; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } NGINX_DEFAULT ok "nginx.conf восстановлен" } # Добавляем блок stream{} в nginx.conf (идемпотентно) nginx_ensure_stream_block() { # Сначала проверяем целостность файла nginx_restore_if_broken # Проверяем через grep -E (Extended regex) — поддерживает | if grep -qE 'proxy-stream-block|telemt-stream-block|xray-stream-block' "$NGINX_CONF_MAIN" 2>/dev/null; then ok "Блок stream{} уже есть в nginx.conf" return fi # Дополнительно — вдруг stream{} добавлен вручную if grep -q 'stream[[:space:]]*{' "$NGINX_CONF_MAIN" 2>/dev/null; then ok "Блок stream{} уже есть в nginx.conf" return fi info "Добавляю блок stream{} в конец nginx.conf..." mkdir -p "$NGINX_STREAM_DIR" printf '\n# proxy-stream-block\nstream {\n include /etc/nginx/stream.d/*.conf;\n}\n# /proxy-stream-block\n' \ >> "$NGINX_CONF_MAIN" } # Перезапуск nginx с валидацией nginx_reload() { info "Проверяю конфигурацию nginx..." if ! nginx -t 2>&1; then die "Конфигурация nginx невалидна — см. вывод выше" fi # Если внешний порт уже занят НЕ nginx (частый случай — легаси telemt, # держащий 443 напрямую), nginx упадёт с 'Address already in use'. # Ловим это заранее и даём понятную диагностику вместо туманной ошибки. if port_in_use "$NGINX_EXT_PORT"; then local holder holder="$(ss -tlnpH "( sport = :${NGINX_EXT_PORT} )" 2>/dev/null | grep -oE 'users:\(\("[^"]+"' | grep -oE '"[^"]+"' | tr -d '"' | head -1 || true)" if [[ -n "$holder" && "$holder" != "nginx" ]]; then warn "Порт ${NGINX_EXT_PORT} уже занят процессом '${holder}' — nginx не сможет его забиндить." if [[ "$holder" == telemt* ]]; then warn "Похоже, это telemt в direct-режиме (он сам слушает ${NGINX_EXT_PORT})." warn "На этой ноде xray ходит через nginx:${NGINX_EXT_PORT} — нельзя держать оба на одном порту." warn "Останови/удали telemt: systemctl stop telemt* ; либо запусти 'bash $0 purge' для очистки легаси." fi die "Освободи порт ${NGINX_EXT_PORT} и повтори (см. предупреждения выше)." fi fi svc_enable nginx svc_restart nginx ok "nginx перезапущен, слушает :${NGINX_EXT_PORT}" } # Проверяем доступность сервиса через nginx nginx_check_port() { local sni="$1" info "Проверяю доступность ${sni}:${NGINX_EXT_PORT}..." local code code="$(curl -sk -o /dev/null -w '%{http_code}' \ --connect-timeout 5 \ --resolve "${sni}:${NGINX_EXT_PORT}:127.0.0.1" \ "https://${sni}:${NGINX_EXT_PORT}" 2>/dev/null || true)" if [[ "$code" =~ ^[0-9]+$ ]]; then ok "nginx отвечает на :${NGINX_EXT_PORT} (HTTP ${code}) ✓" else warn "nginx не ответил на :${NGINX_EXT_PORT} — проверьте: ufw status, systemctl status nginx" fi } # Открываем внешний порт в файрволе firewall_open() { local port="$1" label="${2:-proxy}" if command -v ufw &>/dev/null; then ufw allow "${port}/tcp" comment "$label" > /dev/null 2>&1 || true ok "ufw: порт ${port}/tcp открыт" elif command -v firewall-cmd &>/dev/null; then firewall-cmd --permanent --add-port="${port}/tcp" > /dev/null 2>&1 || true firewall-cmd --reload > /dev/null 2>&1 || true ok "firewalld: порт ${port}/tcp открыт" elif command -v iptables &>/dev/null; then iptables -C INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null || \ iptables -A INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null || true ok "iptables: порт ${port}/tcp открыт" else warn "Файрвол не найден — откройте порт ${port} вручную" fi } # Блокируем внутренний порт снаружи (только lo) firewall_block_internal() { local port="$1" if command -v iptables &>/dev/null; then iptables -C INPUT -p tcp --dport "$port" -i lo -j ACCEPT 2>/dev/null || \ iptables -I INPUT 1 -p tcp --dport "$port" -i lo -j ACCEPT 2>/dev/null || true iptables -C INPUT -p tcp --dport "$port" -j DROP 2>/dev/null || \ iptables -A INPUT -p tcp --dport "$port" -j DROP 2>/dev/null || true ok "Внутренний порт ${port} доступен только через lo" fi } # Записываем stream-конфиг с map по SNI (один server на порту 443) # Принимает: файл назначения, лог-файл, массив пар "sni:backend" nginx_write_stream_conf() { local dest="$1" logfile="$2" shift 2 local pairs=("$@") # "sni1:backend1" "sni2:backend2" ... mkdir -p "$NGINX_STREAM_DIR" "$(dirname "$logfile")" { cat << 'HEADER' # nginx stream: SNI-роутинг (авто-генерация setup.sh) # Редактируйте /etc/nginx/stream.d/*.conf или перезапустите setup.sh log_format proxy_stream '$remote_addr [$time_local] ' '$protocol $status $bytes_sent $bytes_received ' '$session_time "$ssl_preread_server_name" -> $upstream_addr'; HEADER echo "access_log ${logfile} proxy_stream;" echo "" echo "map \$ssl_preread_server_name \$proxy_backend {" echo ' default "";' for pair in "${pairs[@]}"; do local sni="${pair%%:*}" local backend="${pair#*:}" printf ' "%s" "%s";\n' "${sni}" "${backend}" done echo "}" echo "" cat << 'SERVER' server { listen 443; ssl_preread on; proxy_connect_timeout 5s; proxy_timeout 600s; proxy_pass $proxy_backend; } SERVER } > "$dest" ok "Stream конфиг записан → ${dest}" } # Добавляем или обновляем одну запись в существующем map nginx_upsert_map_entry() { local conf="$1" sni="$2" backend="$3" if grep -qF "\"${sni}\"" "$conf" 2>/dev/null; then # Обновляем существующую sed -i "s|\"${sni}\".*|\"${sni}\" \"${backend}\";|" "$conf" info "Обновлена запись SNI '${sni}' → ${backend}" else # Вставляем новую после строки с default sed -i "/default.*\"\";/a\\ \"${sni}\" \"${backend}\";" "$conf" ok "Добавлена запись SNI '${sni}' → ${backend}" fi } # ══════════════════════════════════════════════════════════════════════════════ # FAIL2BAN (для telemt — баним пустой/неверный SNI) # ══════════════════════════════════════════════════════════════════════════════ configure_fail2ban() { section "fail2ban" if ! command -v fail2ban-client &>/dev/null; then info "Устанавливаю fail2ban..." case "$OS_FAMILY" in debian) pkg_install fail2ban ;; rhel) pkg_install fail2ban 2>/dev/null || { pkg_install epel-release 2>/dev/null || true pkg_install fail2ban 2>/dev/null || { warn "fail2ban недоступен"; return; } } ;; alpine) pkg_install fail2ban 2>/dev/null || { warn "fail2ban недоступен"; return; } ;; esac fi mkdir -p "$(dirname "$F2B_FILTER")" "$(dirname "$F2B_JAIL")" tee "$F2B_FILTER" > /dev/null << 'EOF' [Definition] failregex = ^ \[.*\] \S+ \S+ \S+ \S+ \S+ ".*" -> -$ ignoreregex = datepattern = \[%%d/%%b/%%Y:%%H:%%M:%%S %%z\] EOF cat > "$F2B_JAIL" << EOF [nginx-stream-empty-sni] enabled = true filter = nginx-stream-empty-sni logpath = ${NGINX_STREAM_LOG} backend = auto findtime = 300 maxretry = 10 bantime = 21600 action = iptables-multiport[name=nginx-stream-empty-sni, port="${NGINX_EXT_PORT}", protocol=tcp] EOF svc_enable fail2ban svc_restart fail2ban ok "fail2ban: 10 попыток / 5мин → бан 6ч" } # ══════════════════════════════════════════════════════════════════════════════ # МОДУЛЬ TELEMT # ══════════════════════════════════════════════════════════════════════════════ # client_mss: "" (не трогать MSS) | пресеты extreme-low|tspu|2in8 | число 88..4096 # Дефолт "tspu" (=92) — быстрый фикс под агрессивный профиль ТСПУ. # Требует telemt >= 3.4.15. # В direct-схеме (без nginx) MSS реально доходит до клиента — здесь он работает. TELEMT_CLIENT_MSS="${TELEMT_CLIENT_MSS:-tspu}" TELEMT_MIN_MSS_VERSION="3.4.15" # ── Direct-инстансы (без nginx): порт|домен|api_порт ── # Каждая пара порт+домен подобрана под повседневный фоновый трафик ОС/сервисов: # 443 + cloudflare → обычный HTTPS/CDN # 5223 + apple → Apple Push Notification Service (APNs) # 8530 + microsoft → Windows Update / WSUS # Несколько инстансов = запас на точечную блокировку пары порт+домен. # Формат строки: "ИМЯ:ПОРТ:ДОМЕН:API_ПОРТ" TELEMT_DIRECT_INSTANCES=( "telemt1:443:www.cloudflare.com:9091" "telemt2:5223:www.apple.com:9092" "telemt3:8530:www.microsoft.com:9093" ) # Карта «основной порт → запасной», если основной занят (конфликт bind). # 443 → 853: оба не шейпятся, 853 = DNS-over-TLS. Для остальных запасного нет # (5223/8530 редко заняты). Это про доступность порта, не про обход DPI. declare -A TELEMT_PORT_FALLBACK=( [443]=853 ) # Фактические порты инстансов после возможной подмены (заполняется при установке). TELEMT_DIRECT_PORTS=() # ── Бэкенд запуска: "systemd" (нативный бинарь + юниты) или "docker" ── # Можно задать заранее через переменную окружения, иначе спросим в меню. TELEMT_BACKEND="${TELEMT_BACKEND:-}" # Официальный образ telemt (можно переопределить в env). TELEMT_IMAGE="${TELEMT_IMAGE:-ghcr.io/telemt/telemt:latest}" # docker-compose файл для 3 инстансов. TELEMT_COMPOSE="/etc/telemt/docker-compose.yml" # ── SOCKS5 upstream (РФ-нода) ── # Если заданы — telemt отправляет исходящий трафик к Telegram через SOCKS5 # (xray/sing-box/ssh -D на localhost). По FAQ telemt для этого режима ОБЯЗАТЕЛЕН # use_middle_proxy=false (он у нас и так false). Пусто = direct (выход прямой). TELEMT_SOCKS5_HOST="" TELEMT_SOCKS5_PORT="" TELEMT_SOCKS5_USER="" TELEMT_SOCKS5_PASS="" # Валидация значения client_mss. Печатает нормализованное значение, либо die. telemt_validate_mss() { local v="$1" case "$v" in ""|extreme-low|tspu|2in8) printf '%s' "$v"; return 0 ;; *) if [[ "$v" =~ ^[0-9]+$ ]] && (( v >= 88 && v <= 4096 )); then printf '%s' "$v"; return 0 fi die "Недопустимое значение client_mss: '${v}' (ожидается пусто|extreme-low|tspu|2in8|число 88..4096)" ;; esac } # Сравнение версий: возвращает 0, если $1 >= $2 (semver x.y.z). telemt_version_ge() { local a="$1" b="$2" [[ "$(printf '%s\n%s\n' "$a" "$b" | sort -V | head -1)" == "$b" ]] } telemt_detect_arch() { local m; m="$(uname -m)" case "$m" in x86_64|amd64) if grep -qE 'avx2.*bmi2|bmi2.*avx2' /proc/cpuinfo 2>/dev/null; then echo "x86_64-v3" else echo "x86_64"; fi ;; aarch64|arm64) echo "aarch64" ;; *) die "Неподдерживаемая архитектура: $m" ;; esac } telemt_detect_libc() { for f in /lib/ld-musl-*.so.* /lib64/ld-musl-*.so.*; do [[ -e "$f" ]] && { echo musl; return; }; done grep -qE '^ID="?alpine"?' /etc/os-release 2>/dev/null && { echo musl; return; } echo gnu } telemt_gen_secret() { local s="" s="$(openssl rand -hex 16 2>/dev/null || true)" [[ ${#s} -eq 32 ]] && { echo "$s"; return; } s="$(dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')" [[ ${#s} -eq 32 ]] && { echo "$s"; return; } die "Не удалось сгенерировать секрет" } telemt_ensure_user() { mkdir -p /opt/telemt /etc/telemt if [[ "$TELEMT_BACKEND" == "docker" ]]; then # Контейнер бежит от root (user: "0:0"); системный юзер telemt на хосте # не нужен. telemt-API пишет config.toml через write-temp → rename в той # же директории, поэтому каталог должен быть доступен на запись владельцу. chown root:root /opt/telemt /etc/telemt chmod 755 /opt/telemt /etc/telemt return fi # systemd: telemt бежит под непривилегированным юзером telemt local nologin; nologin="$(command -v nologin 2>/dev/null || echo /bin/false)" getent group telemt &>/dev/null || grep -q '^telemt:' /etc/group 2>/dev/null || \ groupadd -r telemt 2>/dev/null || addgroup -S telemt 2>/dev/null || true getent passwd telemt &>/dev/null || grep -q '^telemt:' /etc/passwd 2>/dev/null || \ useradd -r -g telemt -d /opt/telemt -s "$nologin" -c "Telemt" telemt 2>/dev/null || \ adduser -S -D -H -h /opt/telemt -s "$nologin" -G telemt telemt 2>/dev/null || true chown telemt:telemt /opt/telemt && chmod 750 /opt/telemt chown telemt:telemt /etc/telemt && chmod 750 /etc/telemt } # Генерация конфигов всех инстансов (общее для systemd и docker). # Заполняет TELEMT_DIRECT_PORTS и TELEMT_DIRECT_SECRETS, пишет /etc/telemt/telemtN.toml. TELEMT_DIRECT_SECRETS=() TELEMT_INSTALLED_VERSION="" _telemt_generate_configs() { telemt_ensure_user local mss; mss="$(telemt_validate_mss "$TELEMT_CLIENT_MSS")" local client_mss_line="" [[ -n "$mss" ]] && client_mss_line="client_mss = \"${mss}\"" # Блок upstream: SOCKS5 (РФ-нода) либо direct (прямой выход). # type="socks5" требует use_middle_proxy=false (у нас уже так). local upstream_block if [[ -n "$TELEMT_SOCKS5_HOST" && -n "$TELEMT_SOCKS5_PORT" ]]; then upstream_block=$'\n[[upstreams]]\ntype = "socks5"\naddress = "'"${TELEMT_SOCKS5_HOST}:${TELEMT_SOCKS5_PORT}"$'"' if [[ -n "$TELEMT_SOCKS5_USER" && -n "$TELEMT_SOCKS5_PASS" ]]; then upstream_block+=$'\nusername = "'"${TELEMT_SOCKS5_USER}"$'"\npassword = "'"${TELEMT_SOCKS5_PASS}"$'"' fi upstream_block+=$'\nweight = 1\nenabled = true' else upstream_block=$'\n[[upstreams]]\ntype = "direct"\nweight = 1\nenabled = true' fi local idx=0 TELEMT_DIRECT_SECRETS=() local entry name port domain api secret escaped_domain for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name port domain api <<< "$entry" # Если основной порт занят (напр. nginx/apache/панель уже на 443) — # переключаемся на запасной из TELEMT_PORT_FALLBACK. Это решает конфликт # bind, а НЕ проблему DPI: по замерам мая-2026 853/8443 на федеральных # операторах блокируются наравне с 443. if port_in_use "$port"; then local fb="${TELEMT_PORT_FALLBACK[$port]:-}" if [[ -n "$fb" ]] && ! port_in_use "$fb"; then warn "Порт ${port} занят — переключаю ${name} на запасной ${fb}" port="$fb" elif [[ -n "$fb" ]]; then warn "Порт ${port} и запасной ${fb} оба заняты — ${name} может не запуститься (bind error)" else warn "Порт ${port} занят, запасного нет — ${name} может не запуститься" fi fi TELEMT_DIRECT_PORTS[idx]="$port" secret="$(telemt_gen_secret)" TELEMT_DIRECT_SECRETS[idx]="$secret" escaped_domain="$(printf '%s' "$domain" | sed 's/\\/\\\\/g; s/"/\\"/g')" cat > "/etc/telemt/${name}.toml" << EOF [general] fast_mode = true use_middle_proxy = false [general.modes] classic = false secure = false tls = true [network] ipv4 = true ipv6 = false prefer = 4 [server] port = ${port} listen_addr_ipv4 = "0.0.0.0" ${client_mss_line} [server.api] enabled = true listen = "127.0.0.1:${api}" whitelist = ["127.0.0.1/32"] [censorship] tls_domain = "${escaped_domain}" mask = true mask_port = 443 tls_emulation = true unknown_sni_action = "reject_handshake" fake_cert_len = 2048 [access] replay_check_len = 65536 ignore_time_skew = false [access.users] ${name} = "${secret}" ${upstream_block} EOF # Права на конфиг зависят от бэкенда: # - systemd: telemt бежит под юзером telemt → 640 root:telemt (узкие права); # - docker: внутри контейнера группы telemt нет, образ может бежать под # другим UID → даём 644 root:root, чтобы конфиг гарантированно читался. if [[ "$TELEMT_BACKEND" == "docker" ]]; then chown root:root "/etc/telemt/${name}.toml" chmod 644 "/etc/telemt/${name}.toml" else chown root:telemt "/etc/telemt/${name}.toml" chmod 640 "/etc/telemt/${name}.toml" fi ok "${name}: порт ${port}, ${domain}, api ${api}" idx=$((idx + 1)) done } # ── Бэкенд systemd: нативный бинарь + юнит на инстанс ── install_telemt_systemd() { section "Установка telemt (systemd, ${#TELEMT_DIRECT_INSTANCES[@]} инстанса)" TELEMT_INSTALLED_VERSION="$(telemt_download_binary)" [[ -n "$TELEMT_INSTALLED_VERSION" ]] || die "Не удалось установить бинарь telemt" if [[ -n "$TELEMT_CLIENT_MSS" ]] && ! telemt_version_ge "$TELEMT_INSTALLED_VERSION" "$TELEMT_MIN_MSS_VERSION"; then warn "telemt ${TELEMT_INSTALLED_VERSION} < ${TELEMT_MIN_MSS_VERSION}: client_mss будет проигнорирован ядром telemt." fi _telemt_generate_configs local svcmgr; svcmgr="$(get_svc_mgr)" local entry name port domain api for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name port domain api <<< "$entry" if [[ "$svcmgr" == "systemd" ]]; then cat > "/etc/systemd/system/${name}.service" << EOF [Unit] Description=Telemt MTProto Proxy (${domain}) After=network-online.target Wants=network-online.target [Service] Type=simple User=telemt Group=telemt WorkingDirectory=/opt/telemt ExecStart=/bin/telemt /etc/telemt/${name}.toml Restart=on-failure RestartSec=5 LimitNOFILE=65536 AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true [Install] WantedBy=multi-user.target EOF elif [[ "$svcmgr" == "openrc" ]]; then cat > "/etc/init.d/${name}" << EOF #!/sbin/openrc-run name="${name}" command="/bin/telemt" command_args="/etc/telemt/${name}.toml" command_background=true command_user="telemt:telemt" pidfile="/run/${name}.pid" directory="/opt/telemt" rc_ulimit="-n 65536" depend() { need net; } EOF chmod 0755 "/etc/init.d/${name}" fi done [[ "$svcmgr" == "systemd" ]] && systemctl daemon-reload for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name _ _ _ <<< "$entry" svc_enable "$name" svc_restart "$name" done ok "telemt ${TELEMT_INSTALLED_VERSION} установлен (systemd)" } # ── Бэкенд docker: официальный образ + 3 сервиса в одном compose ── # network_mode: host — порты контейнеров видны на хосте напрямую (как systemd), # что сохраняет работу UFW per-port rate-limit и client_mss (нужен NET_ADMIN). install_telemt_docker() { section "Установка telemt (docker, ${#TELEMT_DIRECT_INSTANCES[@]} инстанса)" telemt_ensure_docker _telemt_generate_configs info "Тяну образ ${TELEMT_IMAGE}..." docker pull "$TELEMT_IMAGE" || warn "Не удалось обновить образ (использую локальный, если есть)" # Собираем docker-compose с 3 сервисами. # client_mss требует CAP_NET_ADMIN; бинд привилегированных портов — NET_BIND_SERVICE. # Конфиг монтируем как директорию /etc/telemt (требование API telemt для # атомарного rename), каждому сервису передаём свой файл аргументом. { echo "services:" local entry name port domain api for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name port domain api <<< "$entry" cat << EOF ${name}: image: ${TELEMT_IMAGE} container_name: ${name} restart: unless-stopped network_mode: host user: "0:0" cap_add: - NET_BIND_SERVICE - NET_ADMIN volumes: - /etc/telemt:/etc/telemt command: ["/etc/telemt/${name}.toml"] ulimits: nofile: soft: 65536 hard: 65536 logging: driver: json-file options: max-size: "10m" max-file: "3" EOF done } > "$TELEMT_COMPOSE" chmod 640 "$TELEMT_COMPOSE" info "Запускаю контейнеры..." (cd /etc/telemt && docker compose up -d) || die "Не удалось запустить контейнеры telemt" TELEMT_INSTALLED_VERSION="$(docker run --rm "$TELEMT_IMAGE" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "?")" ok "telemt ${TELEMT_INSTALLED_VERSION} установлен (docker)" } # Установка Docker по официальному скрипту, если его нет. telemt_ensure_docker() { if command -v docker &>/dev/null && docker compose version &>/dev/null; then return 0 fi info "Устанавливаю Docker (get.docker.com)..." curl -fsSL https://get.docker.com | sh || die "Не удалось установить Docker" svc_enable docker; svc_start docker command -v docker &>/dev/null || die "Docker не установлен" docker compose version &>/dev/null || die "docker compose недоступен (нужен Compose v2)" } # Диспетчер бэкенда: вызывает нужный установщик. install_telemt_direct() { case "$TELEMT_BACKEND" in docker) install_telemt_docker ;; *) install_telemt_systemd ;; esac } # Определить установленный бэкенд по артефактам на диске. # Печатает "docker" | "systemd" | "" (не установлен). telemt_detect_backend() { [[ -f "$TELEMT_COMPOSE" ]] && { echo "docker"; return; } ls /etc/telemt/telemt*.toml &>/dev/null && { echo "systemd"; return; } echo "" } # Активен ли инстанс $1 — независимо от бэкенда. telemt_instance_active() { local name="$1" if [[ "$(telemt_detect_backend)" == "docker" ]]; then command -v docker &>/dev/null && docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name" else svc_active "$name" 2>/dev/null fi } # Перезапустить все инстансы — независимо от бэкенда. telemt_restart_all() { if [[ "$(telemt_detect_backend)" == "docker" ]]; then (cd /etc/telemt && docker compose restart) 2>/dev/null || true else local entry name for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name _ _ _ <<< "$entry" svc_restart "$name" done fi } # Скачать и установить актуальный бинарь telemt поверх существующего. # Печатает установленную версию в stdout (последней строкой). telemt_download_binary() { local arch libc latest fn url tmpd arch="$(telemt_detect_arch)"; libc="$(telemt_detect_libc)" info "Архитектура: ${arch}, libc: ${libc}" >&2 latest="$(curl -fsI 'https://github.com/telemt/telemt/releases/latest' 2>/dev/null \ | grep -i '^location:' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" [[ -z "$latest" ]] && die "Не удалось определить версию telemt" info "Версия: ${latest}" >&2 fn="telemt-${arch}-linux-${libc}.tar.gz" url="https://github.com/telemt/telemt/releases/download/${latest}/${fn}" tmpd="$(mktemp -d)" info "Скачиваю ${fn}..." >&2 if ! curl -fsSL "$url" -o "${tmpd}/${fn}" 2>/dev/null; then if [[ "$arch" == "x86_64-v3" ]]; then warn "x86_64-v3 недоступен, откат к x86_64..." arch="x86_64"; fn="telemt-${arch}-linux-${libc}.tar.gz" url="https://github.com/telemt/telemt/releases/download/${latest}/${fn}" curl -fsSL "$url" -o "${tmpd}/${fn}" || die "Не удалось скачать telemt" else die "Не удалось скачать telemt: $url" fi fi gzip -dc "${tmpd}/${fn}" | tar -xf - -C "$tmpd" 2>/dev/null || die "Ошибка распаковки" local bin; bin="$(find "$tmpd" -type f -name telemt | head -1)" [[ -n "$bin" ]] || die "Бинарник telemt не найден в архиве" install -m 0755 "$bin" /bin/telemt command -v setcap &>/dev/null && \ setcap cap_net_bind_service,cap_net_admin=+ep /bin/telemt 2>/dev/null || true rm -rf "$tmpd" printf '%s' "$latest" } # Идемпотентно записать client_mss в секцию [server] всех direct-конфигов. # $1 = значение MSS (уже валидированное; пусто = удалить параметр). telemt_set_mss_in_config() { local mss="$1" cfg found=0 for cfg in /etc/telemt/telemt*.toml; do [[ -f "$cfg" ]] || continue found=1 # Удалить любые существующие строки client_mss sed -i -E '/^[[:space:]]*client_mss[[:space:]]*=/d' "$cfg" if [[ -n "$mss" ]]; then if grep -qE '^\[server\][[:space:]]*$' "$cfg"; then sed -i -E "/^\[server\][[:space:]]*$/a client_mss = \"${mss}\"" "$cfg" else warn "Секция [server] не найдена в $cfg — пропуск" fi fi done [[ "$found" -eq 1 ]] || die "Конфиги telemt не найдены в /etc/telemt/" } # Список имён установленных direct-инстансов (по конфигам на диске). telemt_installed_instances() { local cfg name for cfg in /etc/telemt/telemt*.toml; do [[ -f "$cfg" ]] || continue name="$(basename "$cfg" .toml)" echo "$name" done } # Обновление telemt + настройка client_mss одной командой (по всем инстансам). # Использование: update_telemt [MSS] # MSS: extreme-low|tspu|2in8|число 88..4096|"" (снять параметр). По умолчанию tspu. update_telemt() { section "Обновление telemt + client_mss" local insts; insts="$(telemt_installed_instances)" [[ -n "$insts" ]] || \ die "telemt не установлен (нет /etc/telemt/telemt*.toml). Сначала: bash $0 telemt" detect_os local backend; backend="$(telemt_detect_backend)" [[ -n "$backend" ]] || backend="systemd" info "Бэкенд: ${backend}" local mss; mss="$(telemt_validate_mss "${1:-$TELEMT_CLIENT_MSS}")" # 1. Обновить telemt: образ (docker) или бинарь (systemd) local latest if [[ "$backend" == "docker" ]]; then telemt_ensure_docker info "Обновляю образ ${TELEMT_IMAGE}..." docker pull "$TELEMT_IMAGE" || warn "Не удалось обновить образ" latest="$(docker run --rm "$TELEMT_IMAGE" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "?")" else latest="$(telemt_download_binary)" fi # 2. Проверка поддержки client_mss if [[ -n "$mss" && "$latest" != "?" ]] && ! telemt_version_ge "$latest" "$TELEMT_MIN_MSS_VERSION"; then warn "telemt ${latest} < ${TELEMT_MIN_MSS_VERSION}: client_mss не поддерживается этой версией." fi # 3. Прописать параметр во все конфиги (с бэкапом) local cfg for cfg in /etc/telemt/telemt*.toml; do [[ -f "$cfg" ]] && cp -a "$cfg" "${cfg}.bak.$(date +%s)" 2>/dev/null || true done telemt_set_mss_in_config "$mss" if [[ "$backend" == "docker" ]]; then chown root:root /etc/telemt/telemt*.toml 2>/dev/null || true chmod 644 /etc/telemt/telemt*.toml 2>/dev/null || true else chown root:telemt /etc/telemt/telemt*.toml 2>/dev/null || true chmod 640 /etc/telemt/telemt*.toml 2>/dev/null || true fi # 4. Применить: docker — пересоздать контейнеры с новым образом; systemd — рестарт if [[ "$backend" == "docker" ]]; then (cd /etc/telemt && docker compose up -d) || warn "docker compose up -d не выполнен" else telemt_restart_all fi sleep 1 ok "telemt обновлён до ${latest} (${backend})" if [[ -n "$mss" ]]; then ok "client_mss = \"${mss}\" применён ко всем инстансам" else ok "client_mss снят — MSS определяет ядро" fi info "Проверка: grep client_mss /etc/telemt/telemt*.toml" } # ── UFW: открыть порты инстансов + per-port rate-limit (анти-зондирование) ── # По гайду: не больше 1 нового SYN/сек с одного IP на каждый порт. # Раздельные recent-списки на порт (mtp443, mtp5223…) — иначе Telegram при # переключении между прокси шлёт SYN на все порты разом и лимит рвёт соединение. telemt_ufw_ratelimit() { section "UFW: порты + rate-limit" if ! command -v ufw &>/dev/null; then warn "ufw не установлен — пропускаю rate-limit. Открой порты вручную." return fi # Порты инстансов — фактические (после возможной подмены 443→853). # Если массив пуст (вызов вне install), читаем порт из конфигов на диске. local ports=() local entry name port if [[ ${#TELEMT_DIRECT_PORTS[@]} -gt 0 ]]; then ports=("${TELEMT_DIRECT_PORTS[@]}") else for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name port _ _ <<< "$entry" if [[ -f "/etc/telemt/${name}.toml" ]]; then port="$(awk -F'=' '/^port[[:space:]]*=/{gsub(/[[:space:]]/,"",$2); print $2; exit}' "/etc/telemt/${name}.toml")" fi ports+=("$port") done fi # SSH первым — критично, чтобы не отрезать себя local ssh_port; ssh_port="$(ss -tlnpH 2>/dev/null | grep -i sshd | grep -oE ':[0-9]+' | head -1 | tr -d ':' || true)" ssh_port="${ssh_port:-22}" info "Разрешаю SSH-порт ${ssh_port} перед включением UFW" ufw allow "${ssh_port}/tcp" >/dev/null 2>&1 || true local p for p in "${ports[@]}"; do ufw allow "${p}/tcp" >/dev/null 2>&1 || true done ufw --force enable >/dev/null 2>&1 || true # Модуль xt_recent — без него UFW молча выбросит правила -m recent modprobe xt_recent 2>/dev/null || true echo xt_recent > /etc/modules-load.d/xt_recent.conf 2>/dev/null || true if ! lsmod 2>/dev/null | grep -q xt_recent; then warn "Модуль xt_recent не загрузился — rate-limit не будет применён." warn "Проверь вручную: modprobe xt_recent && lsmod | grep xt_recent" ok "Порты открыты (без rate-limit)" return fi # Бэкап и вставка правил в ufw-before-input (после established) local rules="/etc/ufw/before.rules" if [[ -f "$rules" ]]; then cp "$rules" "${rules}.bak.$(date +%s)" 2>/dev/null || true TELEMT_RL_PORTS="${ports[*]}" python3 - "$rules" << 'PYEOF' import os, sys path = sys.argv[1] ports = os.environ["TELEMT_RL_PORTS"].split() lines = open(path).readlines() if any("MTProto rate-limit" in l for l in lines): print(" правила rate-limit уже есть, пропуск"); raise SystemExit idx = None for i, l in enumerate(lines): if "ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT" in l: idx = i + 1; break if idx is None: # запасная точка: вставка после первого ufw-before-input ACCEPT lo for i, l in enumerate(lines): if "ufw-before-input -i lo -j ACCEPT" in l: idx = i + 1; break if idx is None: print(" ОШИБКА: точка вставки в before.rules не найдена"); raise SystemExit block = ["\n# === MTProto rate-limit (1 SYN/сек на IP per-port) ===\n"] for p in ports: block.append(f"-A ufw-before-input -p tcp --dport {p} --syn -m recent --name mtp{p} --rcheck --seconds 1 -j DROP\n") block.append(f"-A ufw-before-input -p tcp --dport {p} --syn -m recent --name mtp{p} --set -j ACCEPT\n") block.append("# === конец MTProto rate-limit ===\n") lines[idx:idx] = block open(path, "w").writelines(lines) print(f" rate-limit вставлен для портов: {', '.join(ports)}") PYEOF ufw reload >/dev/null 2>&1 || true ok "UFW rate-limit активен (1 SYN/сек/IP на порт)" else warn "$rules не найден — rate-limit пропущен, порты открыты" fi } # Получить tls-ссылку инстанса из его локального API. $1 = api-порт. # Печатает ссылку или пусто. Делает несколько попыток (API поднимается не мгновенно). telemt_link_from_api() { local api="$1" i link for i in 1 2 3 4 5; do link="$(curl -s --max-time 3 "http://127.0.0.1:${api}/v1/users" 2>/dev/null \ | python3 -c "import sys,json try: d=json.load(sys.stdin) print(d['data'][0]['links']['tls'][0]) except Exception: pass" 2>/dev/null)" [[ -n "$link" ]] && { printf '%s' "$link"; return 0; } sleep 1 done return 1 } # Общая установка telemt (3 инстанса) + UFW + вывод ссылок. # $1 = роль ("direct" | "rf") — влияет только на заголовок и подсказки. # Режим выхода определяется переменными TELEMT_SOCKS5_* (заданы до вызова). _telemt_install_and_report() { local role="$1" detect_os show_ports # Выбор бэкенда запуска, если не задан заранее (env TELEMT_BACKEND). if [[ -z "$TELEMT_BACKEND" ]]; then echo "" echo -e "${BOLD} Бэкенд запуска telemt:${NC}" echo -e " ${GREEN}1)${NC} systemd (нативный бинарь + юниты)" echo -e " ${GREEN}2)${NC} docker (официальный образ ${TELEMT_IMAGE}, 3 сервиса)" echo -n " Выбор [1]: " read -r _be case "${_be:-1}" in 2) TELEMT_BACKEND="docker" ;; *) TELEMT_BACKEND="systemd" ;; esac fi info "Бэкенд: ${TELEMT_BACKEND}" # Зависимости. python3/jq/ufw нужны на хосте в обоих режимах (API-ссылки, # UFW rate-limit). Для systemd дополнительно нужны tar/gzip/openssl (бинарь); # для docker — сам docker (ставится в install_telemt_docker). pkg_update if [[ "$TELEMT_BACKEND" == "docker" ]]; then pkg_install curl jq python3 iptables ufw else pkg_install curl wget tar gzip openssl jq python3 iptables ufw fi # Установка (диспетчер выберет systemd или docker) install_telemt_direct # UFW: открыть порты инстансов + per-port rate-limit telemt_ufw_ratelimit sleep 2 local server_ip; server_ip="$(get_public_ip)" echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" if [[ "$role" == "rf" ]]; then echo -e "${BOLD}${GREEN}║ TELEGRAM ПРОКСИ РФ (telemt → SOCKS5) НАСТРОЕН ${NC}" else echo -e "${BOLD}${GREEN}║ TELEGRAM ПРОКСИ (telemt direct) НАСТРОЕН ${NC}" fi echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" if [[ "$role" == "rf" ]]; then echo -e "${BOLD}Схема:${NC} Клиент → telemt (FakeTLS) → SOCKS5 ${TELEMT_SOCKS5_HOST}:${TELEMT_SOCKS5_PORT} → туннель → Telegram" else echo -e "${BOLD}Схема:${NC} Клиент → telemt (FakeTLS, порт=домен) → Telegram (прямой выход)" fi [[ -n "$TELEMT_CLIENT_MSS" ]] && \ echo -e " client_mss: ${TELEMT_CLIENT_MSS} — фрагментация TCP клиента (доходит до клиента: nginx нет)" echo "" local entry name port domain api link status local i=0 for entry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r name port domain api <<< "$entry" # Фактический порт (после возможной подмены 443→853) port="${TELEMT_DIRECT_PORTS[$i]:-$port}" if telemt_instance_active "$name" 2>/dev/null; then status="${GREEN}active${NC}"; else status="${RED}не запущен${NC}"; fi echo -e "${BOLD}${CYAN}── ${name}: порт ${port}, домен ${domain} [${status}${CYAN}]${NC}" link="$(telemt_link_from_api "$api" || true)" if [[ -n "$link" ]]; then link="$(printf '%s' "$link" | sed -E "s/server=[^&]*/server=${server_ip}/")" echo -e " ${BOLD}${CYAN}${link}${NC}" else local hex_domain client_secret secret secret="${TELEMT_DIRECT_SECRETS[$i]:-}" hex_domain="$(printf '%s' "$domain" | xxd -p 2>/dev/null | tr -d '\n' || \ printf '%s' "$domain" | od -A n -t x1 | tr -d ' \n')" client_secret="ee${secret}${hex_domain}" echo -e " ${YELLOW}(API не ответил, ссылка собрана вручную)${NC}" echo -e " ${BOLD}${CYAN}tg://proxy?server=${server_ip}&port=${port}&secret=${client_secret}${NC}" fi echo "" i=$((i + 1)) done echo -e "${BOLD}Команды:${NC}" if [[ "$TELEMT_BACKEND" == "docker" ]]; then echo -e " Логи: docker logs -f telemt1 (telemt2 / telemt3)" echo -e " Рестарт: cd /etc/telemt && docker compose restart" echo -e " Обновить: cd /etc/telemt && docker compose pull && docker compose up -d" else echo -e " Логи: journalctl -u telemt1 -f (telemt2 / telemt3)" echo -e " Рестарт: systemctl restart telemt1 telemt2 telemt3" fi echo -e " Статус: bash $0 status" echo -e " Ссылки: for p in 9091 9092 9093; do curl -s http://127.0.0.1:\$p/v1/users | jq -r '.data[0].links.tls[0]'; done" echo -e " Баны UFW: iptables -L ufw-before-input -n | grep recent" echo "" if [[ "$role" == "rf" ]]; then echo -e "${YELLOW} ⚠️ SOCKS5 ${TELEMT_SOCKS5_HOST}:${TELEMT_SOCKS5_PORT} должен слушать ДО трафика (xray/sing-box).${NC}" echo -e "${YELLOW} Проверка: ss -tlnp | grep ${TELEMT_SOCKS5_PORT} | логи telemt при ошибке выхода.${NC}" fi echo -e "${YELLOW} ⚠️ Порт 5223 (APNs) и 8530 (WSUS) должны быть открыты у провайдера/хостера.${NC}" echo "" } setup_telemt() { section "Telegram MTProto Proxy (telemt, direct — без nginx)" # Direct: SOCKS5 не задаём TELEMT_SOCKS5_HOST=""; TELEMT_SOCKS5_PORT="" TELEMT_SOCKS5_USER=""; TELEMT_SOCKS5_PASS="" _telemt_install_and_report "direct" } setup_telemt_rf() { section "Telegram MTProto Proxy РФ (telemt → SOCKS5)" detect_os # SOCKS5 upstream (туннель на localhost: xray/sing-box/ssh -D) local def_sh="${MT_SOCKS5_HOST:-127.0.0.1}" local def_sp="${MT_SOCKS5_PORT:-1080}" echo -n " SOCKS5 host [${def_sh}]: " read -r TELEMT_SOCKS5_HOST; TELEMT_SOCKS5_HOST="${TELEMT_SOCKS5_HOST:-$def_sh}" echo -n " SOCKS5 port [${def_sp}]: " read -r TELEMT_SOCKS5_PORT; TELEMT_SOCKS5_PORT="${TELEMT_SOCKS5_PORT:-$def_sp}" echo -n " SOCKS5 username (пусто = без авторизации): " read -r TELEMT_SOCKS5_USER if [[ -n "$TELEMT_SOCKS5_USER" ]]; then echo -n " SOCKS5 password: " read -r TELEMT_SOCKS5_PASS else TELEMT_SOCKS5_PASS="" fi # Предупреждаем, если на SOCKS5-порту никто не слушает (не фатально) if command -v ss &>/dev/null && [[ "$TELEMT_SOCKS5_HOST" == "127.0.0.1" || "$TELEMT_SOCKS5_HOST" == "localhost" ]]; then if ! ss -tlnH "( sport = :${TELEMT_SOCKS5_PORT} )" 2>/dev/null | grep -q ":${TELEMT_SOCKS5_PORT}"; then warn "На ${TELEMT_SOCKS5_HOST}:${TELEMT_SOCKS5_PORT} сейчас никто не слушает." warn "Подними туннель (xray/sing-box) до старта трафика, иначе выход к Telegram не заработает." fi fi _telemt_install_and_report "rf" } # ══════════════════════════════════════════════════════════════════════════════ # МОДУЛЬ XRAY (direct, без nginx — Reality сам делает TLS-камуфляж) # ══════════════════════════════════════════════════════════════════════════════ # Внешний порт прослушивания xray выбирается из цепочки кандидатов: берём первый # свободный. 443 предпочтителен (сливается с HTTPS-фоном); 853/8443 — запас на # случай, если 443 занят (напр. telemt уже сидит на нём на той же машине). # Это про доступность порта, а НЕ про обход DPI: Reality маскирует трафик # независимо от порта, а 853/8443 на федеральных операторах режутся как 443. # Можно переопределить: XRAY_PORT=<число> жёстко задаёт порт (без перебора). XRAY_PORT_CANDIDATES=(443 853 8443) XRAY_PORT="${XRAY_PORT:-}" # пусто = выбрать из цепочки; число = жёстко XRAY_SNI="" # Выбрать порт для xray: если XRAY_PORT задан жёстко (env) — проверить его; # иначе перебрать XRAY_PORT_CANDIDATES и взять первый свободный. # Результат пишется в XRAY_PORT. die, если ничего не свободно. xray_resolve_port() { if [[ -n "$XRAY_PORT" ]]; then if port_in_use "$XRAY_PORT"; then warn "Порт ${XRAY_PORT} (задан жёстко) занят." warn "Освободи порт: ss -tlnp '( sport = :${XRAY_PORT} )'" die "Порт ${XRAY_PORT} занят." fi info "Xray порт: ${XRAY_PORT} (задан явно)" return fi local p for p in "${XRAY_PORT_CANDIDATES[@]}"; do if ! port_in_use "$p"; then XRAY_PORT="$p" info "Xray порт: ${XRAY_PORT} (первый свободный из: ${XRAY_PORT_CANDIDATES[*]})" return fi warn "Порт ${p} занят — пробую следующий..." done die "Все порты-кандидаты заняты (${XRAY_PORT_CANDIDATES[*]}). Освободи один или задай XRAY_PORT=<порт>." } install_xray_bin() { section "Установка Xray-core" local ver tmpd ver="$(curl -sI 'https://github.com/XTLS/Xray-core/releases/latest' \ | grep -i '^location:' | grep -oP 'v[\d.]+')" [[ -z "$ver" ]] && ver="v26.2.6" info "Версия: ${ver}" tmpd="$(mktemp -d)" wget -q -O "${tmpd}/xray.zip" \ "https://github.com/XTLS/Xray-core/releases/download/${ver}/Xray-linux-64.zip" unzip -q "${tmpd}/xray.zip" -d "${tmpd}/xray" install -m 755 "${tmpd}/xray/xray" "$XRAY_BIN" mkdir -p "$XRAY_DIR" /usr/local/share/xray /var/log/xray info "Скачиваю geo-базы..." wget -q -O /usr/local/share/xray/geoip.dat \ 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat' wget -q -O /usr/local/share/xray/geosite.dat \ 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat' rm -rf "$tmpd" cat > /etc/systemd/system/xray.service << 'UNIT' [Unit] Description=Xray Service After=network.target nss-lookup.target [Service] User=root ExecStart=/usr/local/bin/xray run -config /usr/local/etc/xray/config.json Restart=on-failure LimitNOFILE=1000000 [Install] WantedBy=multi-user.target UNIT systemctl daemon-reload ok "Xray установлен: $("$XRAY_BIN" version | head -1)" } # Возвращает «лояльный» TLS fingerprint для Reality (uTLS). # По разбору «сибирской» схемы (июнь 2026) Сигнал 2 ловит МАССОВЫЕ фингерпринты, # под которые чаще всего маскируются прокси: chrome, safari, ios — они исключены. # randomized тоже исключён: это СИНТЕТИЧЕСКАЯ генерация (может дать комбинацию # расширений/шифров, которой нет ни у одного реального браузера, — сама по себе # аномалия). Оставляем редкие валидные пресеты + безопасный random # (random берёт один из РЕАЛЬНЫХ пресетов браузера, почерк всегда валидный). random_fp() { local fps=("firefox" "edge" "android" "360" "qq" "random") local idx=$(( RANDOM % ${#fps[@]} )) echo "${fps[$idx]}" } setup_xray_euro() { section "Xray Евро-нода (VLESS+Reality, direct — порт из ${XRAY_PORT_CANDIDATES[*]})" detect_os show_ports # SNI-домен local default_sni="www.amd.com" echo -n " SNI-домен для Reality [${default_sni}]: " read -r XRAY_SNI XRAY_SNI="${XRAY_SNI:-$default_sni}" # Выбор внешнего порта из цепочки кандидатов (443→853→8443) xray_resolve_port info "Xray будет слушать 0.0.0.0:${XRAY_PORT} напрямую (без nginx)" pkg_update pkg_install curl wget unzip uuid-runtime openssl python3 install_xray_bin # Ключи info "Генерирую ключи Reality..." local keys; keys="$("$XRAY_BIN" x25519)" local priv_key pub_key priv_key="$(echo "$keys" | grep -i 'private' | awk '{print $NF}')" pub_key="$(echo "$keys" | grep -i 'public' | awk '{print $NF}')" [[ -z "$priv_key" || -z "$pub_key" ]] && die "Ошибка генерации ключей: $keys" local uuid short_id fp uuid="$(uuidgen)" short_id="$(openssl rand -hex 8)" fp="$(random_fp)" info "TLS fingerprint: ${fp}" # Конфиг xray — слушает напрямую на 0.0.0.0:XRAY_PORT cat > "${XRAY_DIR}/config.json" << EOF { "log": { "loglevel": "warning" }, "dns": { "servers": ["1.1.1.1", "8.8.8.8"] }, "inbounds": [{ "listen": "0.0.0.0", "port": ${XRAY_PORT}, "protocol": "vless", "settings": { "clients": [{ "id": "${uuid}" }], "decryption": "none" }, "streamSettings": { "network": "xhttp", "security": "reality", "realitySettings": { "show": false, "dest": "${XRAY_SNI}:443", "xver": 0, "serverNames": ["${XRAY_SNI}"], "privateKey": "${priv_key}", "shortIds": ["${short_id}"] }, "xhttpSettings": { "path": "/", "mode": "auto" } } }], "outbounds": [{ "protocol": "freedom", "tag": "direct", "settings": { "domainStrategy": "UseIPv4v6" } }] } EOF # PQ шифрование (если поддерживается) local pq_enc="none" pq_enc="$(XRAY_UUID="$uuid" XRAY_DIR="$XRAY_DIR" python3 - << 'PYEOF' import subprocess, json, os try: out = subprocess.check_output(["/usr/local/bin/xray", "vlessenc"]).decode() enc = [l for l in out.split("\n") if '"encryption"' in l] pq = enc[1].split('"encryption": "')[1].rstrip('"') if len(enc) > 1 else "none" if pq != "none": dec = [l for l in out.split("\n") if '"decryption"' in l] pq_dec = dec[1].split('"decryption": "')[1].rstrip('"') if len(dec) > 1 else "none" with open(os.environ["XRAY_DIR"] + "/config.json") as f: cfg = json.load(f) cfg["inbounds"][0]["settings"]["decryption"] = pq_dec with open(os.environ["XRAY_DIR"] + "/config.json", "w") as f: json.dump(cfg, f, indent=2) print(pq) except: print("none") PYEOF )" || pq_enc="none" # Файрвол: открываем внешний порт xray firewall_open "$XRAY_PORT" "xray-direct" # systemd systemctl enable xray > /dev/null 2>&1 systemctl restart xray sleep 2 svc_active xray && ok "Xray запущен" || die "Xray не запустился — journalctl -xe -u xray" # Сохраняем для РФ-ноды local server_ip; server_ip="$(get_public_ip)" cat > "$XRAY_STATE_FILE" << EOF EURO_IP=${server_ip} EURO_PORT=${XRAY_PORT} EURO_UUID=${uuid} EURO_PUBLIC_KEY=${pub_key} EURO_SHORT_ID=${short_id} EURO_SNI=${XRAY_SNI} EURO_ENCRYPTION=${pq_enc} EOF ok "Данные евро-ноды → ${XRAY_STATE_FILE}" # Итог echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${GREEN}║ ЕВРО-НОДА НАСТРОЕНА — СОХРАНИ ДАННЫЕ ${NC}" echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${BOLD}Схема:${NC}" echo -e " Клиент → xray:${XRAY_PORT} (Reality, SNI: ${XRAY_SNI}) → интернет (без nginx)" echo "" echo -e "${BOLD}Данные для РФ-ноды:${NC}" echo -e " EURO_IP=${server_ip} EURO_PORT=${XRAY_PORT}" echo -e " EURO_UUID=${uuid}" echo -e " EURO_PUBLIC_KEY=${pub_key}" echo -e " EURO_SHORT_ID=${short_id} EURO_SNI=${XRAY_SNI}" echo -e " (сохранено в ${XRAY_STATE_FILE})" echo "" echo -e "${BOLD}VLESS URI (прямое подключение к :${XRAY_PORT}):${NC}" echo -e "${GREEN}vless://${uuid}@${server_ip}:${XRAY_PORT}?encryption=${pq_enc}&security=reality&sni=${XRAY_SNI}&fp=${fp}&pbk=${pub_key}&sid=${short_id}&type=xhttp&path=%2F&mode=auto#EURO-Reality${NC}" echo "" echo -e "${BOLD}Проверка:${NC}" echo -e " curl -v https://${server_ip}:${XRAY_PORT} --resolve ${XRAY_SNI}:${XRAY_PORT}:${server_ip}" echo -e " journalctl -fu xray" echo "" echo -e "${YELLOW} ⚠️ Порт ${XRAY_PORT} должен быть открыт у хостера. Следующий шаг: bash setup.sh xray-rf${NC}" echo "" } setup_xray_rf() { section "Xray РФ-нода (мост → евро)" detect_os show_ports # Загружаем данные евро-ноды. EURO_PORT — порт ВЫХОДНОЙ евро-ноды (куда # форвардим), не путать с портом прослушивания РФ-ноды (XRAY_PORT). # Точное значение приходит из state-файла или ручного ввода; дефолт 443. local EURO_IP="" EURO_PORT="443" EURO_UUID="" EURO_PUBLIC_KEY="" local EURO_SHORT_ID="" EURO_SNI="www.amd.com" EURO_ENCRYPTION="none" local RF_SNI="vkvideo.ru" if [[ -f "$XRAY_STATE_FILE" ]]; then echo -e "${GREEN} Найден файл ${XRAY_STATE_FILE}${NC}" echo -n " Загрузить данные евро-ноды из файла? [Y/n]: " read -r ans if [[ -z "$ans" || "$ans" =~ ^[Yy]$ ]]; then # shellcheck disable=SC1090 source "$XRAY_STATE_FILE" ok "Данные загружены: ${EURO_IP}:${EURO_PORT}" fi fi if [[ -z "$EURO_IP" ]]; then echo -e "${BOLD} Введите данные Евро-ноды вручную:${NC}" read -rp " EURO_IP: " EURO_IP read -rp " EURO_PORT [443]: " EURO_PORT; EURO_PORT="${EURO_PORT:-443}" read -rp " EURO_UUID: " EURO_UUID read -rp " EURO_PUBLIC_KEY: " EURO_PUBLIC_KEY read -rp " EURO_SHORT_ID: " EURO_SHORT_ID read -rp " EURO_SNI [www.amd.com]: " EURO_SNI; EURO_SNI="${EURO_SNI:-www.amd.com}" read -rp " EURO_ENCRYPTION [none]: " EURO_ENCRYPTION; EURO_ENCRYPTION="${EURO_ENCRYPTION:-none}" fi echo -n " SNI-домен для РФ-ноды [${RF_SNI}]: " read -r inp_sni; XRAY_SNI="${inp_sni:-$RF_SNI}" # Выбор внешнего порта прослушивания из цепочки (443→853→8443). # Это порт, на котором РФ-нода принимает клиентов — НЕ EURO_PORT (порт евро). xray_resolve_port info "Xray РФ будет слушать 0.0.0.0:${XRAY_PORT} напрямую (без nginx)" pkg_update pkg_install curl wget unzip uuid-runtime openssl python3 install_xray_bin # Ключи для клиентского inbound info "Генерирую ключи Reality для клиентов..." local keys; keys="$("$XRAY_BIN" x25519)" local priv_key pub_key priv_key="$(echo "$keys" | grep -i 'private' | awk '{print $NF}')" pub_key="$(echo "$keys" | grep -i 'public' | awk '{print $NF}')" local uuid short_id fp uuid="$(uuidgen)" short_id="$(openssl rand -hex 8)" fp="$(random_fp)" info "TLS fingerprint: ${fp}" # Конфиг РФ-ноды через Python (PQ + chain) export UUID="$uuid" PRIV_KEY="$priv_key" SHORT_ID="$short_id" FP="$fp" export XRAY_SNI XRAY_PORT export EURO_IP EURO_PORT EURO_UUID EURO_PUBLIC_KEY EURO_SHORT_ID EURO_SNI EURO_ENCRYPTION export XRAY_DIR python3 - << 'PYEOF' import json, os, subprocess e = os.environ # PQ ключи try: out = subprocess.check_output(["/usr/local/bin/xray", "vlessenc"]).decode() dec_lines = [l for l in out.split("\n") if '"decryption"' in l] enc_lines = [l for l in out.split("\n") if '"encryption"' in l] pq_dec = dec_lines[1].split('"decryption": "')[1].rstrip('"') if len(dec_lines) > 1 else "none" pq_enc = enc_lines[1].split('"encryption": "')[1].rstrip('"') if len(enc_lines) > 1 else "none" except: pq_dec = pq_enc = "none" cfg = { "log": {"loglevel": "warning"}, "dns": {"servers": [ {"address": "223.5.5.5", "domains": ["geosite:category-ru", "geosite:yandex"]}, "1.1.1.1", "8.8.8.8" ]}, "inbounds": [{ "tag": "inbound-clients", "listen": "0.0.0.0", "port": int(e["XRAY_PORT"]), "protocol": "vless", "settings": { "clients": [{"id": e["UUID"]}], "decryption": pq_dec }, "streamSettings": { "network": "xhttp", "security": "reality", "realitySettings": { "show": False, "dest": e["XRAY_SNI"] + ":443", "xver": 0, "serverNames": [e["XRAY_SNI"]], "privateKey": e["PRIV_KEY"], "shortIds": [e["SHORT_ID"]] }, "xhttpSettings": { "path": "/", "mode": "packet-up", "extra": {"xPaddingBytes": "100-1000"} } }, "sniffing": {"enabled": True, "destOverride": ["http","tls","quic","fakedns"]} }], "outbounds": [ { "tag": "chain-to-euro", "protocol": "vless", "settings": {"vnext": [{ "address": e["EURO_IP"], "port": int(e["EURO_PORT"]), "users": [{"id": e["EURO_UUID"], "encryption": e["EURO_ENCRYPTION"]}] }]}, "streamSettings": { "network": "xhttp", "security": "reality", "realitySettings": { "fingerprint": e["FP"], "serverName": e["EURO_SNI"], "publicKey": e["EURO_PUBLIC_KEY"], "shortId": e["EURO_SHORT_ID"] }, "xhttpSettings": {"path": "/", "mode": "auto"} }, "mux": { "enabled": True, "concurrency": 8, "xudpConcurrency": 16, "xudpProxyUDP443": "reject" } }, {"protocol": "freedom", "tag": "direct", "settings": {"domainStrategy": "UseIPv4v6"}}, {"protocol": "blackhole", "tag": "block"} ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ {"type":"field","outboundTag":"block","ip":["geoip:private"],"domain":["geosite:category-ads-all"]}, {"type":"field","outboundTag":"direct","domain":["geosite:category-ru","geosite:yandex","regexp:\\.ru$","full:cp.cloudflare.com"]}, {"type":"field","outboundTag":"direct","ip":["geoip:ru"]}, {"type":"field","inboundTag":["inbound-clients"],"outboundTag":"chain-to-euro"} ] } } with open(e["XRAY_DIR"] + "/config.json", "w") as f: json.dump(cfg, f, indent=2, ensure_ascii=False) with open("/tmp/pq_enc_rf.txt", "w") as f: f.write(pq_enc) print(f" Config OK | PQ: {pq_dec[:30]}...") PYEOF local pq_enc_rf; pq_enc_rf="$(cat /tmp/pq_enc_rf.txt 2>/dev/null || echo none)" # Файрвол: открываем внешний порт xray firewall_open "$XRAY_PORT" "xray-direct" # systemd systemctl enable xray > /dev/null 2>&1 systemctl restart xray sleep 2 svc_active xray && ok "Xray запущен" || die "Xray не запустился — journalctl -xe -u xray" # Итог local server_ip; server_ip="$(get_public_ip)" echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${GREEN}║ РФ-НОДА НАСТРОЕНА УСПЕШНО ${NC}" echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${BOLD}Схема трафика:${NC}" echo -e " Клиент → xray:${XRAY_PORT} (Reality, SNI: ${XRAY_SNI}) → Евро (${EURO_IP}):${EURO_PORT} → Интернет (без nginx)" echo "" echo -e "${BOLD}VLESS URI для клиента (прямое подключение к :${XRAY_PORT}):${NC}" echo -e "${GREEN}vless://${uuid}@${server_ip}:${XRAY_PORT}?encryption=${pq_enc_rf}&security=reality&sni=${XRAY_SNI}&fp=${fp}&pbk=${pub_key}&sid=${short_id}&type=xhttp&path=%2F&mode=packet-up#RF-Bridge${NC}" echo "" echo -e "${BOLD}Проверка:${NC}" echo -e " curl -v https://${server_ip}:${XRAY_PORT} --resolve ${XRAY_SNI}:${XRAY_PORT}:${server_ip}" echo -e " journalctl -fu xray" echo "" echo -e "${BOLD}Клиентские приложения:${NC}" echo -e " Android: v2rayNG https://github.com/2dust/v2rayNG" echo -e " Windows: v2rayN https://github.com/2dust/v2rayN" echo -e " iOS: Streisand https://apps.apple.com/app/id6450534064" echo "" echo -e "${BOLD}Роутинг:${NC}" echo -e " ✅ .ru, geoip:ru, yandex → DIRECT 🚫 ads → BLOCK 🌍 остальное → Евро" echo "" echo -e "${YELLOW} ⚠️ Порт ${XRAY_PORT} должен быть открыт у хостера РФ-ноды.${NC}" echo "" } # ══════════════════════════════════════════════════════════════════════════════ # СТАТУС # ══════════════════════════════════════════════════════════════════════════════ show_status() { echo "" echo -e "${BOLD}${CYAN}━━━ Сервисы ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" # telemt теперь мульти-инстанс (telemt1/2/3) — проверяем каждый отдельно for svc in nginx xray fail2ban; do if command -v "$svc" &>/dev/null || systemctl list-units --type=service 2>/dev/null | grep -q "$svc"; then if svc_active "$svc" 2>/dev/null; then echo -e " ${GREEN}● ${svc}: активен${NC}" else echo -e " ${RED}● ${svc}: не запущен${NC}" fi fi done local tname tentry for tentry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r tname _ _ _ <<< "$tentry" if [[ -f "/etc/telemt/${tname}.toml" ]]; then if telemt_instance_active "$tname" 2>/dev/null; then echo -e " ${GREEN}● ${tname}: активен${NC}" else echo -e " ${RED}● ${tname}: не запущен${NC}" fi fi done echo "" echo -e "${BOLD}${CYAN}━━━ nginx stream конфиги ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" for f in "$NGINX_STREAM_DIR"/*.conf; do [[ -f "$f" ]] || continue echo -e " ${CYAN}$(basename "$f")${NC}" grep -E 'listen|"[^"]+"|default' "$f" 2>/dev/null | head -8 | sed 's/^/ /' done echo "" local tbackend; tbackend="$(telemt_detect_backend)" echo -e "${BOLD}${CYAN}━━━ Telemt${tbackend:+ (${tbackend})} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" local found_telemt=0 cfg for cfg in /etc/telemt/telemt*.toml; do [[ -f "$cfg" ]] || continue found_telemt=1 echo -e " ${CYAN}$(basename "$cfg")${NC}" awk -F'"' '/tls_domain/{print " Домен: "$2} /client_mss/{print " MSS: "$2}' "$cfg" 2>/dev/null grep -E '^port' "$cfg" 2>/dev/null | sed 's/^/ /' # Режим выхода: socks5-upstream или direct if grep -q 'type = "socks5"' "$cfg" 2>/dev/null; then local up_addr; up_addr="$(awk -F'"' '/address =/{print $2; exit}' "$cfg" 2>/dev/null)" echo -e " Выход: ${YELLOW}SOCKS5 ${up_addr}${NC}" else echo -e " Выход: direct" fi done [[ "$found_telemt" -eq 0 ]] && echo -e " ${YELLOW}telemt не установлен${NC}" echo "" echo -e "${BOLD}${CYAN}━━━ Xray ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" if [[ -f "${XRAY_DIR}/config.json" ]]; then python3 - << 'PYEOF' import json try: with open("/usr/local/etc/xray/config.json") as f: c = json.load(f) ib = c["inbounds"][0] print(f" Listen: {ib.get('listen','0.0.0.0')}:{ib['port']}") print(f" Протокол: {ib['protocol']}") ob_tags = [o["tag"] for o in c.get("outbounds",[])] print(f" Outbounds: {ob_tags}") except Exception as e: print(f" Ошибка чтения конфига: {e}") PYEOF else echo -e " ${YELLOW}xray не установлен${NC}" fi echo "" show_ports } # ══════════════════════════════════════════════════════════════════════════════ # УДАЛЕНИЕ # ══════════════════════════════════════════════════════════════════════════════ do_purge() { section "Полное удаление" echo -n " Удалить telemt, xray, nginx stream конфиги и fail2ban? [y/N]: " read -r ans [[ "$ans" =~ ^[Yy]$ ]] || { echo "Отменено."; exit 0; } # ── ШАГ 1. Сначала ОСТАНАВЛИВАЕМ все сервисы ───────────────────────────── # Это делается ДО удаления любых файлов: процессы не должны держать порты, # сокеты и конфиги, которые мы собираемся стереть. nginx тоже глушим — # иначе он продолжит слушать :443 с уже удалёнными stream-конфигами. section "Остановка сервисов" for svc in fail2ban xray nginx; do if svc_active "$svc" 2>/dev/null; then info "Останавливаю ${svc}..." svc_stop "$svc" 2>/dev/null || true fi done # telemt — мульти-инстанс (systemd или docker) local tname tentry if [[ -f "$TELEMT_COMPOSE" ]] && command -v docker &>/dev/null; then info "Останавливаю telemt-контейнеры..." (cd /etc/telemt && docker compose down 2>/dev/null) || true for tentry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r tname _ _ _ <<< "$tentry" docker rm -f "$tname" 2>/dev/null || true done fi for tentry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r tname _ _ _ <<< "$tentry" svc_active "$tname" 2>/dev/null && { info "Останавливаю ${tname}..."; svc_stop "$tname" 2>/dev/null || true; } done # Легаси: одиночный telemt.service из ранних версий скрипта (имя без цифры). # Его нет в TELEMT_DIRECT_INSTANCES, поэтому гасим отдельно — иначе он # продолжит держать 443 после удаления файла юнита. if svc_active telemt 2>/dev/null; then info "Останавливаю легаси telemt.service..." svc_stop telemt 2>/dev/null || true fi # Контрольно добиваем процессы telemt/xray, если systemd-юнит уже снят pkill -x telemt 2>/dev/null || true pkill -x xray 2>/dev/null || true sleep 1 ok "Сервисы остановлены" # ── ШАГ 2. Отключаем автозапуск ────────────────────────────────────────── for svc in xray fail2ban telemt; do svc_disable "$svc" 2>/dev/null || true done for tentry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r tname _ _ _ <<< "$tentry" svc_disable "$tname" 2>/dev/null || true done # ── ШАГ 3. Удаляем файлы ───────────────────────────────────────────────── # nginx stream конфиги (сам nginx оставляем — он мог быть до нас) rm -f "$TELEMT_STREAM_CONF" "$XRAY_STREAM_CONF" sed -i '/# proxy-stream-block/,/# \/proxy-stream-block/d' "$NGINX_CONF_MAIN" 2>/dev/null || true sed -i '/# telemt-stream-block/,/# \/telemt-stream-block/d' "$NGINX_CONF_MAIN" 2>/dev/null || true sed -i '/# xray-stream-block/,/# \/xray-stream-block/d' "$NGINX_CONF_MAIN" 2>/dev/null || true # nginx запускаем обратно только если его конфиг валиден и сам бинарь остаётся if command -v nginx &>/dev/null && nginx -t &>/dev/null; then svc_start nginx || true fi # fail2ban rm -f "$F2B_FILTER" "$F2B_JAIL" # telemt — docker: удаляем образ (compose-файл уйдёт с /etc/telemt ниже) if command -v docker &>/dev/null; then docker rmi "$TELEMT_IMAGE" 2>/dev/null || true fi # telemt — мульти-инстанс: юниты, конфиги, бинарь, юзер for tentry in "${TELEMT_DIRECT_INSTANCES[@]}"; do IFS=':' read -r tname _ _ _ <<< "$tentry" rm -f "/etc/systemd/system/${tname}.service" "/etc/init.d/${tname}" done # на всякий случай — старый одиночный юнит из прежней версии скрипта rm -f /etc/systemd/system/telemt.service /etc/init.d/telemt rm -f /bin/telemt /usr/bin/telemt rm -rf /etc/telemt /opt/telemt pkill -u telemt 2>/dev/null || true sleep 1; pkill -9 -u telemt 2>/dev/null || true userdel telemt 2>/dev/null || deluser telemt 2>/dev/null || true groupdel telemt 2>/dev/null || delgroup telemt 2>/dev/null || true # UFW rate-limit правила (вставленные telemt_ufw_ratelimit) if [[ -f /etc/ufw/before.rules ]] && grep -q "MTProto rate-limit" /etc/ufw/before.rules 2>/dev/null; then cp /etc/ufw/before.rules "/etc/ufw/before.rules.bak.$(date +%s)" 2>/dev/null || true sed -i '/# === MTProto rate-limit/,/# === конец MTProto rate-limit ===/d' /etc/ufw/before.rules 2>/dev/null || true command -v ufw &>/dev/null && ufw reload >/dev/null 2>&1 || true info "UFW rate-limit правила удалены" fi # xray rm -f /etc/systemd/system/xray.service "$XRAY_BIN" rm -rf "$XRAY_DIR" /var/log/xray systemctl daemon-reload 2>/dev/null || true echo -e "\n${GREEN}${BOLD}Удаление завершено.${NC}\n" } # ══════════════════════════════════════════════════════════════════════════════ # МЕНЮ # ══════════════════════════════════════════════════════════════════════════════ show_menu() { banner echo -e "${BOLD} Выберите действие:${NC}\n" echo -e " ${GREEN}1)${NC} Установить ${BOLD}Telegram прокси (direct)${NC} (telemt: 3 инстанса + UFW rate-limit, выход прямой)" echo -e " ${GREEN}2)${NC} Установить ${BOLD}Xray Евро-ноду${NC} (VLESS+Reality direct, порт 443→853→8443, выходная)" echo -e " ${GREEN}3)${NC} Установить ${BOLD}Xray РФ-ноду${NC} (VLESS+Reality direct, порт 443→853→8443, мост → евро)" echo -e " ${GREEN}4)${NC} Статус всех сервисов" echo -e " ${GREEN}5)${NC} Удалить всё" echo -e " ${GREEN}6)${NC} Установить ${BOLD}Telegram прокси РФ (через SOCKS5)${NC} (telemt → туннель на localhost)" echo -e " ${GREEN}0)${NC} Выход\n" echo -n " Ваш выбор [0-6]: " read -r choice case "$choice" in 1) setup_telemt ;; 2) setup_xray_euro ;; 3) setup_xray_rf ;; 4) show_status ;; 5) do_purge ;; 6) setup_telemt_rf ;; 0) exit 0 ;; *) echo -e "${RED}Неверный выбор${NC}"; show_menu ;; esac } # ══════════════════════════════════════════════════════════════════════════════ # ТОЧКА ВХОДА # ══════════════════════════════════════════════════════════════════════════════ case "${1:-}" in telemt) setup_telemt ;; telemt-rf) setup_telemt_rf ;; update) shift; update_telemt "${1:-}" ;; xray-euro) setup_xray_euro ;; xray-rf) setup_xray_rf ;; status) show_status ;; purge) do_purge ;; *) show_menu ;; esac