#!/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($1${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($1$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$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 <