diff --git a/scripts/fuck-rkn.sh b/scripts/fuck-rkn.sh new file mode 100644 index 0000000..25dcec0 --- /dev/null +++ b/scripts/fuck-rkn.sh @@ -0,0 +1,1908 @@ +#!/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