#!/bin/bash set -o pipefail # ══════════════════════════════════════════════════════════════ # KASKAD PRO v2.1 — Cascading VPN / Proxy Manager # Telegram Bot · Live Ping · Monitoring · Alerts · System Stats # Channel: https://www.youtube.com/@antenkaru # ══════════════════════════════════════════════════════════════ KASKAD_VERSION="2.1" KASKAD_DIR="/etc/kaskad" KASKAD_CONF="$KASKAD_DIR/config" KASKAD_LOG="/var/log/kaskad.log" MONITOR_DIR="$KASKAD_DIR/monitors" ALIASES_FILE="$KASKAD_DIR/aliases" BOT_STATE_DIR="$KASKAD_DIR/bot_state" BOT_PID_FILE="/var/run/kaskad_bot.pid" MONITOR_PID_FILE="/var/run/kaskad_monitor.pid" RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m' YELLOW='\033[1;33m'; MAGENTA='\033[0;35m'; WHITE='\033[1;37m' BLUE='\033[0;34m'; NC='\033[0m' IFACE="" MY_IP="" BOT_TOKEN="" BOT_CHAT_ID="" MENU_STYLE="" # ─── Config ─────────────────────────────────────────────────── init_config() { mkdir -p "$KASKAD_DIR" "$MONITOR_DIR" "$BOT_STATE_DIR" touch "$ALIASES_FILE" if [ ! -f "$KASKAD_CONF" ]; then cat > "$KASKAD_CONF" <<'CONF' BOT_TOKEN="" BOT_CHAT_ID="" MENU_STYLE="compact" CONF fi source "$KASKAD_CONF" } save_config_val() { local key="$1" value="$2" if grep -q "^${key}=" "$KASKAD_CONF" 2>/dev/null; then sed -i "s|^${key}=.*|${key}=\"${value}\"|" "$KASKAD_CONF" else echo "${key}=\"${value}\"" >> "$KASKAD_CONF" fi source "$KASKAD_CONF" } # ─── Aliases (server names) ────────────────────────────────── set_alias() { local ip="$1" name="$2" if grep -q "^${ip}=" "$ALIASES_FILE" 2>/dev/null; then sed -i "s|^${ip}=.*|${ip}=${name}|" "$ALIASES_FILE" else echo "${ip}=${name}" >> "$ALIASES_FILE" fi } get_alias() { local ip="$1" grep "^${ip}=" "$ALIASES_FILE" 2>/dev/null | head -1 | cut -d= -f2- } fmt_ip() { local ip="$1" local alias alias=$(get_alias "$ip") if [ -n "$alias" ]; then echo "${alias} (${ip})" else echo "$ip" fi } # ─── Logging ────────────────────────────────────────────────── log_action() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$KASKAD_LOG" } # ─── Validation ─────────────────────────────────────────────── validate_ip() { local ip="$1" if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then IFS='.' read -r -a octets <<< "$ip" for o in "${octets[@]}"; do (( o > 255 )) && return 1 done return 0 fi return 1 } validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( $1 >= 1 && $1 <= 65535 )) } read_validated_ip() { local prompt="${1:-Введите IP адрес назначения:}" while true; do echo -e "$prompt" read -p "> " _RET_IP if validate_ip "$_RET_IP"; then return 0; fi echo -e "${RED}Ошибка: некорректный IP-адрес!${NC}" done } read_validated_port() { local prompt="${1:-Введите порт:}" while true; do echo -e "$prompt" read -p "> " _RET_PORT if validate_port "$_RET_PORT"; then return 0; fi echo -e "${RED}Ошибка: порт должен быть числом от 1 до 65535!${NC}" done } read_server_name() { local ip="$1" local existing existing=$(get_alias "$ip") if [ -n "$existing" ]; then echo -e "Текущее имя для $ip: ${GREEN}$existing${NC}" fi echo -e "Введите имя сервера (или Enter — пропустить):" read -p "> " _RET_NAME if [ -n "$_RET_NAME" ]; then set_alias "$ip" "$_RET_NAME" fi } # ─── System ─────────────────────────────────────────────────── check_root() { if [ "$EUID" -ne 0 ]; then echo -e "${RED}[ERROR] Запустите скрипт с правами root!${NC}" exit 1 fi } detect_interface() { IFACE=$(ip route get 8.8.8.8 2>/dev/null | sed -n 's/.*dev \([^ ]*\).*/\1/p' | head -1) if [[ -z "$IFACE" ]]; then echo -e "${RED}[ERROR] Не удалось определить сетевой интерфейс!${NC}" exit 1 fi } get_my_ip() { MY_IP=$(curl -s4 --max-time 5 ifconfig.me 2>/dev/null || echo "N/A") } save_iptables() { if command -v netfilter-persistent &>/dev/null; then netfilter-persistent save > /dev/null 2>&1 elif command -v service &>/dev/null; then service iptables save > /dev/null 2>&1 fi } prepare_system() { if [ "$(readlink -f "$0" 2>/dev/null)" != "/usr/local/bin/gokaskad" ]; then cp -f "$0" "/usr/local/bin/gokaskad" chmod +x "/usr/local/bin/gokaskad" fi if grep -qE '^[[:space:]]*#?[[:space:]]*net\.ipv4\.ip_forward' /etc/sysctl.conf; then sed -i 's/^#*\s*net\.ipv4\.ip_forward.*/net.ipv4.ip_forward=1/' /etc/sysctl.conf else echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf fi grep -q "^net.core.default_qdisc=fq" /etc/sysctl.conf || echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf grep -q "^net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf || echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf sysctl -p > /dev/null 2>&1 export DEBIAN_FRONTEND=noninteractive local need_install=0 for cmd in iptables jq curl qrencode; do command -v "$cmd" &>/dev/null || need_install=1 done dpkg -s iptables-persistent &>/dev/null 2>&1 || need_install=1 if [ "$need_install" -eq 1 ]; then if command -v apt-get &>/dev/null; then apt-get update -y > /dev/null 2>&1 apt-get install -y iptables-persistent netfilter-persistent qrencode jq curl procps > /dev/null 2>&1 elif command -v dnf &>/dev/null; then dnf install -y iptables-services jq qrencode curl procps-ng > /dev/null 2>&1 elif command -v yum &>/dev/null; then yum install -y iptables-services jq qrencode curl procps-ng > /dev/null 2>&1 else echo -e "${RED}[ERROR] Неподдерживаемый пакетный менеджер!${NC}" exit 1 fi fi } # ─── System Stats ───────────────────────────────────────────── get_system_stats() { local cpu_line load_avg mem_info disk_info uptime_str top_procs cpu_line=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || echo "?") load_avg=$(cat /proc/loadavg 2>/dev/null | awk '{print $1, $2, $3}') mem_info=$(free -m 2>/dev/null | awk '/^Mem:/ {printf "%d/%dMB (%.1f%%)", $3, $2, $3/$2*100}') local swap_info swap_info=$(free -m 2>/dev/null | awk '/^Swap:/ {if($2>0) printf "%d/%dMB", $3, $2; else print "N/A"}') disk_info=$(df -h / 2>/dev/null | awk 'NR==2 {printf "%s/%s (%s)", $3, $2, $5}') uptime_str=$(uptime -p 2>/dev/null || uptime | sed 's/.*up /up /' | sed 's/,.*load.*//') top_procs=$(ps aux --sort=-%cpu 2>/dev/null | head -8 | awk 'NR>1 {printf "%-6s %-4s%% %-4s%% %s\n", $2, $3, $4, $11}') local cpu_usage cpu_usage=$(awk '/^cpu / {u=$2+$4; t=$2+$3+$4+$5+$6+$7+$8; if(t>0) printf "%.1f", u/t*100; else print "0"}' /proc/stat 2>/dev/null) local result="" result+="📊 Системная информация\n\n" result+="Uptime: ${uptime_str}\n" result+="CPU: ${cpu_line} ядер | загрузка: ${cpu_usage}%\n" result+="Load Avg: ${load_avg}\n" result+="RAM: ${mem_info}\n" result+="Swap: ${swap_info}\n" result+="Disk /: ${disk_info}\n\n" result+="Топ процессов (CPU):\n" result+="
PID    CPU%  MEM%  CMD\n"
    result+="${top_procs}
" echo "$result" } # ─── iptables helpers ───────────────────────────────────────── get_rules_list() { iptables -t nat -S PREROUTING 2>/dev/null | grep "DNAT" | while read -r line; do local port proto dest port=$(echo "$line" | grep -oP '(?<=--dport )\d+') proto=$(echo "$line" | grep -oP '(?<=-p )\w+') dest=$(echo "$line" | grep -oP '(?<=--to-destination )[\d.:]+') [ -n "$port" ] && echo "${proto}|${port}|${dest}" done } get_target_ips() { get_rules_list | awk -F'|' '{split($3,a,":"); print a[1]}' | sort -u } remove_rules_for_port() { local proto="$1" in_port="$2" iptables -t nat -S PREROUTING 2>/dev/null | grep "DNAT" | grep -P "\b--dport ${in_port}\b" | grep -P "\b-p ${proto}\b" | while read -r rule; do eval "iptables -t nat -D ${rule#-A }" 2>/dev/null done iptables -S INPUT 2>/dev/null | grep "kaskad" | grep -P "\b--dport ${in_port}\b" | grep -P "\b-p ${proto}\b" | while read -r rule; do eval "iptables -D ${rule#-A }" 2>/dev/null done iptables -S FORWARD 2>/dev/null | grep "kaskad" | grep -P "\b-p ${proto}\b" | while read -r rule; do local rd=$(echo "$rule" | grep -oP '(?<=--dport )\d+') local rs=$(echo "$rule" | grep -oP '(?<=--sport )\d+') if [[ "$rd" == "$in_port" || "$rs" == "$in_port" ]]; then eval "iptables -D ${rule#-A }" 2>/dev/null fi done } check_target_reachable() { local ip="$1" if ! ping -c 1 -W 3 "$ip" &>/dev/null; then echo -e "${YELLOW}[WARN] Сервер $ip не отвечает на ping.${NC}" read -p "Продолжить? (y/n): " ans [[ "$ans" != "y" ]] && return 1 fi return 0 } apply_iptables_rules() { local proto="$1" in_port="$2" out_port="$3" target_ip="$4" name="$5" echo -e "${YELLOW}[*] Применение правил...${NC}" log_action "ADD rule: $proto :$in_port -> $target_ip:$out_port ($name)" remove_rules_for_port "$proto" "$in_port" iptables -I INPUT -p "$proto" --dport "$in_port" \ -m comment --comment "kaskad:${in_port}:${proto}" -j ACCEPT iptables -t nat -A PREROUTING -p "$proto" --dport "$in_port" \ -j DNAT --to-destination "$target_ip:$out_port" if ! iptables -t nat -C POSTROUTING -o "$IFACE" -j MASQUERADE 2>/dev/null; then iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE fi iptables -I FORWARD -p "$proto" -d "$target_ip" --dport "$out_port" \ -m state --state NEW,ESTABLISHED,RELATED \ -m comment --comment "kaskad:${in_port}:${proto}" -j ACCEPT iptables -I FORWARD -p "$proto" -s "$target_ip" --sport "$out_port" \ -m state --state ESTABLISHED,RELATED \ -m comment --comment "kaskad:${in_port}:${proto}" -j ACCEPT if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow "$in_port/$proto" > /dev/null 2>&1 fi save_iptables echo -e "${GREEN}[SUCCESS] $name настроен!${NC}" echo -e "$proto: ${MY_IP:-*}:$in_port -> $target_ip:$out_port" } # ─── Interactive rule configuration ────────────────────────── configure_rule() { local proto="$1" name="$2" echo -e "\n${CYAN}--- Настройка $name ($proto) ---${NC}" read_validated_ip "Введите IP адрес назначения:" local target_ip="$_RET_IP" read_server_name "$target_ip" check_target_reachable "$target_ip" || return read_validated_port "Введите Порт (одинаковый для входа и выхода):" local port="$_RET_PORT" echo -e "\n${YELLOW}Будет создано правило:${NC}" echo -e " $proto: ${MY_IP:-*}:$port -> $(fmt_ip "$target_ip"):$port" read -p "Применить? (y/n): " confirm [[ "$confirm" != "y" ]] && return apply_iptables_rules "$proto" "$port" "$port" "$target_ip" "$name" read -p "Нажмите Enter для возврата в меню..." } configure_custom_rule() { echo -e "\n${CYAN}--- Универсальное кастомное правило ---${NC}" echo -e "${WHITE}Позволяет указать разные порты для входа и выхода.${NC}\n" local proto while true; do echo -e "Выберите протокол (${YELLOW}tcp${NC} или ${YELLOW}udp${NC}):" read -p "> " proto [[ "$proto" == "tcp" || "$proto" == "udp" ]] && break echo -e "${RED}Ошибка: введите tcp или udp!${NC}" done read_validated_ip "Введите IP адрес назначения (куда отправляем трафик):" local target_ip="$_RET_IP" read_server_name "$target_ip" check_target_reachable "$target_ip" || return read_validated_port "Введите ${YELLOW}ВХОДЯЩИЙ Порт${NC} (на этом сервере):" local in_port="$_RET_PORT" read_validated_port "Введите ${YELLOW}ИСХОДЯЩИЙ Порт${NC} (на конечном сервере):" local out_port="$_RET_PORT" echo -e "\n${YELLOW}Будет создано правило:${NC}" echo -e " $proto: ${MY_IP:-*}:$in_port -> $(fmt_ip "$target_ip"):$out_port" read -p "Применить? (y/n): " confirm [[ "$confirm" != "y" ]] && return apply_iptables_rules "$proto" "$in_port" "$out_port" "$target_ip" "Custom Rule" read -p "Нажмите Enter для возврата в меню..." } # ─── List / Delete / Flush ──────────────────────────────────── list_active_rules() { echo -e "\n${CYAN}--- Активные переадресации ---${NC}" echo -e "${WHITE}Сервер каскада: ${GREEN}${MY_IP:-N/A}${NC}\n" echo -e "${MAGENTA}ВХОД (IP:ПОРТ)\t\tПРОТОКОЛ\tЦЕЛЬ${NC}" local rules rules=$(get_rules_list) if [ -z "$rules" ]; then echo -e "${YELLOW}Нет активных правил.${NC}" else echo "$rules" | while IFS='|' read -r proto port dest; do local dest_ip="${dest%:*}" local alias_str alias_str=$(get_alias "$dest_ip") local label="$dest" [ -n "$alias_str" ] && label="$dest [$alias_str]" echo -e "${MY_IP:-*}:$port\t\t$proto\t\t$label" done fi echo "" read -p "Нажмите Enter..." } delete_single_rule() { echo -e "\n${CYAN}--- Удаление правила ---${NC}" local -a rules_arr=() local i=1 while IFS='|' read -r proto port dest; do rules_arr[$i]="$proto|$port|$dest" local dest_ip="${dest%:*}" local label label=$(fmt_ip "$dest_ip") echo -e "${YELLOW}[$i]${NC} ${MY_IP:-*}:$port ($proto) -> ${dest#*:} @ $label" ((i++)) done <<< "$(get_rules_list)" if [ ${#rules_arr[@]} -eq 0 ]; then echo -e "${RED}Нет активных правил.${NC}" read -p "Нажмите Enter..." return fi echo "" read -p "Номер правила для удаления (0 — отмена): " rule_num if [[ "$rule_num" == "0" || -z "${rules_arr[$rule_num]:-}" ]]; then return; fi IFS='|' read -r d_proto d_port d_dest <<< "${rules_arr[$rule_num]}" local target_ip="${d_dest%:*}" iptables -t nat -D PREROUTING -p "$d_proto" --dport "$d_port" -j DNAT --to-destination "$d_dest" 2>/dev/null iptables -S INPUT 2>/dev/null | grep "kaskad:${d_port}:${d_proto}" | while read -r rule; do eval "iptables -D ${rule#-A }" 2>/dev/null done iptables -S FORWARD 2>/dev/null | grep "kaskad:${d_port}:${d_proto}" | while read -r rule; do eval "iptables -D ${rule#-A }" 2>/dev/null done save_iptables log_action "DELETE rule: $d_proto :$d_port -> $d_dest" echo -e "${GREEN}[OK] Правило удалено.${NC}" read -p "Нажмите Enter..." } flush_rules() { echo -e "\n${RED}!!! ВНИМАНИЕ !!!${NC}" echo "Будут удалены только правила, созданные Kaskad." read -p "Вы уверены? (y/n): " confirm if [[ "$confirm" == "y" ]]; then while iptables -t nat -S PREROUTING 2>/dev/null | grep -q "DNAT"; do local rule rule=$(iptables -t nat -S PREROUTING | grep "DNAT" | head -1) eval "iptables -t nat -D ${rule#-A }" 2>/dev/null done for chain in INPUT FORWARD; do while iptables -S "$chain" 2>/dev/null | grep -q "kaskad"; do local rule rule=$(iptables -S "$chain" | grep "kaskad" | head -1) eval "iptables -D ${rule#-A }" 2>/dev/null done done save_iptables log_action "FLUSH all kaskad rules" echo -e "${GREEN}[OK] Правила Kaskad очищены.${NC}" fi read -p "Нажмите Enter..." } manage_aliases_menu() { while true; do clear echo -e "${CYAN}━━━ Имена серверов ━━━${NC}" local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)" if [ ${#ips[@]} -eq 0 ]; then echo -e "${YELLOW}Нет целевых серверов.${NC}" read -p "Enter..." return fi for i in "${!ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} ${ips[$i]} — ${GREEN}$(get_alias "${ips[$i]}" || echo "без имени")${NC}" done echo -e " ${YELLOW}[0]${NC} Назад" read -p "Выберите сервер для переименования: " choice [[ "$choice" == "0" || -z "$choice" ]] && return local idx=$((choice - 1)) [ -z "${ips[$idx]:-}" ] && continue echo -e "Новое имя для ${ips[$idx]}:" read -p "> " new_name [ -n "$new_name" ] && set_alias "${ips[$idx]}" "$new_name" echo -e "${GREEN}[OK] Сохранено.${NC}" read -p "Enter..." done } # ─── Auto-update ────────────────────────────────────────────── self_update() { local repo_url="https://raw.githubusercontent.com/anten-ka/kaskad-pro/main/install.sh" local update_token update_token=$(bot_get_state "system" "UPDATE_TOKEN" 2>/dev/null) echo -e "${YELLOW}[*] Загрузка обновления...${NC}" local ok=0 if [ -n "$update_token" ]; then curl -sL -H "Authorization: token $update_token" "$repo_url" -o /tmp/kaskad_update.sh 2>/dev/null && ok=1 fi if [ "$ok" -eq 0 ]; then wget -qO /tmp/kaskad_update.sh "$repo_url" 2>/dev/null && ok=1 fi if [ "$ok" -eq 0 ] && [ -n "$update_token" ]; then wget -qO /tmp/kaskad_update.sh --header="Authorization: token $update_token" "$repo_url" 2>/dev/null && ok=1 fi if [ "$ok" -eq 1 ] && [ -s /tmp/kaskad_update.sh ]; then cp -f /tmp/kaskad_update.sh /usr/local/bin/gokaskad chmod +x /usr/local/bin/gokaskad rm -f /tmp/kaskad_update.sh systemctl restart kaskad-bot 2>/dev/null systemctl restart kaskad-monitor 2>/dev/null echo -e "${GREEN}[OK] Скрипт обновлён! Службы перезапущены.${NC}" echo -e "${GREEN}Перезапустите меню: gokaskad${NC}" log_action "Self-update completed" else echo -e "${RED}[ERROR] Не удалось скачать обновление.${NC}" rm -f /tmp/kaskad_update.sh fi read -p "Нажмите Enter..." } # ═══════════════════════════════════════════════════════════════ # LIVE PING # ═══════════════════════════════════════════════════════════════ ping_live() { local ip="$1" local -a results=() lines=() local count=0 lost=0 running=1 trap 'running=0' INT while [ "$running" -eq 1 ]; do local ms ms=$(ping -c 1 -W 2 "$ip" 2>/dev/null | grep -oP 'time=\K[\d.]+') ((count++)) if [ -n "$ms" ]; then results+=("$ms") lines+=("${GREEN}#${count}: ${ms} ms${NC}") else ((lost++)) lines+=("${RED}#${count}: timeout${NC}") fi clear echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${CYAN} Live Ping: ${WHITE}$(fmt_ip "$ip")${CYAN} [Ctrl+C — стоп]${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" local show=20 local start_idx=$(( ${#lines[@]} - show )) (( start_idx < 0 )) && start_idx=0 for (( i=start_idx; i<${#lines[@]}; i++ )); do echo -e " ${lines[$i]}" done if [ ${#results[@]} -gt 0 ]; then local stats stats=$(printf '%s\n' "${results[@]}" | awk ' BEGIN {mn=999999; mx=0; s=0} {s+=$1; if($1mx)mx=$1} END {printf "%.2f|%.2f|%.2f", mn, mx, s/NR} ') IFS='|' read -r s_min s_max s_avg <<< "$stats" echo "" echo -e " ${WHITE}Мин:${NC} ${s_min}ms ${WHITE}Макс:${NC} ${s_max}ms ${WHITE}Сред:${NC} ${s_avg}ms" fi echo -e " ${WHITE}Потеряно:${NC} $lost / $count" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" sleep 1 done trap - INT echo "" read -p "Нажмите Enter для возврата в меню..." } ping_menu() { echo -e "\n${CYAN}--- Ping серверов ---${NC}" local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip") done <<< "$(get_target_ips)" if [ ${#ips[@]} -eq 0 ]; then echo -e "${YELLOW}Нет активных целевых серверов.${NC}" read -p "Нажмите Enter..." return fi echo -e "Активные целевые серверы:" for i in "${!ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip "${ips[$i]}")" done echo -e " ${YELLOW}[0]${NC} Отмена" read -p "Выберите сервер: " choice [[ "$choice" == "0" || -z "$choice" ]] && return local idx=$((choice - 1)) [ -z "${ips[$idx]:-}" ] && return ping_live "${ips[$idx]}" } # ═══════════════════════════════════════════════════════════════ # MONITORING (with configurable alert cooldown + auto start/stop) # ═══════════════════════════════════════════════════════════════ add_monitor() { local ip="$1" interval="$2" threshold="$3" cooldown="${4:-300}" cat > "$MONITOR_DIR/${ip}.conf" </dev/null 2>&1; then start_monitoring_silent fi else if systemctl is-active kaskad-monitor &>/dev/null 2>&1; then stop_monitoring_silent fi fi } start_monitoring_silent() { cat > /etc/systemd/system/kaskad-monitor.service < /dev/null 2>&1 systemctl start kaskad-monitor 2>/dev/null log_action "Monitoring auto-started" } stop_monitoring_silent() { systemctl stop kaskad-monitor 2>/dev/null systemctl disable kaskad-monitor 2>/dev/null rm -f "$MONITOR_PID_FILE" log_action "Monitoring auto-stopped (no monitors)" } list_monitors() { local found=0 for conf in "$MONITOR_DIR"/*.conf; do [ -f "$conf" ] || continue found=1 local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300 source "$conf" local label label=$(fmt_ip "$MON_IP") echo -e " ${WHITE}$label${NC} инт: ${MON_INTERVAL}s порог: ${MON_THRESHOLD}ms уведомл: ${MON_COOLDOWN}s" done if [ "$found" -eq 0 ]; then echo -e " ${YELLOW}Нет настроенных мониторов.${NC}" fi } monitor_alert() { local ip="$1" ping_ms="$2" threshold="$3" cooldown="${4:-300}" local alert_file="$MONITOR_DIR/.last_alert_${ip}" local last_alert=0 [ -f "$alert_file" ] && last_alert=$(cat "$alert_file") local now now=$(date +%s) if (( now - last_alert < cooldown )); then return; fi echo "$now" > "$alert_file" log_action "ALERT: $ip ping=${ping_ms}ms threshold=${threshold}ms" source "$KASKAD_CONF" 2>/dev/null if [ -n "${BOT_TOKEN:-}" ] && [ -n "${BOT_CHAT_ID:-}" ]; then local label label=$(get_alias "$ip") local header="$ip" [ -n "$label" ] && header="$label ($ip)" local text if [ "$ping_ms" = "TIMEOUT" ]; then text="⚠️ ALERT: ${header}\nPing: TIMEOUT (порог: ${threshold}ms)" else text="⚠️ ALERT: ${header}\nPing: ${ping_ms}ms (порог: ${threshold}ms)" fi tg_send "$BOT_CHAT_ID" "$text" "" > /dev/null 2>&1 fi } monitor_daemon() { log_action "Monitor daemon started (PID $$)" echo $$ > "$MONITOR_PID_FILE" while true; do local now now=$(date +%s) for conf_file in "$MONITOR_DIR"/*.conf; do [ -f "$conf_file" ] || continue local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300 source "$conf_file" local check_file="$MONITOR_DIR/.last_check_${MON_IP}" local last_check=0 [ -f "$check_file" ] && last_check=$(cat "$check_file") if (( now - last_check >= MON_INTERVAL )); then echo "$now" > "$check_file" local ping_result ping_result=$(ping -c 1 -W 3 "$MON_IP" 2>/dev/null | grep -oP 'time=\K[\d.]+') if [ -z "$ping_result" ]; then monitor_alert "$MON_IP" "TIMEOUT" "$MON_THRESHOLD" "$MON_COOLDOWN" else local ping_int ping_int=$(awk "BEGIN {printf \"%d\", $ping_result + 0.5}") if (( ping_int > MON_THRESHOLD )); then monitor_alert "$MON_IP" "$ping_result" "$MON_THRESHOLD" "$MON_COOLDOWN" fi fi fi done sleep 1 done } monitoring_menu() { while true; do clear local mon_status="${RED}Остановлен${NC}" if systemctl is-active kaskad-monitor &>/dev/null 2>&1; then mon_status="${GREEN}Работает${NC}" fi echo -e "${CYAN}━━━ Мониторинг (авто) ━━━${NC}" echo -e "Статус: $mon_status" echo -e "${YELLOW}Служба запускается/останавливается автоматически.${NC}" echo "" list_monitors echo "" echo -e "1) Добавить мониторинг" echo -e "2) Удалить мониторинг" echo -e "0) Назад" read -p "Выбор: " choice case $choice in 1) local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)" if [ ${#ips[@]} -eq 0 ]; then echo -e "${YELLOW}Нет активных серверов.${NC}" read -p "Enter..." continue fi echo "Серверы:" for i in "${!ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip "${ips[$i]}")"; done read -p "Сервер: " s_choice local s_idx=$((s_choice - 1)) [ -z "${ips[$s_idx]:-}" ] && continue local sel_ip="${ips[$s_idx]}" echo -e "Интервал проверки:" echo -e " 1) Каждые 10 сек" echo -e " 2) Каждую минуту" echo -e " 3) Каждые 5 мин" read -p "Выбор: " int_ch local interval=60 case $int_ch in 1) interval=10 ;; 2) interval=60 ;; 3) interval=300 ;; esac read_validated_port "Порог уведомления (мс):" local threshold="$_RET_PORT" echo -e "Частота уведомлений (не чаще чем):" echo -e " 1) Каждые 10 сек" echo -e " 2) Каждые 60 сек" echo -e " 3) Каждые 5 мин" echo -e " 4) Каждые 15 мин" read -p "Выбор: " cd_ch local cooldown=300 case $cd_ch in 1) cooldown=10 ;; 2) cooldown=60 ;; 3) cooldown=300 ;; 4) cooldown=900 ;; esac add_monitor "$sel_ip" "$interval" "$threshold" "$cooldown" echo -e "${GREEN}[OK] Мониторинг для $(fmt_ip "$sel_ip") добавлен.${NC}" read -p "Enter..." ;; 2) local -a mon_ips=() for conf in "$MONITOR_DIR"/*.conf; do [ -f "$conf" ] || continue local MON_IP="" source "$conf" mon_ips+=("$MON_IP") done if [ ${#mon_ips[@]} -eq 0 ]; then echo -e "${YELLOW}Нет мониторов.${NC}" read -p "Enter..." continue fi for i in "${!mon_ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip "${mon_ips[$i]}")"; done read -p "Номер для удаления: " d_ch local d_idx=$((d_ch - 1)) [ -n "${mon_ips[$d_idx]:-}" ] && remove_monitor "${mon_ips[$d_idx]}" && echo -e "${GREEN}[OK] Удалено.${NC}" read -p "Enter..." ;; 0) return ;; esac done } # ═══════════════════════════════════════════════════════════════ # TELEGRAM BOT # ═══════════════════════════════════════════════════════════════ tg_api() { local method="$1" payload="$2" curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/${method}" \ -H "Content-Type: application/json" \ -d "$payload" 2>/dev/null } tg_send() { local chat_id="$1" text keyboard="${3:-}" text=$(printf '%b' "$2") local payload if [ -n "$keyboard" ]; then payload=$(jq -n --arg c "$chat_id" --arg t "$text" --argjson k "$keyboard" \ '{chat_id:$c, text:$t, parse_mode:"HTML", reply_markup:{inline_keyboard:$k}}') else payload=$(jq -n --arg c "$chat_id" --arg t "$text" \ '{chat_id:$c, text:$t, parse_mode:"HTML"}') fi tg_api "sendMessage" "$payload" } tg_edit() { local chat_id="$1" msg_id="$2" text keyboard="${4:-}" text=$(printf '%b' "$3") local payload if [ -n "$keyboard" ]; then payload=$(jq -n --arg c "$chat_id" --argjson m "$msg_id" --arg t "$text" --argjson k "$keyboard" \ '{chat_id:$c, message_id:$m, text:$t, parse_mode:"HTML", reply_markup:{inline_keyboard:$k}}') else payload=$(jq -n --arg c "$chat_id" --argjson m "$msg_id" --arg t "$text" \ '{chat_id:$c, message_id:$m, text:$t, parse_mode:"HTML"}') fi tg_api "editMessageText" "$payload" } tg_answer_cb() { local cb_id="$1" text="${2:-}" tg_api "answerCallbackQuery" "{\"callback_query_id\":\"$cb_id\",\"text\":\"$text\"}" } # ─── Bot state ──────────────────────────────────────────────── bot_set_state() { local chat_id="$1"; shift printf '%s\n' "$@" > "$BOT_STATE_DIR/$chat_id" } bot_get_state() { local chat_id="$1" key="$2" [ -f "$BOT_STATE_DIR/$chat_id" ] && grep "^${key}=" "$BOT_STATE_DIR/$chat_id" | head -1 | cut -d= -f2- } bot_clear_state() { rm -f "$BOT_STATE_DIR/$1" } # ─── Bot keyboards ─────────────────────────────────────────── get_menu_style() { source "$KASKAD_CONF" 2>/dev/null echo "${MENU_STYLE:-compact}" } kbd_main() { local style style=$(get_menu_style) if [ "$style" = "large" ]; then cat <<'JSON' [ [{"text":"🔀 AmneziaWG / WireGuard (UDP)","callback_data":"a_u"}], [{"text":"🔀 VLESS / XRay (TCP)","callback_data":"a_t"}], [{"text":"🔀 MTProto / TProxy (TCP)","callback_data":"a_mt"}], [{"text":"🛠 Кастомное правило (TCP/UDP)","callback_data":"a_c"}], [{"text":"📋 Активные правила","callback_data":"lr"}], [{"text":"🏓 Ping серверов","callback_data":"pm"}], [{"text":"📊 Мониторинг","callback_data":"mm"}], [{"text":"💻 Состояние системы","callback_data":"sys"}], [{"text":"❌ Удалить правило","callback_data":"dr"}], [{"text":"🗑 Сбросить все правила","callback_data":"fa"}], [{"text":"🏢 Хостинг, который работает","callback_data":"promo"}], [{"text":"⚙️ Компактное меню","callback_data":"sw_compact"}] ] JSON else cat <<'JSON' [ [{"text":"🔀 AWG","callback_data":"a_u"},{"text":"🔀 VLESS","callback_data":"a_t"},{"text":"🔀 MTProto","callback_data":"a_mt"}], [{"text":"🛠 Custom","callback_data":"a_c"},{"text":"📋 Правила","callback_data":"lr"}], [{"text":"🏓 Ping","callback_data":"pm"},{"text":"📊 Монитор","callback_data":"mm"}], [{"text":"💻 Система","callback_data":"sys"}], [{"text":"❌ Удалить","callback_data":"dr"},{"text":"🗑 Сброс","callback_data":"fa"}], [{"text":"🏢 Хостинг","callback_data":"promo"}], [{"text":"⚙️ Большое меню","callback_data":"sw_large"}] ] JSON fi } kbd_back() { echo '[[{"text":"⬅️ Меню","callback_data":"m"}]]' } kbd_proto() { echo '[[{"text":"TCP","callback_data":"a_cp_tcp"},{"text":"UDP","callback_data":"a_cp_udp"}],[{"text":"⬅️ Меню","callback_data":"m"}]]' } kbd_ping_opts() { local ip="$1" jq -n --arg ip "$ip" '[ [{"text":"1 раз","callback_data":("po:" + $ip)}], [{"text":"10 раз (среднее)","callback_data":("p10:" + $ip)}], [{"text":"60 сек (непрерывно)","callback_data":("p60:" + $ip)}], [{"text":"⬅️ Меню","callback_data":"m"}] ]' } kbd_monitor() { cat <<'JSON' [ [{"text":"➕ Добавить","callback_data":"ma"}], [{"text":"📋 Список","callback_data":"ml"}], [{"text":"➖ Удалить","callback_data":"md"}], [{"text":"⬅️ Меню","callback_data":"m"}] ] JSON } kbd_intervals() { local ip="$1" jq -n --arg ip "$ip" '[ [{"text":"10 сек","callback_data":("mi:" + $ip + ":10")}], [{"text":"1 мин","callback_data":("mi:" + $ip + ":60")}], [{"text":"5 мин","callback_data":("mi:" + $ip + ":300")}], [{"text":"⬅️ Меню","callback_data":"m"}] ]' } kbd_cooldowns() { local ip="$1" interval="$2" threshold="$3" jq -n --arg ip "$ip" --arg int "$interval" --arg thr "$threshold" '[ [{"text":"10 сек","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":10")}], [{"text":"60 сек","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":60")}], [{"text":"5 мин","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":300")}], [{"text":"15 мин","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":900")}], [{"text":"⬅️ Меню","callback_data":"m"}] ]' } build_ip_kbd() { local prefix="$1"; shift local ips=("$@") local rows="" first=1 for ip in "${ips[@]}"; do local label label=$(get_alias "$ip") [ -z "$label" ] && label="$ip" || label="$label ($ip)" [ "$first" -eq 0 ] && rows+="," rows+="[{\"text\":\"$label\",\"callback_data\":\"${prefix}:${ip}\"}]" first=0 done echo "[${rows},[{\"text\":\"⬅️ Меню\",\"callback_data\":\"m\"}]]" } build_delete_kbd() { local rules rules=$(get_rules_list) local rows="" i=1 first=1 while IFS='|' read -r proto port dest; do [ -z "$port" ] && continue local dest_ip="${dest%:*}" local label label=$(get_alias "$dest_ip") local btn_text="❌ ${MY_IP:-*}:$port ($proto) → $dest" [ -n "$label" ] && btn_text="❌ :$port → $label" [ "$first" -eq 0 ] && rows+="," rows+="[{\"text\":\"$btn_text\",\"callback_data\":\"dr_${i}\"}]" first=0 ((i++)) done <<< "$rules" if [ -z "$rows" ]; then echo '[[{"text":"Нет правил","callback_data":"m"}]]' else echo "[${rows},[{\"text\":\"⬅️ Меню\",\"callback_data\":\"m\"}]]" fi } # ─── Bot handlers ───────────────────────────────────────────── bot_main_menu() { local chat_id="$1" msg_id="${2:-}" bot_clear_state "$chat_id" local text="Kaskad PRO v${KASKAD_VERSION}\nIP: ${MY_IP:-N/A}\nВыберите действие:" local kbd kbd=$(kbd_main) if [ -n "$msg_id" ]; then tg_edit "$chat_id" "$msg_id" "$text" "$kbd" else tg_send "$chat_id" "$text" "$kbd" fi } bot_handle_callback() { local chat_id="$1" msg_id="$2" cb_id="$3" data="$4" tg_answer_cb "$cb_id" > /dev/null case "$data" in m) bot_main_menu "$chat_id" "$msg_id" ;; sw_compact) save_config_val "MENU_STYLE" "compact" bot_main_menu "$chat_id" "$msg_id" ;; sw_large) save_config_val "MENU_STYLE" "large" bot_main_menu "$chat_id" "$msg_id" ;; # ── Add rules ── a_u) bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=udp" "NAME=AmneziaWG" "CUSTOM=0" tg_edit "$chat_id" "$msg_id" "🔀 AmneziaWG / WireGuard (UDP)\n\nВведите IP адрес целевого сервера:" "$(kbd_back)" ;; a_t) bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=tcp" "NAME=VLESS" "CUSTOM=0" tg_edit "$chat_id" "$msg_id" "🔀 VLESS / XRay (TCP)\n\nВведите IP адрес целевого сервера:" "$(kbd_back)" ;; a_mt) bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=tcp" "NAME=MTProto" "CUSTOM=0" tg_edit "$chat_id" "$msg_id" "🔀 MTProto / TProxy (TCP)\n\nВведите IP адрес целевого сервера:" "$(kbd_back)" ;; a_c) tg_edit "$chat_id" "$msg_id" "🛠 Custom Rule\n\nВыберите протокол:" "$(kbd_proto)" ;; a_cp_tcp|a_cp_udp) local proto="${data#a_cp_}" bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=$proto" "NAME=Custom" "CUSTOM=1" tg_edit "$chat_id" "$msg_id" "🛠 Custom Rule ($proto)\n\nВведите IP адрес целевого сервера:" "$(kbd_back)" ;; # ── List rules ── lr) local rules text="" rules=$(get_rules_list) if [ -z "$rules" ]; then text="📋 Нет активных правил." else text="📋 Активные правила:\nСервер: ${MY_IP:-N/A}\n\n" while IFS='|' read -r proto port dest; do if [ -n "$port" ]; then local dest_ip="${dest%:*}" local label label=$(get_alias "$dest_ip") local line="${MY_IP:-*}:$port ($proto) → $dest" [ -n "$label" ] && line+=" [$label]" text+="$line\n" fi done <<< "$rules" fi tg_edit "$chat_id" "$msg_id" "$text" "$(kbd_back)" ;; # ── Delete rule ── dr) tg_edit "$chat_id" "$msg_id" "❌ Выберите правило для удаления:" "$(build_delete_kbd)" ;; dr_*) local idx="${data#dr_}" local line line=$(get_rules_list | sed -n "${idx}p") if [ -n "$line" ]; then IFS='|' read -r d_proto d_port d_dest <<< "$line" iptables -t nat -D PREROUTING -p "$d_proto" --dport "$d_port" -j DNAT --to-destination "$d_dest" 2>/dev/null iptables -S INPUT 2>/dev/null | grep "kaskad:${d_port}:${d_proto}" | while read -r rule; do eval "iptables -D ${rule#-A }" 2>/dev/null done iptables -S FORWARD 2>/dev/null | grep "kaskad:${d_port}:${d_proto}" | while read -r rule; do eval "iptables -D ${rule#-A }" 2>/dev/null done save_iptables log_action "BOT DELETE rule: $d_proto :$d_port -> $d_dest" tg_edit "$chat_id" "$msg_id" "✅ Правило $d_proto :$d_port → $d_dest удалено." "$(kbd_back)" else tg_edit "$chat_id" "$msg_id" "Правило не найдено." "$(kbd_back)" fi ;; # ── Flush ── fa) tg_edit "$chat_id" "$msg_id" "🗑 Вы уверены?\nБудут удалены ВСЕ правила Kaskad." \ '[[{"text":"✅ Да, сбросить","callback_data":"fa_y"},{"text":"❌ Отмена","callback_data":"m"}]]' ;; fa_y) while iptables -t nat -S PREROUTING 2>/dev/null | grep -q "DNAT"; do local rule rule=$(iptables -t nat -S PREROUTING | grep "DNAT" | head -1) eval "iptables -t nat -D ${rule#-A }" 2>/dev/null done for chain in INPUT FORWARD; do while iptables -S "$chain" 2>/dev/null | grep -q "kaskad"; do local rule rule=$(iptables -S "$chain" | grep "kaskad" | head -1) eval "iptables -D ${rule#-A }" 2>/dev/null done done save_iptables log_action "BOT FLUSH all kaskad rules" tg_edit "$chat_id" "$msg_id" "✅ Все правила Kaskad удалены." "$(kbd_back)" ;; # ── System stats ── sys) local stats stats=$(get_system_stats) tg_edit "$chat_id" "$msg_id" "$stats" "$(kbd_back)" ;; # ── Promo ── promo) local promo_text="" promo_text+="🏢 Хостинг, который работает\n\n" promo_text+="🌍 Локации: РФ и Европа\n" promo_text+="👉 https://vk.cc/ct29NQ\n\n" promo_text+="OFF60 — 60% скидка на 1-й месяц\n" promo_text+="antenka20 — +20% к балансу (3 мес)\n" promo_text+="antenka6 — +15% к балансу (6 мес)\n" promo_text+="antenka12 — +5% к балансу (12 мес)\n\n" promo_text+="🇧🇾 Локация: Беларусь\n" promo_text+="👉 https://vk.cc/cUxAhj\n\n" promo_text+="OFF60 — 60% скидка на 1-й месяц" tg_edit "$chat_id" "$msg_id" "$promo_text" "$(kbd_back)" ;; # ── Ping ── pm) local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)" if [ ${#ips[@]} -eq 0 ]; then tg_edit "$chat_id" "$msg_id" "🏓 Нет целевых серверов для пинга." "$(kbd_back)" else tg_edit "$chat_id" "$msg_id" "🏓 Выберите сервер:" "$(build_ip_kbd "ps" "${ips[@]}")" fi ;; ps:*) local ip="${data#ps:}" local label label=$(fmt_ip "$ip") tg_edit "$chat_id" "$msg_id" "🏓 Ping $label\nВыберите режим:" "$(kbd_ping_opts "$ip")" ;; po:*) local ip="${data#po:}" local label label=$(fmt_ip "$ip") ( local ms ms=$(ping -c 1 -W 3 "$ip" 2>/dev/null | grep -oP 'time=\K[\d.]+') if [ -n "$ms" ]; then tg_send "$chat_id" "🏓 $label\nОтвет: ${ms} ms" "$(kbd_back)" > /dev/null else tg_send "$chat_id" "🏓 $label\nОтвет: timeout" "$(kbd_back)" > /dev/null fi ) & ;; p10:*) local ip="${data#p10:}" local label label=$(fmt_ip "$ip") ( local resp resp=$(tg_send "$chat_id" "🏓 Ping $label (10 раз)...\nОжидайте." "") local mid mid=$(echo "$resp" | jq -r '.result.message_id // empty') local -a results=() local lost=0 text="" for i in $(seq 1 10); do local ms ms=$(ping -c 1 -W 2 "$ip" 2>/dev/null | grep -oP 'time=\K[\d.]+') if [ -n "$ms" ]; then results+=("$ms") text+="#$i: ${ms}ms\n" else ((lost++)) text+="#$i: timeout\n" fi sleep 1 done local summary="🏓 Ping $label (10 раз)\n${text}" if [ ${#results[@]} -gt 0 ]; then local avg avg=$(printf '%s\n' "${results[@]}" | awk '{s+=$1} END {printf "%.2f", s/NR}') summary+="\nСреднее: ${avg}ms" fi summary+="\nПотеряно: $lost / 10" if [ -n "$mid" ]; then tg_edit "$chat_id" "$mid" "$summary" "$(kbd_back)" > /dev/null else tg_send "$chat_id" "$summary" "$(kbd_back)" > /dev/null fi ) & ;; p60:*) local ip="${data#p60:}" local label label=$(fmt_ip "$ip") ( local resp resp=$(tg_send "$chat_id" "🏓 Ping $label (60 сек)...\nОбновление каждые 10 сек." "") local mid mid=$(echo "$resp" | jq -r '.result.message_id // empty') local -a results=() local lost=0 for i in $(seq 1 60); do local ms ms=$(ping -c 1 -W 2 "$ip" 2>/dev/null | grep -oP 'time=\K[\d.]+') if [ -n "$ms" ]; then results+=("$ms"); else ((lost++)); fi if (( i % 10 == 0 )) && [ -n "$mid" ]; then local partial="🏓 $label: ${i}/60 сек\nУспешно: ${#results[@]} | Потеряно: $lost" if [ ${#results[@]} -gt 0 ]; then local pavg pavg=$(printf '%s\n' "${results[@]}" | awk '{s+=$1} END {printf "%.2f", s/NR}') partial+="\nСреднее: ${pavg}ms" fi tg_edit "$chat_id" "$mid" "$partial" "" > /dev/null fi sleep 1 done local summary="🏓 $label (60 сек) — завершён\n" if [ ${#results[@]} -gt 0 ]; then local stats stats=$(printf '%s\n' "${results[@]}" | awk ' BEGIN{mn=999999;mx=0;s=0} {s+=$1;if($1mx)mx=$1} END{printf "%.2f|%.2f|%.2f",mn,mx,s/NR}') IFS='|' read -r s_min s_max s_avg <<< "$stats" summary+="Мин: ${s_min}ms\nМакс: ${s_max}ms\nСреднее: ${s_avg}ms\n" fi summary+="Потеряно: $lost / 60" if [ -n "$mid" ]; then tg_edit "$chat_id" "$mid" "$summary" "$(kbd_back)" > /dev/null else tg_send "$chat_id" "$summary" "$(kbd_back)" > /dev/null fi ) & ;; # ── Monitoring ── mm) tg_edit "$chat_id" "$msg_id" "📊 Мониторинг\nВыберите действие:" "$(kbd_monitor)" ;; ma) local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)" if [ ${#ips[@]} -eq 0 ]; then tg_edit "$chat_id" "$msg_id" "Нет целевых серверов." "$(kbd_back)" else tg_edit "$chat_id" "$msg_id" "📊 Выберите сервер для мониторинга:" "$(build_ip_kbd "ma" "${ips[@]}")" fi ;; ma:*) local ip="${data#ma:}" local label label=$(fmt_ip "$ip") tg_edit "$chat_id" "$msg_id" "📊 Мониторинг $label\nВыберите интервал проверки:" "$(kbd_intervals "$ip")" ;; mi:*) local rest="${data#mi:}" local ip="${rest%:*}" local interval="${rest##*:}" bot_set_state "$chat_id" "STATE=awaiting_threshold" "MON_IP=$ip" "MON_INTERVAL=$interval" local label label=$(fmt_ip "$ip") tg_edit "$chat_id" "$msg_id" "📊 $label (каждые ${interval}с)\n\nВведите порог уведомления (мс):" "$(kbd_back)" ;; mc:*) local rest="${data#mc:}" IFS=':' read -r ip interval threshold cooldown <<< "$rest" add_monitor "$ip" "$interval" "$threshold" "$cooldown" local label label=$(fmt_ip "$ip") tg_edit "$chat_id" "$msg_id" "✅ Мониторинг для $label добавлен.\nИнтервал: ${interval}с | Порог: ${threshold}мс | Уведомл: ${cooldown}с" "$(kbd_back)" ;; ml) local text="📊 Активные мониторы:\n" local found=0 for conf in "$MONITOR_DIR"/*.conf; do [ -f "$conf" ] || continue found=1 local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300 source "$conf" local label label=$(fmt_ip "$MON_IP") text+="$label\n инт: ${MON_INTERVAL}с | порог: ${MON_THRESHOLD}мс | уведомл: ${MON_COOLDOWN}с\n" done [ "$found" -eq 0 ] && text+="Нет настроенных мониторов." tg_edit "$chat_id" "$msg_id" "$text" "$(kbd_monitor)" ;; md) local -a mon_ips=() for conf in "$MONITOR_DIR"/*.conf; do [ -f "$conf" ] || continue local MON_IP="" source "$conf" mon_ips+=("$MON_IP") done if [ ${#mon_ips[@]} -eq 0 ]; then tg_edit "$chat_id" "$msg_id" "Нет мониторов для удаления." "$(kbd_monitor)" else tg_edit "$chat_id" "$msg_id" "📊 Выберите монитор для удаления:" "$(build_ip_kbd "md" "${mon_ips[@]}")" fi ;; md:*) local ip="${data#md:}" remove_monitor "$ip" local label label=$(fmt_ip "$ip") tg_edit "$chat_id" "$msg_id" "✅ Мониторинг для $label удалён." "$(kbd_monitor)" ;; esac } bot_handle_message() { local chat_id="$1" text="$2" if [ "$text" = "/start" ] || [ "$text" = "/menu" ]; then bot_main_menu "$chat_id" return fi local state state=$(bot_get_state "$chat_id" "STATE") case "$state" in awaiting_ip) if ! validate_ip "$text"; then tg_send "$chat_id" "❌ Некорректный IP-адрес. Попробуйте ещё раз:" "$(kbd_back)" > /dev/null return fi local proto name custom proto=$(bot_get_state "$chat_id" "PROTO") name=$(bot_get_state "$chat_id" "NAME") custom=$(bot_get_state "$chat_id" "CUSTOM") bot_set_state "$chat_id" "STATE=awaiting_name" "PROTO=$proto" "NAME=$name" "CUSTOM=$custom" "TARGET_IP=$text" tg_send "$chat_id" "IP: $text ✅\n\nВведите имя сервера (или - чтобы пропустить):" "$(kbd_back)" > /dev/null ;; awaiting_name) local proto name custom target_ip proto=$(bot_get_state "$chat_id" "PROTO") name=$(bot_get_state "$chat_id" "NAME") custom=$(bot_get_state "$chat_id" "CUSTOM") target_ip=$(bot_get_state "$chat_id" "TARGET_IP") if [ "$text" != "-" ] && [ -n "$text" ]; then set_alias "$target_ip" "$text" fi if [ "$custom" = "1" ]; then bot_set_state "$chat_id" "STATE=awaiting_in_port" "PROTO=$proto" "NAME=$name" "CUSTOM=1" "TARGET_IP=$target_ip" tg_send "$chat_id" "Сервер: $(fmt_ip "$target_ip")\n\nВведите ВХОДЯЩИЙ порт:" "$(kbd_back)" > /dev/null else bot_set_state "$chat_id" "STATE=awaiting_port" "PROTO=$proto" "NAME=$name" "CUSTOM=0" "TARGET_IP=$target_ip" tg_send "$chat_id" "Сервер: $(fmt_ip "$target_ip")\n\nВведите порт:" "$(kbd_back)" > /dev/null fi ;; awaiting_port) if ! validate_port "$text"; then tg_send "$chat_id" "❌ Некорректный порт (1-65535)." "" > /dev/null return fi local proto name target_ip proto=$(bot_get_state "$chat_id" "PROTO") name=$(bot_get_state "$chat_id" "NAME") target_ip=$(bot_get_state "$chat_id" "TARGET_IP") bot_clear_state "$chat_id" apply_iptables_rules "$proto" "$text" "$text" "$target_ip" "$name" local label label=$(fmt_ip "$target_ip") tg_send "$chat_id" "✅ $name настроен!\n$proto ${MY_IP:-*}:$text → $target_ip:$text\nСервер: $label" "$(kbd_back)" > /dev/null ;; awaiting_in_port) if ! validate_port "$text"; then tg_send "$chat_id" "❌ Некорректный порт (1-65535)." "" > /dev/null return fi local proto name target_ip proto=$(bot_get_state "$chat_id" "PROTO") name=$(bot_get_state "$chat_id" "NAME") target_ip=$(bot_get_state "$chat_id" "TARGET_IP") bot_set_state "$chat_id" "STATE=awaiting_out_port" "PROTO=$proto" "NAME=$name" "CUSTOM=1" "TARGET_IP=$target_ip" "IN_PORT=$text" tg_send "$chat_id" "Входящий порт: $text ✅\n\nВведите ИСХОДЯЩИЙ порт:" "$(kbd_back)" > /dev/null ;; awaiting_out_port) if ! validate_port "$text"; then tg_send "$chat_id" "❌ Некорректный порт (1-65535)." "" > /dev/null return fi local proto name target_ip in_port proto=$(bot_get_state "$chat_id" "PROTO") name=$(bot_get_state "$chat_id" "NAME") target_ip=$(bot_get_state "$chat_id" "TARGET_IP") in_port=$(bot_get_state "$chat_id" "IN_PORT") bot_clear_state "$chat_id" apply_iptables_rules "$proto" "$in_port" "$text" "$target_ip" "$name" local label label=$(fmt_ip "$target_ip") tg_send "$chat_id" "✅ Custom Rule настроен!\n$proto ${MY_IP:-*}:$in_port → $target_ip:$text\nСервер: $label" "$(kbd_back)" > /dev/null ;; awaiting_threshold) if ! validate_port "$text"; then tg_send "$chat_id" "❌ Введите число от 1 до 65535 (мс):" "" > /dev/null return fi local mon_ip mon_interval mon_ip=$(bot_get_state "$chat_id" "MON_IP") mon_interval=$(bot_get_state "$chat_id" "MON_INTERVAL") bot_clear_state "$chat_id" local label label=$(fmt_ip "$mon_ip") tg_send "$chat_id" "📊 $label\nИнтервал: ${mon_interval}с | Порог: ${text}мс\n\nВыберите частоту уведомлений:" "$(kbd_cooldowns "$mon_ip" "$mon_interval" "$text")" > /dev/null ;; *) tg_send "$chat_id" "Используйте /start или /menu для вызова меню." "" > /dev/null ;; esac } # ─── Bot daemon ─────────────────────────────────────────────── bot_daemon() { log_action "Bot daemon started (PID $$)" echo $$ > "$BOT_PID_FILE" source "$KASKAD_CONF" if [ -z "$BOT_TOKEN" ]; then log_action "BOT ERROR: BOT_TOKEN is empty" exit 1 fi detect_interface get_my_ip local offset=0 while true; do local response response=$(curl -s --max-time 35 \ "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?offset=${offset}&timeout=30" 2>/dev/null) if [ -z "$response" ]; then sleep 2; continue; fi local ok ok=$(echo "$response" | jq -r '.ok // "false"') if [ "$ok" != "true" ]; then sleep 5; continue; fi local count count=$(echo "$response" | jq '.result | length') for (( i=0; i /dev/null continue fi bot_handle_callback "$cb_chat_id" "$cb_msg_id" "$cb_id" "$cb_data" else local msg_chat_id msg_text msg_chat_id=$(echo "$update" | jq -r '.message.chat.id // empty') msg_text=$(echo "$update" | jq -r '.message.text // empty') if [ -n "$msg_chat_id" ] && [ -n "$msg_text" ]; then if [ -n "$BOT_CHAT_ID" ] && [ "$msg_chat_id" != "$BOT_CHAT_ID" ]; then tg_send "$msg_chat_id" "⛔ Нет доступа.\nВаш Chat ID: $msg_chat_id" "" > /dev/null continue fi bot_handle_message "$msg_chat_id" "$msg_text" fi fi done done } start_bot() { source "$KASKAD_CONF" if [ -z "$BOT_TOKEN" ]; then echo -e "${RED}Сначала задайте BOT_TOKEN!${NC}"; return; fi if [ -f "$BOT_PID_FILE" ] && kill -0 "$(cat "$BOT_PID_FILE")" 2>/dev/null; then echo -e "${YELLOW}Бот уже запущен.${NC}"; return fi cat > /etc/systemd/system/kaskad-bot.service < /dev/null 2>&1 systemctl start kaskad-bot sleep 1 if systemctl is-active kaskad-bot &>/dev/null; then echo -e "${GREEN}[OK] Бот запущен.${NC}" log_action "Bot service started" else echo -e "${RED}[ERROR] Проверьте: journalctl -u kaskad-bot${NC}" fi } stop_bot() { systemctl stop kaskad-bot 2>/dev/null systemctl disable kaskad-bot 2>/dev/null rm -f "$BOT_PID_FILE" echo -e "${GREEN}[OK] Бот остановлен.${NC}" log_action "Bot service stopped" } bot_menu() { while true; do clear source "$KASKAD_CONF" 2>/dev/null local bot_status="${RED}Остановлен${NC}" if [ -f "$BOT_PID_FILE" ] && kill -0 "$(cat "$BOT_PID_FILE" 2>/dev/null)" 2>/dev/null; then bot_status="${GREEN}Работает (PID $(cat "$BOT_PID_FILE"))${NC}" fi local token_display="не задан" [ -n "${BOT_TOKEN:-}" ] && token_display="***${BOT_TOKEN: -6}" local ut_val ut_val=$(bot_get_state "system" "UPDATE_TOKEN" 2>/dev/null) local ut_display="не задан" [ -n "$ut_val" ] && ut_display="***${ut_val: -6}" echo -e "${CYAN}━━━ Telegram Bot ━━━${NC}" echo -e "Статус: $bot_status" echo -e "Токен: ${YELLOW}$token_display${NC}" echo -e "Chat ID: ${YELLOW}${BOT_CHAT_ID:-не задан}${NC}" echo -e "Update Token: ${YELLOW}$ut_display${NC}" echo -e "Меню: ${YELLOW}${MENU_STYLE:-compact}${NC}" echo "" echo -e "1) Установить токен бота" echo -e "2) Получить Chat ID (авто)" echo -e "3) Установить Chat ID вручную" echo -e "4) ${GREEN}Запустить бота${NC}" echo -e "5) ${RED}Остановить бота${NC}" echo -e "6) Токен обновления (GitHub PAT)" echo -e "0) Назад" read -p "Выбор: " choice case $choice in 1) echo "Введите токен (от @BotFather):"; read -p "> " t; [ -n "$t" ] && save_config_val "BOT_TOKEN" "$t" && echo -e "${GREEN}OK${NC}"; read -p "Enter..." ;; 2) [ -z "${BOT_TOKEN:-}" ] && echo -e "${RED}Сначала токен!${NC}" && read -p "Enter..." && continue echo -e "${YELLOW}Отправьте боту сообщение в Telegram, затем Enter.${NC}"; read -p "" local cid; cid=$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?limit=1&offset=-1" | jq -r '.result[0].message.chat.id // empty') [ -n "$cid" ] && save_config_val "BOT_CHAT_ID" "$cid" && echo -e "${GREEN}Chat ID: $cid${NC}" || echo -e "${RED}Не удалось.${NC}" read -p "Enter..." ;; 3) echo "Chat ID:"; read -p "> " c; [ -n "$c" ] && save_config_val "BOT_CHAT_ID" "$c" && echo -e "${GREEN}OK${NC}"; read -p "Enter..." ;; 4) start_bot; read -p "Enter..." ;; 5) stop_bot; read -p "Enter..." ;; 6) echo "GitHub PAT:"; read -p "> " u; [ -n "$u" ] && bot_set_state "system" "UPDATE_TOKEN=$u" && echo -e "${GREEN}OK${NC}"; read -p "Enter..." ;; 0) return ;; esac done } # ═══════════════════════════════════════════════════════════════ # PROMO & INSTRUCTIONS (kept from previous version) # ═══════════════════════════════════════════════════════════════ show_promo() { clear echo "" echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${MAGENTA}║ ХОСТИНГ, КОТОРЫЙ РАБОТАЕТ СО СКИДКОЙ ДО -60% ║${NC}" echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${CYAN}🌍 ЛОКАЦИИ: РФ И ЕВРОПА${NC}" echo -e "${WHITE} >>> https://vk.cc/ct29NQ${NC}" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "OFF60" "60% скидка на первый месяц" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "antenka20" "Буст 20% + 3% (при оплате за 3 мес)" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "antenka6" "Буст 15% + 5% (при оплате за 6 мес)" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "antenka12" "Буст 5% + 5% (при оплате за 12 мес)" echo -e "\n${CYAN}🇧🇾 ЛОКАЦИЯ: БЕЛАРУСЬ${NC}" echo -e "${WHITE} >>> https://vk.cc/cUxAhj${NC}" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "OFF60" "60% скидка на первый месяц" echo "" echo -e "\n${YELLOW}Генерация QR-кода... (3 сек)${NC}" for i in {3..1}; do echo -ne "$i..."; sleep 1; done; echo "" echo -e "\n${WHITE}" command -v qrencode &>/dev/null && qrencode -t ANSIUTF8 "https://vk.cc/ct29NQ" || echo "Используйте ссылки выше." echo -e "${NC}" echo -e "${GREEN}Сканируйте камерой телефона!${NC}" echo "" read -p "Нажмите Enter..." } show_instructions() { local page=1 total_pages=7 while true; do clear echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${MAGENTA}║ 📚 ИНСТРУКЦИЯ KASKAD PRO v${KASKAD_VERSION} (стр. ${page}/${total_pages}) ║${NC}" echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" case $page in 1) echo -e "${CYAN}═══ ЧТО ТАКОЕ КАСКАД? ═══${NC}\n\nКаскад — 'мост' между устройством и зарубежным VPN/Proxy.\n\n ${WHITE}Клиент${NC} → ${GREEN}Этот сервер (РФ)${NC} → ${CYAN}Зарубежный VPN${NC} → Интернет\n\nПровайдер видит только подключение к серверу в РФ.\n\n${CYAN}═══ ЧТО НУЖНО ═══${NC}\n 1. VPS в РФ\n 2. Зарубежный VPN/Proxy\n 3. IP и порт зарубежного сервера" ;; 2) echo -e "${CYAN}═══ ПУНКТ 1: AWG/WG (UDP) ═══${NC}\n\n Меню → 1 → IP: 45.10.20.30 → Порт: 51820\n В клиенте Endpoint: ${MY_IP:-185.1.2.3}:51820\n\n${CYAN}═══ ПУНКТ 2: VLESS (TCP) ═══${NC}\n\n Меню → 2 → IP: 67.89.100.200 → Порт: 443\n В v2rayNG/NekoBox замените адрес сервера" ;; 3) echo -e "${CYAN}═══ ПУНКТ 3: MTProto ═══${NC}\n\n Меню → 3 → IP → Порт: 8443\n Telegram → Прокси → ${MY_IP:-*}:8443\n\n${CYAN}═══ ПУНКТ 4: Custom ═══${NC}\n\n Разные порты входа/выхода, SSH, RDP\n Пример SSH: tcp, IP, вход: 2222, выход: 22\n Подключение: ssh user@${MY_IP:-*} -p 2222" ;; 4) echo -e "${CYAN}═══ ПРАВИЛА И PING ═══${NC}\n\nПункт 5 — таблица правил с IP каскада и именами серверов\nПункт 6 — Live Ping (1 сек обновление, Ctrl+C стоп)\nПункт 14 — управление именами серверов" ;; 5) echo -e "${CYAN}═══ МОНИТОРИНГ ═══${NC}\n\nАвтопроверка серверов с Telegram-алертами.\n\n Меню → 7 → Добавить → сервер → интервал → порог → частота уведомл.\n\nСлужба запускается/останавливается автоматически.\nЧастота уведомлений: 10с / 60с / 5мин / 15мин" ;; 6) echo -e "${CYAN}═══ TELEGRAM BOT ═══${NC}\n\n Шаг A. @BotFather → /newbot → получить токен\n Шаг B. Меню → 8 → 1 → вставить токен\n Шаг C. Меню → 8 → 2 → отправить боту сообщение → Enter\n Шаг D. Меню → 8 → 4 → Запустить\n Шаг E. В Telegram: /start\n\nБот имеет 2 стиля меню: компактный и большой (кнопка внизу)" ;; 7) echo -e "${CYAN}═══ ВОЗМОЖНОСТИ БОТА ═══${NC}\n\n Добавление/удаление правил через кнопки\n Ping: 1x / 10x (среднее) / 60 сек\n Мониторинг: добавить/удалить/список\n 💻 Система: CPU, RAM, Swap, диск, топ процессов\n 🏢 Хостинг: промокоды партнёров\n Имена серверов отображаются везде\n\n Пункты 9-13: удаление, сброс, обновление, промо, инструкция" ;; esac echo -e "\n${MAGENTA}──────────────────────────────────────────────────────────────${NC}" if [ "$page" -eq 1 ]; then echo -e " ${YELLOW}[N]${NC} Далее ${YELLOW}[0]${NC} Выход" elif [ "$page" -eq "$total_pages" ]; then echo -e " ${YELLOW}[P]${NC} Назад ${YELLOW}[0]${NC} Выход" else echo -e " ${YELLOW}[P]${NC} Назад ${YELLOW}[N]${NC} Далее ${YELLOW}[0]${NC} Выход"; fi read -p " > " nav case "$nav" in [nN]) (( page < total_pages )) && ((page++)) ;; [pP]) (( page > 1 )) && ((page--)) ;; 0) return ;; [1-7]) page="$nav" ;; esac done } # ═══════════════════════════════════════════════════════════════ # MAIN MENU # ═══════════════════════════════════════════════════════════════ show_menu() { while true; do clear echo -e "${MAGENTA}" echo "******************************************************" echo " anten-ka канал представляет... Kaskad PRO v${KASKAD_VERSION}" echo " YouTube: https://www.youtube.com/@antenkaru" echo "******************************************************" echo -e "${NC}" echo -e "${WHITE}IP сервера: ${GREEN}${MY_IP}${NC} ${WHITE}Интерфейс: ${CYAN}${IFACE}${NC}" echo -e "${YELLOW}Инструкции:${NC} ${BLUE}https://boosty.to/anten-ka${NC}" echo -e "${GREEN}Донат:${NC} https://pay.cloudtips.ru/p/7410814f" echo -e "------------------------------------------------------" echo -e " 1) Настроить ${CYAN}AmneziaWG / WireGuard${NC} (UDP)" echo -e " 2) Настроить ${CYAN}VLESS / XRay${NC} (TCP)" echo -e " 3) Настроить ${CYAN}TProxy / MTProto${NC} (TCP)" echo -e " 4) 🛠 Создать ${YELLOW}Кастомное правило${NC}" echo -e " 5) 📋 Активные правила" echo -e " 6) 🏓 ${CYAN}Ping сервера (live)${NC}" echo -e " 7) 📊 ${CYAN}Мониторинг${NC}" echo -e " 8) 🤖 ${CYAN}Telegram Bot${NC}" echo -e " 9) ${RED}Удалить одно правило${NC}" echo -e "10) ${RED}Сбросить правила Kaskad${NC}" echo -e "11) ${YELLOW}Обновить скрипт${NC}" echo -e "12) ${YELLOW}PROMO${NC}" echo -e "13) ${MAGENTA}📚 Инструкция${NC}" echo -e "14) ${WHITE}Имена серверов${NC}" echo -e " 0) Выход" echo -e "------------------------------------------------------" read -p "Ваш выбор: " choice case $choice in 1) configure_rule "udp" "AmneziaWG" ;; 2) configure_rule "tcp" "VLESS" ;; 3) configure_rule "tcp" "MTProto/TProxy" ;; 4) configure_custom_rule ;; 5) list_active_rules ;; 6) ping_menu ;; 7) monitoring_menu ;; 8) bot_menu ;; 9) delete_single_rule ;; 10) flush_rules ;; 11) self_update ;; 12) show_promo ;; 13) show_instructions ;; 14) manage_aliases_menu ;; 0) exit 0 ;; *) ;; esac done } # ═══════════════════════════════════════════════════════════════ # ENTRY POINT # ═══════════════════════════════════════════════════════════════ case "${1:-}" in --bot-daemon) init_config bot_daemon ;; --monitor-daemon) init_config monitor_daemon ;; *) check_root init_config prepare_system detect_interface get_my_ip show_promo show_menu ;; esac