#!/bin/bash set -o pipefail # ══════════════════════════════════════════════════════════════ # KASKAD PRO v2.3 — Cascading VPN / Proxy Manager # Telegram Bot · Live Ping · Monitoring · Alerts · GeoIP · System Stats # Channel: https://www.youtube.com/@antenkaru # ══════════════════════════════════════════════════════════════ KASKAD_VERSION="2.3" 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="inline" 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: IP=name|note|country|isp ──────────────────────── set_alias_full() { local ip="$1" name="$2" note="${3:-}" country="${4:-}" isp="${5:-}" local val="${name}|${note}|${country}|${isp}" if grep -q "^${ip}=" "$ALIASES_FILE" 2>/dev/null; then sed -i "s|^${ip}=.*|${ip}=${val}|" "$ALIASES_FILE" else echo "${ip}=${val}" >> "$ALIASES_FILE" fi } set_alias() { local ip="$1" name="$2" local existing existing=$(grep "^${ip}=" "$ALIASES_FILE" 2>/dev/null | head -1 | cut -d= -f2-) local old_note old_country old_isp IFS='|' read -r _ old_note old_country old_isp <<< "$existing" set_alias_full "$ip" "$name" "${old_note:-}" "${old_country:-}" "${old_isp:-}" } set_alias_note() { local ip="$1" note="$2" local existing existing=$(grep "^${ip}=" "$ALIASES_FILE" 2>/dev/null | head -1 | cut -d= -f2-) local old_name old_note old_country old_isp IFS='|' read -r old_name old_note old_country old_isp <<< "$existing" set_alias_full "$ip" "${old_name:-}" "$note" "${old_country:-}" "${old_isp:-}" } set_alias_geo() { local ip="$1" country="$2" isp="$3" local existing existing=$(grep "^${ip}=" "$ALIASES_FILE" 2>/dev/null | head -1 | cut -d= -f2-) local old_name old_note old_country old_isp IFS='|' read -r old_name old_note old_country old_isp <<< "$existing" set_alias_full "$ip" "${old_name:-}" "${old_note:-}" "$country" "$isp" } get_alias_field() { local ip="$1" field="$2" local raw raw=$(grep "^${ip}=" "$ALIASES_FILE" 2>/dev/null | head -1 | cut -d= -f2-) local f_name f_note f_country f_isp IFS='|' read -r f_name f_note f_country f_isp <<< "$raw" case "$field" in name) echo "$f_name" ;; note) echo "$f_note" ;; country) echo "$f_country" ;; isp) echo "$f_isp" ;; *) echo "$f_name" ;; esac } get_alias() { get_alias_field "$1" "name"; } fmt_ip() { local ip="$1" local name country isp name=$(get_alias_field "$ip" "name") country=$(get_alias_field "$ip" "country") isp=$(get_alias_field "$ip" "isp") local result="" [ -n "$name" ] && result="${name} " || result="" result+="($ip)" if [ -n "$country" ] || [ -n "$isp" ]; then result+=" " [ -n "$country" ] && result+="$country" [ -n "$isp" ] && result+=" | $isp" fi echo "$result" } fmt_ip_short() { local ip="$1" local name name=$(get_alias_field "$ip" "name") [ -n "$name" ] && echo "$name ($ip)" || echo "$ip" } fmt_ip_tg() { local ip="$1" local name note country isp name=$(get_alias_field "$ip" "name") note=$(get_alias_field "$ip" "note") country=$(get_alias_field "$ip" "country") isp=$(get_alias_field "$ip" "isp") local result="" [ -n "$name" ] && result="$name " || result="" result+="$ip" if [ -n "$country" ] || [ -n "$isp" ]; then result+=" " [ -n "$country" ] && result+="$country" [ -n "$isp" ] && result+=" | $isp" fi [ -n "$note" ] && result+="\n $note" echo "$result" } # ─── 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 } # ─── GeoIP + Probe ──────────────────────────────────────────── geoip_lookup() { local ip="$1" curl -s --max-time 5 "http://ip-api.com/json/${ip}?fields=status,country,regionName,city,isp,org" 2>/dev/null } probe_server_cli() { local ip="$1" port="${2:-}" echo -e "\n${CYAN}━━━ Проверка сервера $ip ━━━${NC}" echo -e "${YELLOW}[*] GeoIP...${NC}" local geo geo=$(geoip_lookup "$ip") local geo_status geo_country geo_region geo_city geo_isp geo_org geo_status=$(echo "$geo" | jq -r '.status // "fail"') if [ "$geo_status" = "success" ]; then geo_country=$(echo "$geo" | jq -r '.country // ""') geo_region=$(echo "$geo" | jq -r '.regionName // ""') geo_city=$(echo "$geo" | jq -r '.city // ""') geo_isp=$(echo "$geo" | jq -r '.isp // ""') geo_org=$(echo "$geo" | jq -r '.org // ""') local geo_loc="${geo_country}" [ -n "$geo_city" ] && geo_loc+=", ${geo_city}" local geo_provider="$geo_isp" [ -n "$geo_org" ] && [ "$geo_org" != "$geo_isp" ] && geo_provider+=" ($geo_org)" echo -e " ${WHITE}GeoIP:${NC} ${GREEN}${geo_loc}${NC} | ${CYAN}${geo_provider}${NC}" set_alias_geo "$ip" "$geo_country" "$geo_isp" else echo -e " ${RED}GeoIP: не удалось определить${NC}" fi echo -e "${YELLOW}[*] Ping (3x)...${NC}" local -a pings=() local plost=0 for n in 1 2 3; do local raw method="" ms="" raw=$(smart_ping "$ip" 3 "$port") if [ -n "$raw" ]; then method="${raw%%|*}"; ms="${raw#*|}" pings+=("$ms") echo -e " #$n: ${GREEN}${ms}ms${NC} ${CYAN}[$method]${NC}" else ((plost++)) echo -e " #$n: ${RED}timeout${NC} ${WHITE}[ICMP$([ -n "$port" ] && echo "+TCP:$port")]${NC}" fi [ "$n" -lt 3 ] && sleep 1 done if [ ${#pings[@]} -gt 0 ]; then local pavg pavg=$(printf '%s\n' "${pings[@]}" | awk '{s+=$1} END {printf "%.2f", s/NR}') echo -e " ${WHITE}Среднее: ${pavg}ms${NC} Потеряно: $plost/3" else echo -e " ${RED}Сервер не отвечает${NC}" fi echo "" local existing_name existing_name=$(get_alias "$ip") if [ -n "$existing_name" ]; then echo -e "Текущее имя: ${GREEN}$existing_name${NC}" fi echo -e "Введите имя сервера (или Enter — пропустить):" read -p "> " _RET_NAME [ -n "$_RET_NAME" ] && set_alias "$ip" "$_RET_NAME" echo -e "Введите примечание (или Enter — пропустить):" read -p "> " _RET_NOTE [ -n "$_RET_NOTE" ] && set_alias_note "$ip" "$_RET_NOTE" if [ ${#pings[@]} -eq 0 ]; then echo -e "\n${YELLOW}━━━ Сервер не ответил ━━━${NC}" echo -e "${WHITE}ICMP заблокирован$([ -n "$port" ] && echo " и TCP:$port не удался").${NC}" echo "" echo -e "${CYAN}Чтобы включить ping на удалённом сервере:${NC}" echo -e " ${WHITE}ssh root@${ip}${NC}" echo -e " ${GREEN}sysctl -w net.ipv4.icmp_echo_ignore_all=0${NC}" echo -e " ${GREEN}echo 'net.ipv4.icmp_echo_ignore_all=0' >> /etc/sysctl.conf${NC}" echo -e " ${GREEN}iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT${NC}" echo "" read -p "Продолжить добавление? (y/n): " ans [[ "$ans" != "y" ]] && return 1 fi return 0 } probe_server_tg() { local ip="$1" port="${2:-}" local result="" local geo geo=$(geoip_lookup "$ip") local geo_status geo_status=$(echo "$geo" | jq -r '.status // "fail"') if [ "$geo_status" = "success" ]; then local geo_country geo_city geo_isp geo_org geo_country=$(echo "$geo" | jq -r '.country // ""') geo_city=$(echo "$geo" | jq -r '.city // ""') geo_isp=$(echo "$geo" | jq -r '.isp // ""') geo_org=$(echo "$geo" | jq -r '.org // ""') local geo_loc="$geo_country" [ -n "$geo_city" ] && geo_loc+=", $geo_city" result+="🌍 GeoIP: $geo_loc | $geo_isp\n" set_alias_geo "$ip" "$geo_country" "$geo_isp" fi local -a pings=() local plost=0 for n in 1 2 3; do local raw method="" ms="" raw=$(smart_ping "$ip" 3 "$port") if [ -n "$raw" ]; then method="${raw%%|*}"; ms="${raw#*|}" pings+=("$ms") result+=" #$n: ${ms}ms [$method]\n" else ((plost++)) result+=" #$n: timeout\n" fi [ "$n" -lt 3 ] && sleep 1 done if [ ${#pings[@]} -gt 0 ]; then local pavg pavg=$(printf '%s\n' "${pings[@]}" | awk '{s+=$1} END {printf "%.2f", s/NR}') result+="Среднее: ${pavg}ms | Потеряно: $plost/3\n" else result+="Сервер не ответил\n" result+="Включите ping:\nsysctl -w net.ipv4.icmp_echo_ignore_all=0\n" result+="iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT\n" fi echo "$result" } # ─── 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) [[ -z "$IFACE" ]] && echo -e "${RED}[ERROR] Не удалось определить интерфейс!${NC}" && exit 1 } 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 } get_system_stats() { local cpu_line load_avg mem_info swap_info disk_info uptime_str top_procs cpu_usage 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}') 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}') 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 r="" r+="📊 Системная информация\n\n" r+="Uptime: ${uptime_str}\n" r+="CPU: ${cpu_line} ядер | ${cpu_usage}%\n" r+="Load: ${load_avg}\n" r+="RAM: ${mem_info}\n" r+="Swap: ${swap_info}\n" r+="Disk /: ${disk_info}\n\n" r+="Топ CPU:\n
PID    CPU%  MEM%  CMD\n${top_procs}
" echo "$r" } # ─── 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" | sed -n 's/.*--dport \([0-9]*\).*/\1/p') proto=$(echo "$line" | sed -n 's/.*-p \([a-z]*\).*/\1/p') dest=$(echo "$line" | sed -n 's/.*--to-destination \([0-9.:]*\).*/\1/p') [ -n "$port" ] && echo "${proto}|${port}|${dest}" done } get_target_ips() { get_rules_list | awk -F'|' '{split($3,a,":"); print a[1]}' | sort -u } get_port_for_ip() { local ip="$1" get_rules_list | awk -F'|' -v ip="$ip" '{split($3,a,":"); if(a[1]==ip){print a[2]; exit}}' } tcp_ping() { local ip="$1" port="$2" tout="${3:-3}" local raw raw=$(curl -so /dev/null -w '%{time_connect}' --max-time "$tout" --connect-timeout "$tout" "http://${ip}:${port}/" 2>/dev/null) [ -z "$raw" ] && return 1 local ms ms=$(awk "BEGIN {v=$raw*1000; if(v<0.5) exit 1; printf \"%.2f\", v}" 2>/dev/null) || return 1 echo "$ms" } smart_ping() { local ip="$1" tout="${2:-3}" port="${3:-}" local ms ms=$(ping -c 1 -W "$tout" "$ip" 2>/dev/null | sed -n 's/.*time=\([0-9.]*\).*/\1/p') if [ -n "$ms" ]; then echo "ICMP|$ms"; return 0; fi [ -z "$port" ] && port=$(get_port_for_ip "$ip") [ -z "$port" ] && return 1 ms=$(tcp_ping "$ip" "$port" "$tout") if [ -n "$ms" ]; then echo "TCP:${port}|$ms"; return 0; fi return 1 } remove_rules_for_port() { local proto="$1" in_port="$2" iptables -t nat -S PREROUTING 2>/dev/null | grep "DNAT" | grep -- "--dport ${in_port} " | grep -- "-p ${proto} " | 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 -- "--dport ${in_port} " | grep -- "-p ${proto} " | while read -r rule; do eval "iptables -D ${rule#-A }" 2>/dev/null done iptables -S FORWARD 2>/dev/null | grep "kaskad" | grep -- "-p ${proto} " | while read -r rule; do local rd; rd=$(echo "$rule" | sed -n 's/.*--dport \([0-9]*\).*/\1/p') local rs; rs=$(echo "$rule" | sed -n 's/.*--sport \([0-9]*\).*/\1/p') [[ "$rd" == "$in_port" || "$rs" == "$in_port" ]] && eval "iptables -D ${rule#-A }" 2>/dev/null done } 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_validated_port "Введите Порт (одинаковый для входа и выхода):" local port="$_RET_PORT" probe_server_cli "$target_ip" "$port" || return echo -e "\n${YELLOW}Будет создано правило:${NC}" echo -e " $proto: ${MY_IP:-*}:$port -> $(fmt_ip_short "$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}" 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_validated_port "Введите ${YELLOW}ВХОДЯЩИЙ Порт${NC} (на этом сервере):" local in_port="$_RET_PORT" read_validated_port "Введите ${YELLOW}ИСХОДЯЩИЙ Порт${NC} (на конечном сервере):" local out_port="$_RET_PORT" probe_server_cli "$target_ip" "$out_port" || return echo -e "\n${YELLOW}Будет создано правило:${NC}" echo -e " $proto: ${MY_IP:-*}:$in_port -> $(fmt_ip_short "$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" 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%:*}" echo -e " ${WHITE}${MY_IP:-*}:${port}${NC} ($proto) → ${GREEN}${dest}${NC} $(fmt_ip "$dest_ip")" 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%:*}" echo -e "${YELLOW}[$i]${NC} ${MY_IP:-*}:$port ($proto) -> $(fmt_ip_short "$dest_ip")" ((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 [[ "$rule_num" == "0" || -z "${rules_arr[$rule_num]:-}" ]] && return IFS='|' read -r d_proto d_port d_dest <<< "${rules_arr[$rule_num]}" 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 if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then ufw delete allow "$d_port/$d_proto" > /dev/null 2>&1 fi 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 if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then iptables -S INPUT 2>/dev/null | grep "kaskad" | while read -r ul; do local up; up=$(echo "$ul" | sed -n 's/.*--dport \([0-9]*\).*/\1/p') local upr; upr=$(echo "$ul" | sed -n 's/.*-p \([a-z]*\).*/\1/p') [ -n "$up" ] && [ -n "$upr" ] && ufw delete allow "$up/$upr" > /dev/null 2>&1 done fi 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] Очищено.${NC}" fi read -p "Нажмите Enter..." } full_uninstall() { clear echo -e "\n${RED}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${RED}║ ⚠ ПОЛНОЕ УДАЛЕНИЕ KASKAD PRO ⚠ ║${NC}" echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${WHITE}Будут удалены:${NC}" echo -e " ${RED}•${NC} Все правила каскада (iptables)" echo -e " ${RED}•${NC} Telegram-бот и мониторинг" echo -e " ${RED}•${NC} Конфигурация ${WHITE}/etc/kaskad/${NC}" echo -e " ${RED}•${NC} Команда ${WHITE}gokaskad${NC}" echo -e " ${RED}•${NC} Логи ${WHITE}/var/log/kaskad.log${NC}" echo "" echo -e "${GREEN}НЕ будет затронуто:${NC}" echo -e " ${GREEN}•${NC} Системные пакеты (iptables, jq, curl, qrencode)" echo -e " ${GREEN}•${NC} Ваши VPN / прокси (WireGuard, XRay и т.д.)" echo -e " ${GREEN}•${NC} Настройки sysctl (ip_forward, bbr)" echo "" read -p "$(echo -e "${RED}Удалить Kaskad PRO полностью? (y/n): ${NC}")" confirm1 [[ "$confirm1" != "y" ]] && { echo -e "\n${CYAN}Отменено.${NC}"; read -p "Нажмите Enter..."; return; } local words=("УДАЛИТЬ" "СТЕРЕТЬ" "СНЕСТИ" "УНИЧТОЖИТЬ" "ПРОЩАЙ") local word="${words[$((RANDOM % ${#words[@]}))]}" echo "" echo -e "${RED}Последний шанс! Введите слово ${WHITE}${word}${RED} для подтверждения:${NC}" read -p "> " confirm2 if [[ "$confirm2" != "$word" ]]; then echo -e "\n${CYAN}Неверное слово. Удаление отменено.${NC}" read -p "Нажмите Enter..." return fi echo "" echo -e "${YELLOW}Удаление Kaskad PRO...${NC}\n" if systemctl is-active kaskad-bot &>/dev/null; then systemctl stop kaskad-bot 2>/dev/null; systemctl disable kaskad-bot 2>/dev/null fi [ -f "$BOT_PID_FILE" ] && { kill "$(cat "$BOT_PID_FILE")" 2>/dev/null; rm -f "$BOT_PID_FILE"; } rm -f /etc/systemd/system/kaskad-bot.service echo -e " ${GREEN}✓${NC} Telegram-бот остановлен" if systemctl is-active kaskad-monitor &>/dev/null; then systemctl stop kaskad-monitor 2>/dev/null; systemctl disable kaskad-monitor 2>/dev/null fi [ -f "$MONITOR_PID_FILE" ] && { kill "$(cat "$MONITOR_PID_FILE")" 2>/dev/null; rm -f "$MONITOR_PID_FILE"; } rm -f /etc/systemd/system/kaskad-monitor.service systemctl daemon-reload 2>/dev/null echo -e " ${GREEN}✓${NC} Мониторинг остановлен" if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then iptables -S INPUT 2>/dev/null | grep "kaskad" | while read -r ul; do local up; up=$(echo "$ul" | sed -n 's/.*--dport \([0-9]*\).*/\1/p') local upr; upr=$(echo "$ul" | sed -n 's/.*-p \([a-z]*\).*/\1/p') [ -n "$up" ] && [ -n "$upr" ] && ufw delete allow "$up/$upr" > /dev/null 2>&1 done echo -e " ${GREEN}✓${NC} Правила UFW очищены" fi 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 echo -e " ${GREEN}✓${NC} Правила iptables удалены" rm -rf "$KASKAD_DIR" echo -e " ${GREEN}✓${NC} Конфигурация удалена" rm -f "$KASKAD_LOG" echo -e " ${GREEN}✓${NC} Логи удалены" rm -f /usr/local/bin/gokaskad echo -e " ${GREEN}✓${NC} Команда gokaskad удалена" echo "" echo -e "${GREEN}══════════════════════════════════════════${NC}" echo -e "${GREEN} Kaskad PRO полностью удалён.${NC}" echo -e "${WHITE} Спасибо, что пользовались!${NC}" echo -e "${GREEN}══════════════════════════════════════════${NC}" echo "" read -p "Нажмите Enter..." exit 0 } 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} $(fmt_ip "${ips[$i]}")" local note; note=$(get_alias_field "${ips[$i]}" "note") [ -n "$note" ] && echo -e " ${WHITE}Примечание:${NC} $note" done echo -e " ${YELLOW}[0]${NC} Назад" read -p "Сервер: " choice [[ "$choice" == "0" || -z "$choice" ]] && return local idx=$((choice - 1)) [ -z "${ips[$idx]:-}" ] && continue local sel="${ips[$idx]}" echo -e "Новое имя для $sel (Enter — оставить):" read -p "> " nn; [ -n "$nn" ] && set_alias "$sel" "$nn" echo -e "Новое примечание (Enter — оставить):" read -p "> " nt; [ -n "$nt" ] && set_alias_note "$sel" "$nt" 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) [ -z "$update_token" ] && update_token=$(grep "^GITHUB_PAT=" "$KASKAD_CONF" 2>/dev/null | cut -d'"' -f2) echo -e "${YELLOW}[*] Загрузка обновления...${NC}" local ok=0 [ -n "$update_token" ] && curl -sL -H "Authorization: token $update_token" "$repo_url" -o /tmp/kaskad_update.sh 2>/dev/null \ && head -1 /tmp/kaskad_update.sh 2>/dev/null | grep -q "#!/bin/bash" && ok=1 if [ "$ok" -eq 0 ]; then curl -sL "$repo_url" -o /tmp/kaskad_update.sh 2>/dev/null \ && head -1 /tmp/kaskad_update.sh 2>/dev/null | grep -q "#!/bin/bash" && ok=1 fi if [ "$ok" -eq 0 ]; then echo -e "${RED}Не удалось скачать. Репозиторий приватный.${NC}" echo -e "${WHITE}Введите GitHub PAT (токен доступа) или Enter для отмены:${NC}" echo -e "${CYAN}(Создать: GitHub → Settings → Developer settings → Personal access tokens)${NC}" read -p "> " new_token if [ -n "$new_token" ]; then curl -sL -H "Authorization: token $new_token" "$repo_url" -o /tmp/kaskad_update.sh 2>/dev/null \ && head -1 /tmp/kaskad_update.sh 2>/dev/null | grep -q "#!/bin/bash" && ok=1 if [ "$ok" -eq 1 ]; then mkdir -p "$BOT_STATE_DIR" bot_set_state "system" "UPDATE_TOKEN=$new_token" save_config_val "GITHUB_PAT" "$new_token" echo -e "${GREEN}Токен сохранён для будущих обновлений.${NC}" else echo -e "${RED}Токен не подошёл или ошибка сети.${NC}" fi fi 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] Обновлён! Перезапустите: gokaskad${NC}"; log_action "Self-update completed" else [ "$ok" -eq 0 ] && echo -e "${RED}[ERROR] Не удалось обновить.${NC}" rm -f /tmp/kaskad_update.sh fi read -p "Нажмите Enter..." } # ═══════════════════════════════════════════════════════════════ # LIVE PING with ASCII bar # ═══════════════════════════════════════════════════════════════ make_ping_bar() { local ms_str="$1" width=25 local ms_int ms_int=$(awk "BEGIN {printf \"%d\", $ms_str + 0.5}") local filled=$(( ms_int * width / 100 )) (( filled > width )) && filled=$width (( filled < 1 )) && filled=1 local empty=$(( width - filled )) local color="$GREEN" (( ms_int > 50 )) && color="$YELLOW" (( ms_int > 100 )) && color="$RED" local bar="${color}" for (( b=0; b 1 )); then local hist_start=$(( count - show )) (( hist_start < 0 )) && hist_start=0 fi echo "" 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 -e " ${WHITE}Мин:${NC} ${s_min}ms ${WHITE}│${NC} ${WHITE}Макс:${NC} ${s_max}ms ${WHITE}│${NC} ${WHITE}Сред:${NC} ${s_avg}ms" fi echo -e " ${WHITE}Потеряно:${NC} $lost / $count" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" sleep 1 done trap - INT echo "" if [ ${#results[@]} -eq 0 ] && [ "$count" -gt 0 ]; then echo -e "${YELLOW}━━━ Сервер не ответил ни разу ━━━${NC}" echo -e "${WHITE}ICMP заблокирован$([ -n "$_port" ] && echo " и TCP:$_port не удался").${NC}" echo "" echo -e "${CYAN}Чтобы включить ping на удалённом сервере:${NC}" echo -e " ${WHITE}ssh root@${ip}${NC}" echo -e " ${GREEN}sysctl -w net.ipv4.icmp_echo_ignore_all=0${NC}" echo -e " ${GREEN}echo 'net.ipv4.icmp_echo_ignore_all=0' >> /etc/sysctl.conf${NC}" echo -e " ${GREEN}iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT${NC}" echo "" fi 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 # ═══════════════════════════════════════════════════════════════ add_monitor() { local ip="$1" interval="$2" threshold="$3" cooldown="${4:-300}" cat > "$MONITOR_DIR/${ip}.conf" </dev/null 2>&1 || start_monitoring_silent else systemctl is-active kaskad-monitor &>/dev/null 2>&1 && stop_monitoring_silent 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" } 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" echo -e " ${WHITE}$(fmt_ip_short "$MON_IP")${NC} инт: ${MON_INTERVAL}s порог: ${MON_THRESHOLD}ms уведомл: ${MON_COOLDOWN}s" done [ "$found" -eq 0 ] && echo -e " ${YELLOW}Нет мониторов.${NC}" } monitor_alert() { local ip="$1" ping_ms="$2" threshold="$3" cooldown="${4:-300}" local alert_file="$MONITOR_DIR/.last_alert_${ip}" last_alert=0 [ -f "$alert_file" ] && last_alert=$(cat "$alert_file") local now; now=$(date +%s) (( now - last_alert < cooldown )) && return 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 header; header=$(fmt_ip_short "$ip") local text [ "$ping_ms" = "TIMEOUT" ] && text="⚠️ ALERT: ${header}\nPing: TIMEOUT (порог: ${threshold}ms)" \ || text="⚠️ ALERT: ${header}\nPing: ${ping_ms}ms (порог: ${threshold}ms)" 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 cf in "$MONITOR_DIR"/*.conf; do [ -f "$cf" ] || continue local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300; source "$cf" local ckf="$MONITOR_DIR/.last_check_${MON_IP}" lc=0 [ -f "$ckf" ] && lc=$(cat "$ckf") if (( now - lc >= MON_INTERVAL )); then echo "$now" > "$ckf" local pr_raw; pr_raw=$(smart_ping "$MON_IP" 3) local pr="${pr_raw#*|}" if [ -z "$pr" ] || [ -z "$pr_raw" ]; then monitor_alert "$MON_IP" "TIMEOUT" "$MON_THRESHOLD" "$MON_COOLDOWN" else local pi; pi=$(awk "BEGIN {printf \"%d\", $pr + 0.5}") (( pi > MON_THRESHOLD )) && monitor_alert "$MON_IP" "$pr" "$MON_THRESHOLD" "$MON_COOLDOWN" fi fi done sleep 1 done } monitoring_menu() { while true; do clear local ms="${RED}Остановлен${NC}" systemctl is-active kaskad-monitor &>/dev/null 2>&1 && ms="${GREEN}Работает${NC}" echo -e "${CYAN}━━━ Мониторинг (авто) ━━━${NC}" echo -e "Статус: $ms"; 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)" [ ${#ips[@]} -eq 0 ] && echo -e "${YELLOW}Нет серверов.${NC}" && read -p "Enter..." && continue for i in "${!ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip_short "${ips[$i]}")"; done read -p "Сервер: " sc; local si=$((sc-1)); [ -z "${ips[$si]:-}" ] && continue echo -e "Интервал: 1) 10с 2) 1мин 3) 5мин" read -p "> " ic; local iv=60; case $ic in 1) iv=10;; 3) iv=300;; esac read_validated_port "Порог (мс):"; local th="$_RET_PORT" echo -e "Уведомления: 1) 10с 2) 60с 3) 5мин 4) 15мин" read -p "> " cc; local cd=300; case $cc in 1) cd=10;; 2) cd=60;; 4) cd=900;; esac add_monitor "${ips[$si]}" "$iv" "$th" "$cd" echo -e "${GREEN}[OK]${NC}"; read -p "Enter..." ;; 2) local -a mi=() for c in "$MONITOR_DIR"/*.conf; do [ -f "$c" ] || continue; local MON_IP=""; source "$c"; mi+=("$MON_IP"); done [ ${#mi[@]} -eq 0 ] && echo -e "${YELLOW}Нет.${NC}" && read -p "Enter..." && continue for i in "${!mi[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip_short "${mi[$i]}")"; done read -p "Номер: " dc; local di=$((dc-1)) [ -n "${mi[$di]:-}" ] && remove_monitor "${mi[$di]}" && echo -e "${GREEN}[OK]${NC}" read -p "Enter..." ;; 0) return ;; esac done } # ═══════════════════════════════════════════════════════════════ # TELEGRAM BOT # ═══════════════════════════════════════════════════════════════ tg_api() { curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/$1" -H "Content-Type: application/json" -d "$2" 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_send_reply_kb() { local chat_id="$1" text keyboard="$3" text=$(printf '%b' "$2") local payload payload=$(jq -n --arg c "$chat_id" --arg t "$text" --argjson k "$keyboard" \ '{chat_id:$c, text:$t, parse_mode:"HTML", reply_markup:{keyboard:$k, resize_keyboard:true, one_time_keyboard:false}}') tg_api "sendMessage" "$payload" } tg_remove_reply_kb() { local chat_id="$1" text="$2" text=$(printf '%b' "$text") local payload payload=$(jq -n --arg c "$chat_id" --arg t "$text" \ '{chat_id:$c, text:$t, parse_mode:"HTML", reply_markup:{remove_keyboard:true}}') 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() { tg_api "answerCallbackQuery" "{\"callback_query_id\":\"$1\",\"text\":\"${2:-}\"}" } # ─── Bot state ──────────────────────────────────────────────── bot_set_state() { local c="$1"; shift; printf '%s\n' "$@" > "$BOT_STATE_DIR/$c"; } bot_get_state() { [ -f "$BOT_STATE_DIR/$1" ] && grep "^${2}=" "$BOT_STATE_DIR/$1" | head -1 | cut -d= -f2-; } bot_clear_state() { rm -f "$BOT_STATE_DIR/$1"; } get_menu_style() { source "$KASKAD_CONF" 2>/dev/null; echo "${MENU_STYLE:-inline}"; } # ─── Bot keyboards ─────────────────────────────────────────── kbd_inline_main() { 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":"⌨️ Reply-клавиатура","callback_data":"sw_reply"}] ] JSON } reply_kb_json() { cat <<'JSON' [ ["🔀 AWG/WG", "🔀 VLESS"], ["🔀 MTProto", "🛠 Custom"], ["📋 Правила", "🏓 Ping"], ["📊 Монитор", "💻 Система"], ["❌ Удалить", "🗑 Сброс"], ["🏢 Хостинг"], ["/inline"] ] JSON } 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 i "$interval" --arg t "$threshold" \ '[[{"text":"10с","callback_data":("mc:"+$ip+":"+$i+":"+$t+":10")}],[{"text":"60с","callback_data":("mc:"+$ip+":"+$i+":"+$t+":60")}],[{"text":"5мин","callback_data":("mc:"+$ip+":"+$i+":"+$t+":300")}],[{"text":"15мин","callback_data":("mc:"+$ip+":"+$i+":"+$t+":900")}],[{"text":"⬅️","callback_data":"m"}]]' } build_ip_kbd() { local prefix="$1"; shift; local ips=("$@") 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 dip="${dest%:*}" label; label=$(get_alias "$dip") local bt="❌ :$port ($proto) → $dest"; [ -n "$label" ] && bt="❌ :$port → $label" [ "$first" -eq 0 ] && rows+=","; rows+="[{\"text\":\"$bt\",\"callback_data\":\"dr_${i}\"}]"; first=0; ((i++)) done <<< "$rules" [ -z "$rows" ] && echo '[[{"text":"Нет правил","callback_data":"m"}]]' || echo "[${rows},[{\"text\":\"⬅️\",\"callback_data\":\"m\"}]]" } # ─── Bot handlers ───────────────────────────────────────────── bot_main_menu() { local chat_id="$1" msg_id="${2:-}" bot_clear_state "$chat_id" local style; style=$(get_menu_style) local text="Kaskad PRO v${KASKAD_VERSION}\nIP: ${MY_IP:-N/A}\nВыберите действие:" if [ "$style" = "reply" ]; then tg_send_reply_kb "$chat_id" "$text" "$(reply_kb_json)" > /dev/null else local kbd; kbd=$(kbd_inline_main) if [ -n "$msg_id" ]; then tg_edit "$chat_id" "$msg_id" "$text" "$kbd" else tg_send "$chat_id" "$text" "$kbd" fi fi } bot_handle_reply_text() { local chat_id="$1" text="$2" case "$text" in "🔀 AWG/WG") bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=udp" "NAME=AmneziaWG" "CUSTOM=0"; tg_send "$chat_id" "🔀 AmneziaWG (UDP)\n\nВведите IP:" "$(kbd_back)" > /dev/null ;; "🔀 VLESS") bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=tcp" "NAME=VLESS" "CUSTOM=0"; tg_send "$chat_id" "🔀 VLESS (TCP)\n\nВведите IP:" "$(kbd_back)" > /dev/null ;; "🔀 MTProto") bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=tcp" "NAME=MTProto" "CUSTOM=0"; tg_send "$chat_id" "🔀 MTProto (TCP)\n\nВведите IP:" "$(kbd_back)" > /dev/null ;; "🛠 Custom") tg_send "$chat_id" "🛠 Custom Rule\n\nВыберите протокол:" "$(kbd_proto)" > /dev/null ;; "📋 Правила") bot_handle_callback "$chat_id" "" "" "lr_new" ;; "🏓 Ping") bot_handle_callback "$chat_id" "" "" "pm_new" ;; "📊 Монитор") bot_handle_callback "$chat_id" "" "" "mm_new" ;; "💻 Система") local s; s=$(get_system_stats); tg_send "$chat_id" "$s" "$(kbd_back)" > /dev/null ;; "❌ Удалить") tg_send "$chat_id" "❌ Выберите правило:" "$(build_delete_kbd)" > /dev/null ;; "🗑 Сброс") tg_send "$chat_id" "🗑 Уверены?" '[[{"text":"✅ Да","callback_data":"fa_y"},{"text":"❌ Нет","callback_data":"m"}]]' > /dev/null ;; "🏢 Хостинг") bot_handle_callback "$chat_id" "" "" "promo_new" ;; *) return 1 ;; esac return 0 } bot_handle_callback() { local chat_id="$1" msg_id="$2" cb_id="$3" data="$4" [ -n "$cb_id" ] && tg_answer_cb "$cb_id" > /dev/null local use_send=0 [[ "$data" == *_new ]] && use_send=1 && data="${data%_new}" case "$data" in m) bot_main_menu "$chat_id" "$msg_id" ;; sw_reply) save_config_val "MENU_STYLE" "reply"; bot_main_menu "$chat_id" ;; sw_inline) save_config_val "MENU_STYLE" "inline"; bot_main_menu "$chat_id" ;; a_u) bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=udp" "NAME=AmneziaWG" "CUSTOM=0" [ "$use_send" -eq 1 ] && tg_send "$chat_id" "🔀 AmneziaWG (UDP)\n\nВведите IP:" "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "🔀 AmneziaWG (UDP)\n\nВведите IP:" "$(kbd_back)" ;; a_t) bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=tcp" "NAME=VLESS" "CUSTOM=0" [ "$use_send" -eq 1 ] && tg_send "$chat_id" "🔀 VLESS (TCP)\n\nВведите IP:" "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "🔀 VLESS (TCP)\n\nВведите IP:" "$(kbd_back)" ;; a_mt) bot_set_state "$chat_id" "STATE=awaiting_ip" "PROTO=tcp" "NAME=MTProto" "CUSTOM=0" [ "$use_send" -eq 1 ] && tg_send "$chat_id" "🔀 MTProto (TCP)\n\nВведите IP:" "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "🔀 MTProto (TCP)\n\nВведите IP:" "$(kbd_back)" ;; a_c) [ "$use_send" -eq 1 ] && tg_send "$chat_id" "🛠 Custom\n\nПротокол:" "$(kbd_proto)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "🛠 Custom\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 ($proto)\n\nВведите IP:" "$(kbd_back)" ;; 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 [ -n "$port" ] || continue local dip="${dest%:*}" text+="${MY_IP:-*}:$port ($proto) → $dest\n" text+=" $(fmt_ip_tg "$dip")\n" done <<< "$rules" fi [ "$use_send" -eq 1 ] && tg_send "$chat_id" "$text" "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "$text" "$(kbd_back)" ;; dr) [ "$use_send" -eq 1 ] && tg_send "$chat_id" "❌ Выберите:" "$(build_delete_kbd)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "❌ Выберите:" "$(build_delete_kbd)" ;; dr_*) local idx="${data#dr_}" line; line=$(get_rules_list | sed -n "${idx}p") if [ -n "$line" ]; then IFS='|' read -r dp dpo dd <<< "$line" iptables -t nat -D PREROUTING -p "$dp" --dport "$dpo" -j DNAT --to-destination "$dd" 2>/dev/null iptables -S INPUT 2>/dev/null | grep "kaskad:${dpo}:${dp}" | while read -r r; do eval "iptables -D ${r#-A }" 2>/dev/null; done iptables -S FORWARD 2>/dev/null | grep "kaskad:${dpo}:${dp}" | while read -r r; do eval "iptables -D ${r#-A }" 2>/dev/null; done save_iptables; log_action "BOT DELETE: $dp :$dpo -> $dd" tg_edit "$chat_id" "$msg_id" "✅ $dp :$dpo → $dd удалено." "$(kbd_back)" else tg_edit "$chat_id" "$msg_id" "Не найдено." "$(kbd_back)"; fi ;; fa) tg_edit "$chat_id" "$msg_id" "🗑 Уверены?" '[[{"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 r; r=$(iptables -t nat -S PREROUTING | grep "DNAT" | head -1); eval "iptables -t nat -D ${r#-A }" 2>/dev/null done for ch in INPUT FORWARD; do while iptables -S "$ch" 2>/dev/null | grep -q "kaskad"; do local r; r=$(iptables -S "$ch" | grep "kaskad" | head -1); eval "iptables -D ${r#-A }" 2>/dev/null done done save_iptables; log_action "BOT FLUSH" tg_edit "$chat_id" "$msg_id" "✅ Очищено." "$(kbd_back)" ;; sys) local s; s=$(get_system_stats) [ "$use_send" -eq 1 ] && tg_send "$chat_id" "$s" "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "$s" "$(kbd_back)" ;; promo) local pt="🏢 Хостинг, который работает\n\n🌍 РФ и Европа\n👉 https://vk.cc/ct29NQ\n\nOFF60 — 60% скидка\nantenka20 — +20% (3мес)\nantenka6 — +15% (6мес)\nantenka12 — +5% (12мес)\n\n🇧🇾 Беларусь\n👉 https://vk.cc/cUxAhj\nOFF60 — 60% скидка" [ "$use_send" -eq 1 ] && tg_send "$chat_id" "$pt" "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "$pt" "$(kbd_back)" ;; pm) local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)" if [ ${#ips[@]} -eq 0 ]; then [ "$use_send" -eq 1 ] && tg_send "$chat_id" "🏓 Нет серверов." "$(kbd_back)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "🏓 Нет серверов." "$(kbd_back)" else [ "$use_send" -eq 1 ] && tg_send "$chat_id" "🏓 Сервер:" "$(build_ip_kbd "ps" "${ips[@]}")" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "🏓 Сервер:" "$(build_ip_kbd "ps" "${ips[@]}")" fi ;; ps:*) local ip="${data#ps:}"; local lb; lb=$(fmt_ip_short "$ip") tg_edit "$chat_id" "$msg_id" "🏓 $lb\nРежим:" "$(kbd_ping_opts "$ip")" ;; po:*) local ip="${data#po:}"; local lb; lb=$(fmt_ip_short "$ip") ( local raw; raw=$(smart_ping "$ip" 3) if [ -n "$raw" ]; then local mtd="${raw%%|*}" pms="${raw#*|}" tg_send "$chat_id" "🏓 $lb\n${pms} ms [$mtd]" "$(kbd_back)" > /dev/null else tg_send "$chat_id" "🏓 $lb\ntimeout\n\nСервер не ответил.\nВключите ping:\nsysctl -w net.ipv4.icmp_echo_ignore_all=0\niptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT" "$(kbd_back)" > /dev/null fi ) & ;; p10:*) local ip="${data#p10:}"; local lb; lb=$(fmt_ip_short "$ip") ( local resp; resp=$(tg_send "$chat_id" "🏓 $lb (10x)..." "") local mid; mid=$(echo "$resp" | jq -r '.result.message_id // empty') local -a res=(); local lost=0 txt="" for n in $(seq 1 10); do local raw; raw=$(smart_ping "$ip" 3) if [ -n "$raw" ]; then local mtd="${raw%%|*}" pms="${raw#*|}" res+=("$pms"); txt+="#$n: ${pms}ms [$mtd]\n" else ((lost++)); txt+="#$n: timeout\n"; fi sleep 1 done local sm="🏓 $lb (10x)\n${txt}" [ ${#res[@]} -gt 0 ] && { local av; av=$(printf '%s\n' "${res[@]}" | awk '{s+=$1} END {printf "%.2f",s/NR}'); sm+="\nСреднее: ${av}ms"; } sm+="\nПотеряно: $lost/10" if [ ${#res[@]} -eq 0 ]; then sm+="\n\nВключите ping:\nsysctl -w net.ipv4.icmp_echo_ignore_all=0"; fi [ -n "$mid" ] && tg_edit "$chat_id" "$mid" "$sm" "$(kbd_back)" > /dev/null || tg_send "$chat_id" "$sm" "$(kbd_back)" > /dev/null ) & ;; p60:*) local ip="${data#p60:}"; local lb; lb=$(fmt_ip_short "$ip") ( local resp; resp=$(tg_send "$chat_id" "🏓 $lb (60с)..." "") local mid; mid=$(echo "$resp" | jq -r '.result.message_id // empty') local -a res=(); local lost=0 for n in $(seq 1 60); do local raw; raw=$(smart_ping "$ip" 3) if [ -n "$raw" ]; then local pms="${raw#*|}"; res+=("$pms") else ((lost++)); fi if (( n % 10 == 0 )) && [ -n "$mid" ]; then local p="🏓 $lb: ${n}/60с\nОК: ${#res[@]} | Lost: $lost" [ ${#res[@]} -gt 0 ] && { local pa; pa=$(printf '%s\n' "${res[@]}" | awk '{s+=$1} END {printf "%.2f",s/NR}'); p+="\nСред: ${pa}ms"; } tg_edit "$chat_id" "$mid" "$p" "" > /dev/null fi; sleep 1 done local sm="🏓 $lb (60с) — готово\n" if [ ${#res[@]} -gt 0 ]; then local st; st=$(printf '%s\n' "${res[@]}" | 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 sn sx sa <<< "$st"; sm+="Мин: ${sn}ms\nМакс: ${sx}ms\nСред: ${sa}ms\n" fi; sm+="Потеряно: $lost/60" if [ ${#res[@]} -eq 0 ]; then sm+="\n\nВключите ping:\nsysctl -w net.ipv4.icmp_echo_ignore_all=0"; fi [ -n "$mid" ] && tg_edit "$chat_id" "$mid" "$sm" "$(kbd_back)" > /dev/null || tg_send "$chat_id" "$sm" "$(kbd_back)" > /dev/null ) & ;; mm) [ "$use_send" -eq 1 ] && tg_send "$chat_id" "📊 Мониторинг" "$(kbd_monitor)" > /dev/null \ || tg_edit "$chat_id" "$msg_id" "📊 Мониторинг" "$(kbd_monitor)" ;; ma) local -a ips=() while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)" [ ${#ips[@]} -eq 0 ] && tg_edit "$chat_id" "$msg_id" "Нет серверов." "$(kbd_back)" \ || tg_edit "$chat_id" "$msg_id" "📊 Сервер:" "$(build_ip_kbd "ma" "${ips[@]}")" ;; ma:*) local ip="${data#ma:}"; tg_edit "$chat_id" "$msg_id" "📊 $(fmt_ip_short "$ip")\nИнтервал:" "$(kbd_intervals "$ip")" ;; mi:*) local rest="${data#mi:}" ip="${rest%:*}" interval="${rest##*:}" bot_set_state "$chat_id" "STATE=awaiting_threshold" "MON_IP=$ip" "MON_INTERVAL=$interval" tg_edit "$chat_id" "$msg_id" "📊 $(fmt_ip_short "$ip") (${interval}с)\n\nПорог (мс):" "$(kbd_back)" ;; mc:*) local rest="${data#mc:}"; IFS=':' read -r ip interval threshold cooldown <<< "$rest" add_monitor "$ip" "$interval" "$threshold" "$cooldown" tg_edit "$chat_id" "$msg_id" "✅ $(fmt_ip_short "$ip")\n${interval}с | ${threshold}мс | уведомл: ${cooldown}с" "$(kbd_back)" ;; ml) local t="📊 Мониторы:\n"; local found=0 for c in "$MONITOR_DIR"/*.conf; do [ -f "$c" ] || continue; found=1 local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300; source "$c" t+="$(fmt_ip_tg "$MON_IP")\n ${MON_INTERVAL}с | ${MON_THRESHOLD}мс | ${MON_COOLDOWN}с\n" done [ "$found" -eq 0 ] && t+="Нет." tg_edit "$chat_id" "$msg_id" "$t" "$(kbd_monitor)" ;; md) local -a mi=() for c in "$MONITOR_DIR"/*.conf; do [ -f "$c" ] || continue; local MON_IP=""; source "$c"; mi+=("$MON_IP"); done [ ${#mi[@]} -eq 0 ] && tg_edit "$chat_id" "$msg_id" "Нет." "$(kbd_monitor)" \ || tg_edit "$chat_id" "$msg_id" "📊 Удалить:" "$(build_ip_kbd "md" "${mi[@]}")" ;; md:*) local ip="${data#md:}"; remove_monitor "$ip" tg_edit "$chat_id" "$msg_id" "✅ $(fmt_ip_short "$ip") удалён." "$(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 if [ "$text" = "/inline" ]; then save_config_val "MENU_STYLE" "inline" tg_remove_reply_kb "$chat_id" "Переключено на inline-кнопки." > /dev/null bot_main_menu "$chat_id"; return fi local style; style=$(get_menu_style) if [ "$style" = "reply" ]; then local state; state=$(bot_get_state "$chat_id" "STATE") if [ -z "$state" ]; then bot_handle_reply_text "$chat_id" "$text" && return fi 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") if [ "$custom" = "1" ]; then bot_set_state "$chat_id" "STATE=awaiting_in_port" "PROTO=$proto" "NAME=$name" "CUSTOM=1" "TARGET_IP=$text" tg_send "$chat_id" "IP: $text ✅\n\nВХОДЯЩИЙ порт:" "$(kbd_back)" > /dev/null else bot_set_state "$chat_id" "STATE=awaiting_port" "PROTO=$proto" "NAME=$name" "CUSTOM=0" "TARGET_IP=$text" tg_send "$chat_id" "IP: $text ✅\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") local port="$text" local probe_msg; probe_msg=$(tg_send "$chat_id" "🔍 Проверяю $target_ip:$port..." "") local probe_mid; probe_mid=$(echo "$probe_msg" | jq -r '.result.message_id // empty') local probe_result; probe_result=$(probe_server_tg "$target_ip" "$port") local info_text="$target_ip:$port\n${probe_result}\nВведите имя (или - — пропустить):" [ -n "$probe_mid" ] && tg_edit "$chat_id" "$probe_mid" "$info_text" "$(kbd_back)" > /dev/null \ || tg_send "$chat_id" "$info_text" "$(kbd_back)" > /dev/null bot_set_state "$chat_id" "STATE=awaiting_name" "PROTO=$proto" "NAME=$name" "CUSTOM=0" "TARGET_IP=$target_ip" "PORT=$port" ;; awaiting_name) local proto name custom target_ip port 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") port=$(bot_get_state "$chat_id" "PORT") if [ "$text" != "-" ] && [ -n "$text" ]; then set_alias "$target_ip" "$text"; fi bot_set_state "$chat_id" "STATE=awaiting_note" "PROTO=$proto" "NAME=$name" "CUSTOM=$custom" "TARGET_IP=$target_ip" "PORT=$port" "IN_PORT=$(bot_get_state "$chat_id" "IN_PORT")" "OUT_PORT=$(bot_get_state "$chat_id" "OUT_PORT")" tg_send "$chat_id" "Примечание (или - — пропустить):" "$(kbd_back)" > /dev/null ;; awaiting_note) local proto name custom target_ip port 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_note "$target_ip" "$text"; fi if [ "$custom" = "1" ]; then local in_port out_port in_port=$(bot_get_state "$chat_id" "IN_PORT"); out_port=$(bot_get_state "$chat_id" "OUT_PORT") bot_clear_state "$chat_id" apply_iptables_rules "$proto" "$in_port" "$out_port" "$target_ip" "$name" tg_send "$chat_id" "✅ Custom\n$proto ${MY_IP:-*}:$in_port → $target_ip:$out_port\n$(fmt_ip_tg "$target_ip")" "$(kbd_back)" > /dev/null else port=$(bot_get_state "$chat_id" "PORT") bot_clear_state "$chat_id" apply_iptables_rules "$proto" "$port" "$port" "$target_ip" "$name" tg_send "$chat_id" "✅ $name\n$proto ${MY_IP:-*}:$port → $target_ip:$port\n$(fmt_ip_tg "$target_ip")" "$(kbd_back)" > /dev/null fi ;; awaiting_in_port) if ! validate_port "$text"; then tg_send "$chat_id" "❌ Порт." "" > /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" "❌ Порт." "" > /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") local probe_msg; probe_msg=$(tg_send "$chat_id" "🔍 Проверяю $target_ip:$text..." "") local probe_mid; probe_mid=$(echo "$probe_msg" | jq -r '.result.message_id // empty') local probe_result; probe_result=$(probe_server_tg "$target_ip" "$text") local info_text="$target_ip (вход:$in_port → выход:$text)\n${probe_result}\nВведите имя (или - — пропустить):" [ -n "$probe_mid" ] && tg_edit "$chat_id" "$probe_mid" "$info_text" "$(kbd_back)" > /dev/null \ || tg_send "$chat_id" "$info_text" "$(kbd_back)" > /dev/null bot_set_state "$chat_id" "STATE=awaiting_name" "PROTO=$proto" "NAME=$name" "CUSTOM=1" "TARGET_IP=$target_ip" "IN_PORT=$in_port" "OUT_PORT=$text" ;; 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" tg_send "$chat_id" "📊 $(fmt_ip_short "$mon_ip")\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" [ -z "$BOT_TOKEN" ] && log_action "BOT ERROR: no token" && exit 1 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) [ -z "$response" ] && sleep 2 && continue local ok; ok=$(echo "$response" | jq -r '.ok // "false"') [ "$ok" != "true" ] && sleep 5 && continue local cnt; cnt=$(echo "$response" | jq '.result | length') for (( i=0; i /dev/null && continue bot_handle_callback "$cci" "$cmi" "$cbi" "$cbd" else local mci mtx mci=$(echo "$upd" | jq -r '.message.chat.id // empty') mtx=$(echo "$upd" | jq -r '.message.text // empty') if [ -n "$mci" ] && [ -n "$mtx" ]; then [ -n "$BOT_CHAT_ID" ] && [ "$mci" != "$BOT_CHAT_ID" ] && tg_send "$mci" "⛔ Нет доступа.\nChat ID: $mci" "" > /dev/null && continue bot_handle_message "$mci" "$mtx" fi fi done done } start_bot() { source "$KASKAD_CONF" [ -z "$BOT_TOKEN" ] && echo -e "${RED}Задайте BOT_TOKEN!${NC}" && return [ -f "$BOT_PID_FILE" ] && kill -0 "$(cat "$BOT_PID_FILE")" 2>/dev/null && echo -e "${YELLOW}Уже запущен.${NC}" && return cat > /etc/systemd/system/kaskad-bot.service < /dev/null 2>&1; systemctl start kaskad-bot; sleep 1 systemctl is-active kaskad-bot &>/dev/null && echo -e "${GREEN}[OK] Бот запущен.${NC}" && log_action "Bot started" \ || echo -e "${RED}[ERROR] journalctl -u kaskad-bot${NC}" } 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 stopped" } bot_menu() { while true; do clear; source "$KASKAD_CONF" 2>/dev/null local bs="${RED}Выкл${NC}"; [ -f "$BOT_PID_FILE" ] && kill -0 "$(cat "$BOT_PID_FILE" 2>/dev/null)" 2>/dev/null && bs="${GREEN}Вкл ($(cat "$BOT_PID_FILE"))${NC}" local td="нет"; [ -n "${BOT_TOKEN:-}" ] && td="***${BOT_TOKEN: -6}" local ut; ut=$(bot_get_state "system" "UPDATE_TOKEN" 2>/dev/null); local ud="нет"; [ -n "$ut" ] && ud="***${ut: -6}" echo -e "${CYAN}━━━ Telegram Bot ━━━${NC}" echo -e "Статус: $bs\nТокен: ${YELLOW}$td${NC}\nChat ID: ${YELLOW}${BOT_CHAT_ID:-нет}${NC}\nUpdate: ${YELLOW}$ud${NC}\nМеню: ${YELLOW}${MENU_STYLE:-inline}${NC}\n" echo -e "1) Токен бота\n2) Chat ID (авто)\n3) Chat ID (вручную)\n4) ${GREEN}Запустить${NC}\n5) ${RED}Остановить${NC}\n6) GitHub PAT\n0) Назад" read -p "Выбор: " ch case $ch in 1) echo "Токен:"; 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 "" && continue echo -e "${YELLOW}Отправьте боту сообщение, затем Enter.${NC}"; read -p "" local c; c=$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?limit=1&offset=-1" | jq -r '.result[0].message.chat.id // empty') [ -n "$c" ] && save_config_val "BOT_CHAT_ID" "$c" && echo -e "${GREEN}$c${NC}" || echo -e "${RED}Нет.${NC}"; read -p "Enter..." ;; 3) echo "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 "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 # ═══════════════════════════════════════════════════════════════ show_promo() { clear; echo "" echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${MAGENTA}║ ХОСТИНГ СО СКИДКОЙ ДО -60% ║${NC}" echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}" echo -e "\n${CYAN}🌍 РФ И ЕВРОПА${NC}\n${WHITE} >>> https://vk.cc/ct29NQ${NC}" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "OFF60" "60% скидка" "antenka20" "+20% (3мес)" "antenka6" "+15% (6мес)" "antenka12" "+5% (12мес)" echo -e "\n${CYAN}🇧🇾 БЕЛАРУСЬ${NC}\n${WHITE} >>> https://vk.cc/cUxAhj${NC}" printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "OFF60" "60% скидка" echo -e "\n${YELLOW}QR-код... (3с)${NC}"; for i in 3 2 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}" read -p "Нажмите Enter..." } show_instructions() { local p=1 tp=7 while true; do clear; echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${MAGENTA}║ 📚 KASKAD PRO v${KASKAD_VERSION} (${p}/${tp}) ║${NC}" echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}\n" case $p in 1) echo -e "${CYAN}═══ ЧТО ТАКОЕ КАСКАД ═══${NC}\n\nМост: Клиент → Этот сервер → Зарубежный VPN → Интернет\nПровайдер видит только РФ IP.\n\nНужно: VPS в РФ + зарубежный VPN + IP:Порт" ;; 2) echo -e "${CYAN}═══ AWG/VLESS/MTProto ═══${NC}\n\n1) AWG: UDP, порт 51820\n2) VLESS: TCP, порт 443\n3) MTProto: TCP, порт 8443\n\nПосле ввода IP — автоматический GeoIP + пинг-тест" ;; 3) echo -e "${CYAN}═══ CUSTOM / ПРАВИЛА ═══${NC}\n\n4) Custom: разные порты, SSH/RDP\n5) Правила: таблица с IP каскада + GeoIP\n14) Имена серверов + примечания" ;; 4) echo -e "${CYAN}═══ PING ═══${NC}\n\n6) Live Ping с ASCII-графиком:\n ▓▓▓░░░░░ — зелёный (<50ms)\n ▓▓▓▓▓▓░░ — жёлтый (50-100ms)\n ▓▓▓▓▓▓▓▓ — красный (>100ms)\n ████████ — TIMEOUT" ;; 5) echo -e "${CYAN}═══ МОНИТОРИНГ ═══${NC}\n\n7) Автопроверка с алертами в Telegram\nИнтервалы: 10с / 1мин / 5мин\nУведомления: 10с / 60с / 5мин / 15мин\nСлужба авто-запуск/стоп" ;; 6) echo -e "${CYAN}═══ TELEGRAM BOT ═══${NC}\n\n8) @BotFather → токен → Chat ID → запуск\nДва режима меню:\n Inline — кнопки под сообщением\n Reply — большие кнопки внизу экрана\nПереключение: кнопка в меню или /inline" ;; 7) echo -e "${CYAN}═══ ВОЗМОЖНОСТИ БОТА ═══${NC}\n\nПравила, Ping (1x/10x/60с), Мониторинг\n💻 Система: CPU/RAM/Swap/Disk/процессы\n🏢 Хостинг: промокоды\nGeoIP + пинг-тест при добавлении\nИмена + примечания серверов" ;; esac echo -e "\n${MAGENTA}──────────────────────────────────────────────────────────────${NC}" [ "$p" -eq 1 ] && echo -e " ${YELLOW}[N]${NC} Далее ${YELLOW}[0]${NC} Выход" [ "$p" -eq "$tp" ] && echo -e " ${YELLOW}[P]${NC} Назад ${YELLOW}[0]${NC} Выход" (( p > 1 && p < tp )) && echo -e " ${YELLOW}[P]${NC} Назад ${YELLOW}[N]${NC} Далее ${YELLOW}[0]${NC} Выход" read -p " > " nav case "$nav" in [nN]) ((p1)) && ((p--));; 0) return;; [1-7]) p="$nav";; esac done } # ═══════════════════════════════════════════════════════════════ # MAIN MENU # ═══════════════════════════════════════════════════════════════ show_menu() { while true; do clear echo -e "${MAGENTA}******************************************************" echo " anten-ka · Kaskad PRO v${KASKAD_VERSION}" echo " YouTube: https://www.youtube.com/@antenkaru" echo -e "******************************************************${NC}" echo -e "${WHITE}IP: ${GREEN}${MY_IP}${NC} ${WHITE}Iface: ${CYAN}${IFACE}${NC}" 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}Сбросить всё${NC}" echo -e "11) ${YELLOW}Обновить скрипт${NC}" echo -e "12) ${YELLOW}PROMO${NC}" echo -e "13) ${MAGENTA}📚 Инструкция${NC}" echo -e "14) ${WHITE}Имена серверов${NC}" echo -e "15) ${RED}⚠ Удалить Kaskad PRO${NC}" echo -e " 0) Выход" echo -e "------------------------------------------------------" read -p "Выбор: " ch case $ch 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;; 15) full_uninstall;; 0) exit 0;; esac done } # ═══════════════════════════════════════════════════════════════ # STARTUP WITH PROGRESS # ═══════════════════════════════════════════════════════════════ run_startup() { local total=7 s=0 clear; echo "" echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${MAGENTA}║ KASKAD PRO v${KASKAD_VERSION} — Загрузка ║${NC}" echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" ((s++)) printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} Проверка прав root..." "$s" "$total" check_root printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} Права root подтверждены \n" "$s" "$total" ((s++)) printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} Загрузка конфигурации..." "$s" "$total" init_config printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} Конфигурация загружена \n" "$s" "$total" ((s++)) printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} Установка gokaskad..." "$s" "$total" 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 printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} Команда gokaskad \n" "$s" "$total" ((s++)) printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} IP Forwarding + BBR..." "$s" "$total" 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 printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} IP Forwarding + BBR Turbo \n" "$s" "$total" 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 ((s++)) if [ "$need_install" -eq 1 ]; then printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} Установка пакетов" "$s" "$total" ( while true; do printf "."; sleep 1; done ) & local dpid=$! 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 kill $dpid 2>/dev/null; wait $dpid 2>/dev/null printf "\r ${CYAN}[%d/%d]${NC} ${RED}✗${NC} Пакетный менеджер не найден! \n" "$s" "$total" exit 1 fi kill $dpid 2>/dev/null; wait $dpid 2>/dev/null printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} Пакеты установлены \n" "$s" "$total" else printf " ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} Все зависимости на месте \n" "$s" "$total" fi ((s++)) printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} Сетевой интерфейс..." "$s" "$total" detect_interface printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} Интерфейс: %-20s \n" "$s" "$total" "$IFACE" ((s++)) printf " ${CYAN}[%d/%d]${NC} ${YELLOW}⏳${NC} Внешний IP..." "$s" "$total" get_my_ip printf "\r ${CYAN}[%d/%d]${NC} ${GREEN}✓${NC} IP: %-25s \n" "$s" "$total" "$MY_IP" echo "" local w=40 bar="" for ((i=0; i