#!/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 ms
ms=$(smart_ping "$ip" 3 "$port")
if [ -n "$ms" ]; then
pings+=("$ms")
echo -e " #$n: ${GREEN}${ms}ms${NC} ${CYAN}[${_PING_METHOD}]${NC}"
else
((plost++))
echo -e " #$n: ${RED}timeout${NC} ${WHITE}[ICMP fail$([ -n "$port" ] && echo ", TCP:$port fail")]${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 ms
ms=$(smart_ping "$ip" 3 "$port")
if [ -n "$ms" ]; then
pings+=("$ms")
result+=" #$n: ${ms}ms [${_PING_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"
}
_PING_METHOD=""
smart_ping() {
local ip="$1" tout="${2:-3}" port="${3:-}"
_PING_METHOD=""
local ms
ms=$(ping -c 1 -W "$tout" "$ip" 2>/dev/null | sed -n 's/.*time=\([0-9.]*\).*/\1/p')
if [ -n "$ms" ]; then _PING_METHOD="ICMP"; echo "$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 _PING_METHOD="TCP:${port}"; echo "$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${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 ms; ms=$(smart_ping "$ip" 3)
if [ -n "$ms" ]; then
tg_send "$chat_id" "🏓 $lb\n${ms} ms" "$(kbd_back)" > /dev/null
else
tg_send "$chat_id" "🏓 $lb\ntimeout\n\nICMP заблокирован на сервере.\nВключить:\nsysctl -w net.ipv4.icmp_echo_ignore_all=0" "$(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 ms; ms=$(smart_ping "$ip" 3)
[ -n "$ms" ] && res+=("$ms") && txt+="#$n: ${ms}ms\n" || { ((lost++)); txt+="#$n: timeout\n"; }
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"
[ -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 ms; ms=$(smart_ping "$ip" 3)
[ -n "$ms" ] && res+=("$ms") || ((lost++))
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($1$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$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 <