mirror of
https://github.com/anten-ka/kaskad-pro.git
synced 2026-05-19 14:26:01 +00:00
1773 lines
76 KiB
Bash
1773 lines
76 KiB
Bash
#!/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+="<b>📊 Системная информация</b>\n\n"
|
||
result+="<b>Uptime:</b> ${uptime_str}\n"
|
||
result+="<b>CPU:</b> ${cpu_line} ядер | загрузка: ${cpu_usage}%\n"
|
||
result+="<b>Load Avg:</b> ${load_avg}\n"
|
||
result+="<b>RAM:</b> ${mem_info}\n"
|
||
result+="<b>Swap:</b> ${swap_info}\n"
|
||
result+="<b>Disk /:</b> ${disk_info}\n\n"
|
||
result+="<b>Топ процессов (CPU):</b>\n"
|
||
result+="<pre>PID CPU% MEM% CMD\n"
|
||
result+="${top_procs}</pre>"
|
||
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<mn)mn=$1; if($1>mx)mx=$1}
|
||
END {printf "%.2f|%.2f|%.2f", mn, mx, s/NR}
|
||
')
|
||
IFS='|' read -r s_min s_max s_avg <<< "$stats"
|
||
echo ""
|
||
echo -e " ${WHITE}Мин:${NC} ${s_min}ms ${WHITE}Макс:${NC} ${s_max}ms ${WHITE}Сред:${NC} ${s_avg}ms"
|
||
fi
|
||
echo -e " ${WHITE}Потеряно:${NC} $lost / $count"
|
||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
|
||
sleep 1
|
||
done
|
||
|
||
trap - INT
|
||
echo ""
|
||
read -p "Нажмите Enter для возврата в меню..."
|
||
}
|
||
|
||
ping_menu() {
|
||
echo -e "\n${CYAN}--- Ping серверов ---${NC}"
|
||
local -a ips=()
|
||
while read -r ip; do
|
||
[ -n "$ip" ] && ips+=("$ip")
|
||
done <<< "$(get_target_ips)"
|
||
|
||
if [ ${#ips[@]} -eq 0 ]; then
|
||
echo -e "${YELLOW}Нет активных целевых серверов.${NC}"
|
||
read -p "Нажмите Enter..."
|
||
return
|
||
fi
|
||
|
||
echo -e "Активные целевые серверы:"
|
||
for i in "${!ips[@]}"; do
|
||
echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip "${ips[$i]}")"
|
||
done
|
||
echo -e " ${YELLOW}[0]${NC} Отмена"
|
||
|
||
read -p "Выберите сервер: " choice
|
||
[[ "$choice" == "0" || -z "$choice" ]] && return
|
||
local idx=$((choice - 1))
|
||
[ -z "${ips[$idx]:-}" ] && return
|
||
|
||
ping_live "${ips[$idx]}"
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# MONITORING (with configurable alert cooldown + auto start/stop)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
add_monitor() {
|
||
local ip="$1" interval="$2" threshold="$3" cooldown="${4:-300}"
|
||
cat > "$MONITOR_DIR/${ip}.conf" <<EOF
|
||
MON_IP="$ip"
|
||
MON_INTERVAL=$interval
|
||
MON_THRESHOLD=$threshold
|
||
MON_COOLDOWN=$cooldown
|
||
EOF
|
||
log_action "MONITOR ADD: $ip interval=${interval}s threshold=${threshold}ms cooldown=${cooldown}s"
|
||
sync_monitoring_service
|
||
}
|
||
|
||
remove_monitor() {
|
||
local ip="$1"
|
||
rm -f "$MONITOR_DIR/${ip}.conf" "$MONITOR_DIR/.last_check_${ip}" "$MONITOR_DIR/.last_alert_${ip}"
|
||
log_action "MONITOR REMOVE: $ip"
|
||
sync_monitoring_service
|
||
}
|
||
|
||
has_monitors() {
|
||
local found=0
|
||
for conf in "$MONITOR_DIR"/*.conf; do
|
||
[ -f "$conf" ] && found=1 && break
|
||
done
|
||
return $(( 1 - found ))
|
||
}
|
||
|
||
sync_monitoring_service() {
|
||
if has_monitors; then
|
||
if ! systemctl is-active kaskad-monitor &>/dev/null 2>&1; then
|
||
start_monitoring_silent
|
||
fi
|
||
else
|
||
if systemctl is-active kaskad-monitor &>/dev/null 2>&1; then
|
||
stop_monitoring_silent
|
||
fi
|
||
fi
|
||
}
|
||
|
||
start_monitoring_silent() {
|
||
cat > /etc/systemd/system/kaskad-monitor.service <<EOF
|
||
[Unit]
|
||
Description=Kaskad Monitoring Daemon
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=/usr/local/bin/gokaskad --monitor-daemon
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
systemctl daemon-reload
|
||
systemctl enable kaskad-monitor > /dev/null 2>&1
|
||
systemctl start kaskad-monitor 2>/dev/null
|
||
log_action "Monitoring auto-started"
|
||
}
|
||
|
||
stop_monitoring_silent() {
|
||
systemctl stop kaskad-monitor 2>/dev/null
|
||
systemctl disable kaskad-monitor 2>/dev/null
|
||
rm -f "$MONITOR_PID_FILE"
|
||
log_action "Monitoring auto-stopped (no monitors)"
|
||
}
|
||
|
||
list_monitors() {
|
||
local found=0
|
||
for conf in "$MONITOR_DIR"/*.conf; do
|
||
[ -f "$conf" ] || continue
|
||
found=1
|
||
local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300
|
||
source "$conf"
|
||
local label
|
||
label=$(fmt_ip "$MON_IP")
|
||
echo -e " ${WHITE}$label${NC} инт: ${MON_INTERVAL}s порог: ${MON_THRESHOLD}ms уведомл: ${MON_COOLDOWN}s"
|
||
done
|
||
if [ "$found" -eq 0 ]; then
|
||
echo -e " ${YELLOW}Нет настроенных мониторов.${NC}"
|
||
fi
|
||
}
|
||
|
||
monitor_alert() {
|
||
local ip="$1" ping_ms="$2" threshold="$3" cooldown="${4:-300}"
|
||
local alert_file="$MONITOR_DIR/.last_alert_${ip}"
|
||
local last_alert=0
|
||
[ -f "$alert_file" ] && last_alert=$(cat "$alert_file")
|
||
local now
|
||
now=$(date +%s)
|
||
|
||
if (( now - last_alert < cooldown )); then return; fi
|
||
echo "$now" > "$alert_file"
|
||
log_action "ALERT: $ip ping=${ping_ms}ms threshold=${threshold}ms"
|
||
|
||
source "$KASKAD_CONF" 2>/dev/null
|
||
if [ -n "${BOT_TOKEN:-}" ] && [ -n "${BOT_CHAT_ID:-}" ]; then
|
||
local label
|
||
label=$(get_alias "$ip")
|
||
local header="$ip"
|
||
[ -n "$label" ] && header="$label ($ip)"
|
||
local text
|
||
if [ "$ping_ms" = "TIMEOUT" ]; then
|
||
text="⚠️ <b>ALERT</b>: ${header}\nPing: TIMEOUT (порог: ${threshold}ms)"
|
||
else
|
||
text="⚠️ <b>ALERT</b>: ${header}\nPing: ${ping_ms}ms (порог: ${threshold}ms)"
|
||
fi
|
||
tg_send "$BOT_CHAT_ID" "$text" "" > /dev/null 2>&1
|
||
fi
|
||
}
|
||
|
||
monitor_daemon() {
|
||
log_action "Monitor daemon started (PID $$)"
|
||
echo $$ > "$MONITOR_PID_FILE"
|
||
|
||
while true; do
|
||
local now
|
||
now=$(date +%s)
|
||
for conf_file in "$MONITOR_DIR"/*.conf; do
|
||
[ -f "$conf_file" ] || continue
|
||
local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300
|
||
source "$conf_file"
|
||
|
||
local check_file="$MONITOR_DIR/.last_check_${MON_IP}"
|
||
local last_check=0
|
||
[ -f "$check_file" ] && last_check=$(cat "$check_file")
|
||
|
||
if (( now - last_check >= MON_INTERVAL )); then
|
||
echo "$now" > "$check_file"
|
||
local ping_result
|
||
ping_result=$(ping -c 1 -W 3 "$MON_IP" 2>/dev/null | grep -oP 'time=\K[\d.]+')
|
||
|
||
if [ -z "$ping_result" ]; then
|
||
monitor_alert "$MON_IP" "TIMEOUT" "$MON_THRESHOLD" "$MON_COOLDOWN"
|
||
else
|
||
local ping_int
|
||
ping_int=$(awk "BEGIN {printf \"%d\", $ping_result + 0.5}")
|
||
if (( ping_int > MON_THRESHOLD )); then
|
||
monitor_alert "$MON_IP" "$ping_result" "$MON_THRESHOLD" "$MON_COOLDOWN"
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
sleep 1
|
||
done
|
||
}
|
||
|
||
monitoring_menu() {
|
||
while true; do
|
||
clear
|
||
local mon_status="${RED}Остановлен${NC}"
|
||
if systemctl is-active kaskad-monitor &>/dev/null 2>&1; then
|
||
mon_status="${GREEN}Работает${NC}"
|
||
fi
|
||
|
||
echo -e "${CYAN}━━━ Мониторинг (авто) ━━━${NC}"
|
||
echo -e "Статус: $mon_status"
|
||
echo -e "${YELLOW}Служба запускается/останавливается автоматически.${NC}"
|
||
echo ""
|
||
list_monitors
|
||
echo ""
|
||
echo -e "1) Добавить мониторинг"
|
||
echo -e "2) Удалить мониторинг"
|
||
echo -e "0) Назад"
|
||
read -p "Выбор: " choice
|
||
|
||
case $choice in
|
||
1)
|
||
local -a ips=()
|
||
while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)"
|
||
if [ ${#ips[@]} -eq 0 ]; then
|
||
echo -e "${YELLOW}Нет активных серверов.${NC}"
|
||
read -p "Enter..."
|
||
continue
|
||
fi
|
||
echo "Серверы:"
|
||
for i in "${!ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip "${ips[$i]}")"; done
|
||
read -p "Сервер: " s_choice
|
||
local s_idx=$((s_choice - 1))
|
||
[ -z "${ips[$s_idx]:-}" ] && continue
|
||
local sel_ip="${ips[$s_idx]}"
|
||
|
||
echo -e "Интервал проверки:"
|
||
echo -e " 1) Каждые 10 сек"
|
||
echo -e " 2) Каждую минуту"
|
||
echo -e " 3) Каждые 5 мин"
|
||
read -p "Выбор: " int_ch
|
||
local interval=60
|
||
case $int_ch in
|
||
1) interval=10 ;; 2) interval=60 ;; 3) interval=300 ;;
|
||
esac
|
||
|
||
read_validated_port "Порог уведомления (мс):"
|
||
local threshold="$_RET_PORT"
|
||
|
||
echo -e "Частота уведомлений (не чаще чем):"
|
||
echo -e " 1) Каждые 10 сек"
|
||
echo -e " 2) Каждые 60 сек"
|
||
echo -e " 3) Каждые 5 мин"
|
||
echo -e " 4) Каждые 15 мин"
|
||
read -p "Выбор: " cd_ch
|
||
local cooldown=300
|
||
case $cd_ch in
|
||
1) cooldown=10 ;; 2) cooldown=60 ;; 3) cooldown=300 ;; 4) cooldown=900 ;;
|
||
esac
|
||
|
||
add_monitor "$sel_ip" "$interval" "$threshold" "$cooldown"
|
||
echo -e "${GREEN}[OK] Мониторинг для $(fmt_ip "$sel_ip") добавлен.${NC}"
|
||
read -p "Enter..."
|
||
;;
|
||
2)
|
||
local -a mon_ips=()
|
||
for conf in "$MONITOR_DIR"/*.conf; do
|
||
[ -f "$conf" ] || continue
|
||
local MON_IP=""
|
||
source "$conf"
|
||
mon_ips+=("$MON_IP")
|
||
done
|
||
if [ ${#mon_ips[@]} -eq 0 ]; then
|
||
echo -e "${YELLOW}Нет мониторов.${NC}"
|
||
read -p "Enter..."
|
||
continue
|
||
fi
|
||
for i in "${!mon_ips[@]}"; do echo -e " ${YELLOW}[$((i+1))]${NC} $(fmt_ip "${mon_ips[$i]}")"; done
|
||
read -p "Номер для удаления: " d_ch
|
||
local d_idx=$((d_ch - 1))
|
||
[ -n "${mon_ips[$d_idx]:-}" ] && remove_monitor "${mon_ips[$d_idx]}" && echo -e "${GREEN}[OK] Удалено.${NC}"
|
||
read -p "Enter..."
|
||
;;
|
||
0) return ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TELEGRAM BOT
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
tg_api() {
|
||
local method="$1" payload="$2"
|
||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/${method}" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$payload" 2>/dev/null
|
||
}
|
||
|
||
tg_send() {
|
||
local chat_id="$1" text keyboard="${3:-}"
|
||
text=$(printf '%b' "$2")
|
||
local payload
|
||
if [ -n "$keyboard" ]; then
|
||
payload=$(jq -n --arg c "$chat_id" --arg t "$text" --argjson k "$keyboard" \
|
||
'{chat_id:$c, text:$t, parse_mode:"HTML", reply_markup:{inline_keyboard:$k}}')
|
||
else
|
||
payload=$(jq -n --arg c "$chat_id" --arg t "$text" \
|
||
'{chat_id:$c, text:$t, parse_mode:"HTML"}')
|
||
fi
|
||
tg_api "sendMessage" "$payload"
|
||
}
|
||
|
||
tg_edit() {
|
||
local chat_id="$1" msg_id="$2" text keyboard="${4:-}"
|
||
text=$(printf '%b' "$3")
|
||
local payload
|
||
if [ -n "$keyboard" ]; then
|
||
payload=$(jq -n --arg c "$chat_id" --argjson m "$msg_id" --arg t "$text" --argjson k "$keyboard" \
|
||
'{chat_id:$c, message_id:$m, text:$t, parse_mode:"HTML", reply_markup:{inline_keyboard:$k}}')
|
||
else
|
||
payload=$(jq -n --arg c "$chat_id" --argjson m "$msg_id" --arg t "$text" \
|
||
'{chat_id:$c, message_id:$m, text:$t, parse_mode:"HTML"}')
|
||
fi
|
||
tg_api "editMessageText" "$payload"
|
||
}
|
||
|
||
tg_answer_cb() {
|
||
local cb_id="$1" text="${2:-}"
|
||
tg_api "answerCallbackQuery" "{\"callback_query_id\":\"$cb_id\",\"text\":\"$text\"}"
|
||
}
|
||
|
||
# ─── Bot state ────────────────────────────────────────────────
|
||
|
||
bot_set_state() {
|
||
local chat_id="$1"; shift
|
||
printf '%s\n' "$@" > "$BOT_STATE_DIR/$chat_id"
|
||
}
|
||
|
||
bot_get_state() {
|
||
local chat_id="$1" key="$2"
|
||
[ -f "$BOT_STATE_DIR/$chat_id" ] && grep "^${key}=" "$BOT_STATE_DIR/$chat_id" | head -1 | cut -d= -f2-
|
||
}
|
||
|
||
bot_clear_state() {
|
||
rm -f "$BOT_STATE_DIR/$1"
|
||
}
|
||
|
||
# ─── Bot keyboards ───────────────────────────────────────────
|
||
|
||
get_menu_style() {
|
||
source "$KASKAD_CONF" 2>/dev/null
|
||
echo "${MENU_STYLE:-compact}"
|
||
}
|
||
|
||
kbd_main() {
|
||
local style
|
||
style=$(get_menu_style)
|
||
if [ "$style" = "large" ]; then
|
||
cat <<'JSON'
|
||
[
|
||
[{"text":"🔀 AmneziaWG / WireGuard (UDP)","callback_data":"a_u"}],
|
||
[{"text":"🔀 VLESS / XRay (TCP)","callback_data":"a_t"}],
|
||
[{"text":"🔀 MTProto / TProxy (TCP)","callback_data":"a_mt"}],
|
||
[{"text":"🛠 Кастомное правило (TCP/UDP)","callback_data":"a_c"}],
|
||
[{"text":"📋 Активные правила","callback_data":"lr"}],
|
||
[{"text":"🏓 Ping серверов","callback_data":"pm"}],
|
||
[{"text":"📊 Мониторинг","callback_data":"mm"}],
|
||
[{"text":"💻 Состояние системы","callback_data":"sys"}],
|
||
[{"text":"❌ Удалить правило","callback_data":"dr"}],
|
||
[{"text":"🗑 Сбросить все правила","callback_data":"fa"}],
|
||
[{"text":"🏢 Хостинг, который работает","callback_data":"promo"}],
|
||
[{"text":"⚙️ Компактное меню","callback_data":"sw_compact"}]
|
||
]
|
||
JSON
|
||
else
|
||
cat <<'JSON'
|
||
[
|
||
[{"text":"🔀 AWG","callback_data":"a_u"},{"text":"🔀 VLESS","callback_data":"a_t"},{"text":"🔀 MTProto","callback_data":"a_mt"}],
|
||
[{"text":"🛠 Custom","callback_data":"a_c"},{"text":"📋 Правила","callback_data":"lr"}],
|
||
[{"text":"🏓 Ping","callback_data":"pm"},{"text":"📊 Монитор","callback_data":"mm"}],
|
||
[{"text":"💻 Система","callback_data":"sys"}],
|
||
[{"text":"❌ Удалить","callback_data":"dr"},{"text":"🗑 Сброс","callback_data":"fa"}],
|
||
[{"text":"🏢 Хостинг","callback_data":"promo"}],
|
||
[{"text":"⚙️ Большое меню","callback_data":"sw_large"}]
|
||
]
|
||
JSON
|
||
fi
|
||
}
|
||
|
||
kbd_back() {
|
||
echo '[[{"text":"⬅️ Меню","callback_data":"m"}]]'
|
||
}
|
||
|
||
kbd_proto() {
|
||
echo '[[{"text":"TCP","callback_data":"a_cp_tcp"},{"text":"UDP","callback_data":"a_cp_udp"}],[{"text":"⬅️ Меню","callback_data":"m"}]]'
|
||
}
|
||
|
||
kbd_ping_opts() {
|
||
local ip="$1"
|
||
jq -n --arg ip "$ip" '[
|
||
[{"text":"1 раз","callback_data":("po:" + $ip)}],
|
||
[{"text":"10 раз (среднее)","callback_data":("p10:" + $ip)}],
|
||
[{"text":"60 сек (непрерывно)","callback_data":("p60:" + $ip)}],
|
||
[{"text":"⬅️ Меню","callback_data":"m"}]
|
||
]'
|
||
}
|
||
|
||
kbd_monitor() {
|
||
cat <<'JSON'
|
||
[
|
||
[{"text":"➕ Добавить","callback_data":"ma"}],
|
||
[{"text":"📋 Список","callback_data":"ml"}],
|
||
[{"text":"➖ Удалить","callback_data":"md"}],
|
||
[{"text":"⬅️ Меню","callback_data":"m"}]
|
||
]
|
||
JSON
|
||
}
|
||
|
||
kbd_intervals() {
|
||
local ip="$1"
|
||
jq -n --arg ip "$ip" '[
|
||
[{"text":"10 сек","callback_data":("mi:" + $ip + ":10")}],
|
||
[{"text":"1 мин","callback_data":("mi:" + $ip + ":60")}],
|
||
[{"text":"5 мин","callback_data":("mi:" + $ip + ":300")}],
|
||
[{"text":"⬅️ Меню","callback_data":"m"}]
|
||
]'
|
||
}
|
||
|
||
kbd_cooldowns() {
|
||
local ip="$1" interval="$2" threshold="$3"
|
||
jq -n --arg ip "$ip" --arg int "$interval" --arg thr "$threshold" '[
|
||
[{"text":"10 сек","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":10")}],
|
||
[{"text":"60 сек","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":60")}],
|
||
[{"text":"5 мин","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":300")}],
|
||
[{"text":"15 мин","callback_data":("mc:" + $ip + ":" + $int + ":" + $thr + ":900")}],
|
||
[{"text":"⬅️ Меню","callback_data":"m"}]
|
||
]'
|
||
}
|
||
|
||
build_ip_kbd() {
|
||
local prefix="$1"; shift
|
||
local ips=("$@")
|
||
local rows="" first=1
|
||
for ip in "${ips[@]}"; do
|
||
local label
|
||
label=$(get_alias "$ip")
|
||
[ -z "$label" ] && label="$ip" || label="$label ($ip)"
|
||
[ "$first" -eq 0 ] && rows+=","
|
||
rows+="[{\"text\":\"$label\",\"callback_data\":\"${prefix}:${ip}\"}]"
|
||
first=0
|
||
done
|
||
echo "[${rows},[{\"text\":\"⬅️ Меню\",\"callback_data\":\"m\"}]]"
|
||
}
|
||
|
||
build_delete_kbd() {
|
||
local rules
|
||
rules=$(get_rules_list)
|
||
local rows="" i=1 first=1
|
||
while IFS='|' read -r proto port dest; do
|
||
[ -z "$port" ] && continue
|
||
local dest_ip="${dest%:*}"
|
||
local label
|
||
label=$(get_alias "$dest_ip")
|
||
local btn_text="❌ ${MY_IP:-*}:$port ($proto) → $dest"
|
||
[ -n "$label" ] && btn_text="❌ :$port → $label"
|
||
[ "$first" -eq 0 ] && rows+=","
|
||
rows+="[{\"text\":\"$btn_text\",\"callback_data\":\"dr_${i}\"}]"
|
||
first=0
|
||
((i++))
|
||
done <<< "$rules"
|
||
if [ -z "$rows" ]; then
|
||
echo '[[{"text":"Нет правил","callback_data":"m"}]]'
|
||
else
|
||
echo "[${rows},[{\"text\":\"⬅️ Меню\",\"callback_data\":\"m\"}]]"
|
||
fi
|
||
}
|
||
|
||
# ─── Bot handlers ─────────────────────────────────────────────
|
||
|
||
bot_main_menu() {
|
||
local chat_id="$1" msg_id="${2:-}"
|
||
bot_clear_state "$chat_id"
|
||
local text="<b>Kaskad PRO v${KASKAD_VERSION}</b>\nIP: <code>${MY_IP:-N/A}</code>\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" "🔀 <b>AmneziaWG / WireGuard (UDP)</b>\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" "🔀 <b>VLESS / XRay (TCP)</b>\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" "🔀 <b>MTProto / TProxy (TCP)</b>\n\nВведите IP адрес целевого сервера:" "$(kbd_back)"
|
||
;;
|
||
a_c)
|
||
tg_edit "$chat_id" "$msg_id" "🛠 <b>Custom Rule</b>\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" "🛠 <b>Custom Rule ($proto)</b>\n\nВведите IP адрес целевого сервера:" "$(kbd_back)"
|
||
;;
|
||
|
||
# ── List rules ──
|
||
lr)
|
||
local rules text=""
|
||
rules=$(get_rules_list)
|
||
if [ -z "$rules" ]; then
|
||
text="📋 <b>Нет активных правил.</b>"
|
||
else
|
||
text="📋 <b>Активные правила:</b>\n<b>Сервер:</b> <code>${MY_IP:-N/A}</code>\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="<code>${MY_IP:-*}:$port ($proto) → $dest</code>"
|
||
[ -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" "❌ <b>Выберите правило для удаления:</b>" "$(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" "✅ Правило <code>$d_proto :$d_port → $d_dest</code> удалено." "$(kbd_back)"
|
||
else
|
||
tg_edit "$chat_id" "$msg_id" "Правило не найдено." "$(kbd_back)"
|
||
fi
|
||
;;
|
||
|
||
# ── Flush ──
|
||
fa)
|
||
tg_edit "$chat_id" "$msg_id" "🗑 <b>Вы уверены?</b>\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+="<b>🏢 Хостинг, который работает</b>\n\n"
|
||
promo_text+="<b>🌍 Локации: РФ и Европа</b>\n"
|
||
promo_text+="👉 https://vk.cc/ct29NQ\n\n"
|
||
promo_text+="<code>OFF60</code> — 60% скидка на 1-й месяц\n"
|
||
promo_text+="<code>antenka20</code> — +20% к балансу (3 мес)\n"
|
||
promo_text+="<code>antenka6</code> — +15% к балансу (6 мес)\n"
|
||
promo_text+="<code>antenka12</code> — +5% к балансу (12 мес)\n\n"
|
||
promo_text+="<b>🇧🇾 Локация: Беларусь</b>\n"
|
||
promo_text+="👉 https://vk.cc/cUxAhj\n\n"
|
||
promo_text+="<code>OFF60</code> — 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" "🏓 <b>Выберите сервер:</b>" "$(build_ip_kbd "ps" "${ips[@]}")"
|
||
fi
|
||
;;
|
||
ps:*)
|
||
local ip="${data#ps:}"
|
||
local label
|
||
label=$(fmt_ip "$ip")
|
||
tg_edit "$chat_id" "$msg_id" "🏓 <b>Ping $label</b>\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" "🏓 <b>$label</b>\nОтвет: <code>${ms} ms</code>" "$(kbd_back)" > /dev/null
|
||
else
|
||
tg_send "$chat_id" "🏓 <b>$label</b>\nОтвет: <code>timeout</code>" "$(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="🏓 <b>Ping $label (10 раз)</b>\n${text}"
|
||
if [ ${#results[@]} -gt 0 ]; then
|
||
local avg
|
||
avg=$(printf '%s\n' "${results[@]}" | awk '{s+=$1} END {printf "%.2f", s/NR}')
|
||
summary+="\n<b>Среднее: ${avg}ms</b>"
|
||
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="🏓 <b>$label</b>: ${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="🏓 <b>$label (60 сек) — завершён</b>\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<mn)mn=$1;if($1>mx)mx=$1}
|
||
END{printf "%.2f|%.2f|%.2f",mn,mx,s/NR}')
|
||
IFS='|' read -r s_min s_max s_avg <<< "$stats"
|
||
summary+="Мин: ${s_min}ms\nМакс: ${s_max}ms\nСреднее: ${s_avg}ms\n"
|
||
fi
|
||
summary+="Потеряно: $lost / 60"
|
||
|
||
if [ -n "$mid" ]; then
|
||
tg_edit "$chat_id" "$mid" "$summary" "$(kbd_back)" > /dev/null
|
||
else
|
||
tg_send "$chat_id" "$summary" "$(kbd_back)" > /dev/null
|
||
fi
|
||
) &
|
||
;;
|
||
|
||
# ── Monitoring ──
|
||
mm)
|
||
tg_edit "$chat_id" "$msg_id" "📊 <b>Мониторинг</b>\nВыберите действие:" "$(kbd_monitor)"
|
||
;;
|
||
ma)
|
||
local -a ips=()
|
||
while read -r ip; do [ -n "$ip" ] && ips+=("$ip"); done <<< "$(get_target_ips)"
|
||
if [ ${#ips[@]} -eq 0 ]; then
|
||
tg_edit "$chat_id" "$msg_id" "Нет целевых серверов." "$(kbd_back)"
|
||
else
|
||
tg_edit "$chat_id" "$msg_id" "📊 Выберите сервер для мониторинга:" "$(build_ip_kbd "ma" "${ips[@]}")"
|
||
fi
|
||
;;
|
||
ma:*)
|
||
local ip="${data#ma:}"
|
||
local label
|
||
label=$(fmt_ip "$ip")
|
||
tg_edit "$chat_id" "$msg_id" "📊 <b>Мониторинг $label</b>\nВыберите интервал проверки:" "$(kbd_intervals "$ip")"
|
||
;;
|
||
mi:*)
|
||
local rest="${data#mi:}"
|
||
local ip="${rest%:*}"
|
||
local interval="${rest##*:}"
|
||
bot_set_state "$chat_id" "STATE=awaiting_threshold" "MON_IP=$ip" "MON_INTERVAL=$interval"
|
||
local label
|
||
label=$(fmt_ip "$ip")
|
||
tg_edit "$chat_id" "$msg_id" "📊 <b>$label</b> (каждые ${interval}с)\n\nВведите порог уведомления (мс):" "$(kbd_back)"
|
||
;;
|
||
mc:*)
|
||
local rest="${data#mc:}"
|
||
IFS=':' read -r ip interval threshold cooldown <<< "$rest"
|
||
add_monitor "$ip" "$interval" "$threshold" "$cooldown"
|
||
local label
|
||
label=$(fmt_ip "$ip")
|
||
tg_edit "$chat_id" "$msg_id" "✅ Мониторинг для <b>$label</b> добавлен.\nИнтервал: ${interval}с | Порог: ${threshold}мс | Уведомл: ${cooldown}с" "$(kbd_back)"
|
||
;;
|
||
ml)
|
||
local text="📊 <b>Активные мониторы:</b>\n"
|
||
local found=0
|
||
for conf in "$MONITOR_DIR"/*.conf; do
|
||
[ -f "$conf" ] || continue
|
||
found=1
|
||
local MON_IP="" MON_INTERVAL="" MON_THRESHOLD="" MON_COOLDOWN=300
|
||
source "$conf"
|
||
local label
|
||
label=$(fmt_ip "$MON_IP")
|
||
text+="<b>$label</b>\n инт: ${MON_INTERVAL}с | порог: ${MON_THRESHOLD}мс | уведомл: ${MON_COOLDOWN}с\n"
|
||
done
|
||
[ "$found" -eq 0 ] && text+="<i>Нет настроенных мониторов.</i>"
|
||
tg_edit "$chat_id" "$msg_id" "$text" "$(kbd_monitor)"
|
||
;;
|
||
md)
|
||
local -a mon_ips=()
|
||
for conf in "$MONITOR_DIR"/*.conf; do
|
||
[ -f "$conf" ] || continue
|
||
local MON_IP=""
|
||
source "$conf"
|
||
mon_ips+=("$MON_IP")
|
||
done
|
||
if [ ${#mon_ips[@]} -eq 0 ]; then
|
||
tg_edit "$chat_id" "$msg_id" "Нет мониторов для удаления." "$(kbd_monitor)"
|
||
else
|
||
tg_edit "$chat_id" "$msg_id" "📊 Выберите монитор для удаления:" "$(build_ip_kbd "md" "${mon_ips[@]}")"
|
||
fi
|
||
;;
|
||
md:*)
|
||
local ip="${data#md:}"
|
||
remove_monitor "$ip"
|
||
local label
|
||
label=$(fmt_ip "$ip")
|
||
tg_edit "$chat_id" "$msg_id" "✅ Мониторинг для <b>$label</b> удалён." "$(kbd_monitor)"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
bot_handle_message() {
|
||
local chat_id="$1" text="$2"
|
||
|
||
if [ "$text" = "/start" ] || [ "$text" = "/menu" ]; then
|
||
bot_main_menu "$chat_id"
|
||
return
|
||
fi
|
||
|
||
local state
|
||
state=$(bot_get_state "$chat_id" "STATE")
|
||
|
||
case "$state" in
|
||
awaiting_ip)
|
||
if ! validate_ip "$text"; then
|
||
tg_send "$chat_id" "❌ Некорректный IP-адрес. Попробуйте ещё раз:" "$(kbd_back)" > /dev/null
|
||
return
|
||
fi
|
||
local proto name custom
|
||
proto=$(bot_get_state "$chat_id" "PROTO")
|
||
name=$(bot_get_state "$chat_id" "NAME")
|
||
custom=$(bot_get_state "$chat_id" "CUSTOM")
|
||
bot_set_state "$chat_id" "STATE=awaiting_name" "PROTO=$proto" "NAME=$name" "CUSTOM=$custom" "TARGET_IP=$text"
|
||
tg_send "$chat_id" "IP: <code>$text</code> ✅\n\nВведите имя сервера (или <code>-</code> чтобы пропустить):" "$(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" "Сервер: <b>$(fmt_ip "$target_ip")</b>\n\nВведите <b>ВХОДЯЩИЙ</b> порт:" "$(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" "Сервер: <b>$(fmt_ip "$target_ip")</b>\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" "✅ <b>$name настроен!</b>\n<code>$proto ${MY_IP:-*}:$text → $target_ip:$text</code>\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" "Входящий порт: <code>$text</code> ✅\n\nВведите <b>ИСХОДЯЩИЙ</b> порт:" "$(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" "✅ <b>Custom Rule настроен!</b>\n<code>$proto ${MY_IP:-*}:$in_port → $target_ip:$text</code>\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" "📊 <b>$label</b>\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<count; i++ )); do
|
||
local update
|
||
update=$(echo "$response" | jq ".result[$i]")
|
||
local update_id
|
||
update_id=$(echo "$update" | jq -r '.update_id')
|
||
offset=$((update_id + 1))
|
||
|
||
local cb_data cb_id cb_chat_id cb_msg_id
|
||
cb_data=$(echo "$update" | jq -r '.callback_query.data // empty')
|
||
|
||
if [ -n "$cb_data" ]; then
|
||
cb_id=$(echo "$update" | jq -r '.callback_query.id')
|
||
cb_chat_id=$(echo "$update" | jq -r '.callback_query.message.chat.id')
|
||
cb_msg_id=$(echo "$update" | jq -r '.callback_query.message.message_id')
|
||
|
||
if [ -n "$BOT_CHAT_ID" ] && [ "$cb_chat_id" != "$BOT_CHAT_ID" ]; then
|
||
tg_answer_cb "$cb_id" "Unauthorized" > /dev/null
|
||
continue
|
||
fi
|
||
|
||
bot_handle_callback "$cb_chat_id" "$cb_msg_id" "$cb_id" "$cb_data"
|
||
else
|
||
local msg_chat_id msg_text
|
||
msg_chat_id=$(echo "$update" | jq -r '.message.chat.id // empty')
|
||
msg_text=$(echo "$update" | jq -r '.message.text // empty')
|
||
|
||
if [ -n "$msg_chat_id" ] && [ -n "$msg_text" ]; then
|
||
if [ -n "$BOT_CHAT_ID" ] && [ "$msg_chat_id" != "$BOT_CHAT_ID" ]; then
|
||
tg_send "$msg_chat_id" "⛔ Нет доступа.\nВаш Chat ID: <code>$msg_chat_id</code>" "" > /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 <<EOF
|
||
[Unit]
|
||
Description=Kaskad Telegram Bot
|
||
After=network.target
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=/usr/local/bin/gokaskad --bot-daemon
|
||
Restart=always
|
||
RestartSec=5
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
systemctl daemon-reload
|
||
systemctl enable kaskad-bot > /dev/null 2>&1
|
||
systemctl start kaskad-bot
|
||
sleep 1
|
||
if systemctl is-active kaskad-bot &>/dev/null; then
|
||
echo -e "${GREEN}[OK] Бот запущен.${NC}"
|
||
log_action "Bot service started"
|
||
else
|
||
echo -e "${RED}[ERROR] Проверьте: journalctl -u kaskad-bot${NC}"
|
||
fi
|
||
}
|
||
|
||
stop_bot() {
|
||
systemctl stop kaskad-bot 2>/dev/null
|
||
systemctl disable kaskad-bot 2>/dev/null
|
||
rm -f "$BOT_PID_FILE"
|
||
echo -e "${GREEN}[OK] Бот остановлен.${NC}"
|
||
log_action "Bot service stopped"
|
||
}
|
||
|
||
bot_menu() {
|
||
while true; do
|
||
clear
|
||
source "$KASKAD_CONF" 2>/dev/null
|
||
local bot_status="${RED}Остановлен${NC}"
|
||
if [ -f "$BOT_PID_FILE" ] && kill -0 "$(cat "$BOT_PID_FILE" 2>/dev/null)" 2>/dev/null; then
|
||
bot_status="${GREEN}Работает (PID $(cat "$BOT_PID_FILE"))${NC}"
|
||
fi
|
||
local token_display="не задан"
|
||
[ -n "${BOT_TOKEN:-}" ] && token_display="***${BOT_TOKEN: -6}"
|
||
local ut_val
|
||
ut_val=$(bot_get_state "system" "UPDATE_TOKEN" 2>/dev/null)
|
||
local ut_display="не задан"
|
||
[ -n "$ut_val" ] && ut_display="***${ut_val: -6}"
|
||
|
||
echo -e "${CYAN}━━━ Telegram Bot ━━━${NC}"
|
||
echo -e "Статус: $bot_status"
|
||
echo -e "Токен: ${YELLOW}$token_display${NC}"
|
||
echo -e "Chat ID: ${YELLOW}${BOT_CHAT_ID:-не задан}${NC}"
|
||
echo -e "Update Token: ${YELLOW}$ut_display${NC}"
|
||
echo -e "Меню: ${YELLOW}${MENU_STYLE:-compact}${NC}"
|
||
echo ""
|
||
echo -e "1) Установить токен бота"
|
||
echo -e "2) Получить Chat ID (авто)"
|
||
echo -e "3) Установить Chat ID вручную"
|
||
echo -e "4) ${GREEN}Запустить бота${NC}"
|
||
echo -e "5) ${RED}Остановить бота${NC}"
|
||
echo -e "6) Токен обновления (GitHub PAT)"
|
||
echo -e "0) Назад"
|
||
read -p "Выбор: " choice
|
||
|
||
case $choice in
|
||
1) echo "Введите токен (от @BotFather):"; read -p "> " t; [ -n "$t" ] && save_config_val "BOT_TOKEN" "$t" && echo -e "${GREEN}OK${NC}"; read -p "Enter..." ;;
|
||
2)
|
||
[ -z "${BOT_TOKEN:-}" ] && echo -e "${RED}Сначала токен!${NC}" && read -p "Enter..." && continue
|
||
echo -e "${YELLOW}Отправьте боту сообщение в Telegram, затем Enter.${NC}"; read -p ""
|
||
local cid; cid=$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?limit=1&offset=-1" | jq -r '.result[0].message.chat.id // empty')
|
||
[ -n "$cid" ] && save_config_val "BOT_CHAT_ID" "$cid" && echo -e "${GREEN}Chat ID: $cid${NC}" || echo -e "${RED}Не удалось.${NC}"
|
||
read -p "Enter..." ;;
|
||
3) echo "Chat ID:"; read -p "> " c; [ -n "$c" ] && save_config_val "BOT_CHAT_ID" "$c" && echo -e "${GREEN}OK${NC}"; read -p "Enter..." ;;
|
||
4) start_bot; read -p "Enter..." ;;
|
||
5) stop_bot; read -p "Enter..." ;;
|
||
6) echo "GitHub PAT:"; read -p "> " u; [ -n "$u" ] && bot_set_state "system" "UPDATE_TOKEN=$u" && echo -e "${GREEN}OK${NC}"; read -p "Enter..." ;;
|
||
0) return ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# PROMO & INSTRUCTIONS (kept from previous version)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
show_promo() {
|
||
clear
|
||
echo ""
|
||
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${MAGENTA}║ ХОСТИНГ, КОТОРЫЙ РАБОТАЕТ СО СКИДКОЙ ДО -60% ║${NC}"
|
||
echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}🌍 ЛОКАЦИИ: РФ И ЕВРОПА${NC}"
|
||
echo -e "${WHITE} >>> https://vk.cc/ct29NQ${NC}"
|
||
printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "OFF60" "60% скидка на первый месяц"
|
||
printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "antenka20" "Буст 20% + 3% (при оплате за 3 мес)"
|
||
printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "antenka6" "Буст 15% + 5% (при оплате за 6 мес)"
|
||
printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "antenka12" "Буст 5% + 5% (при оплате за 12 мес)"
|
||
echo -e "\n${CYAN}🇧🇾 ЛОКАЦИЯ: БЕЛАРУСЬ${NC}"
|
||
echo -e "${WHITE} >>> https://vk.cc/cUxAhj${NC}"
|
||
printf " ${YELLOW}%-12s${NC} : ${WHITE}%s${NC}\n" "OFF60" "60% скидка на первый месяц"
|
||
echo ""
|
||
echo -e "\n${YELLOW}Генерация QR-кода... (3 сек)${NC}"
|
||
for i in {3..1}; do echo -ne "$i..."; sleep 1; done; echo ""
|
||
echo -e "\n${WHITE}"
|
||
command -v qrencode &>/dev/null && qrencode -t ANSIUTF8 "https://vk.cc/ct29NQ" || echo "Используйте ссылки выше."
|
||
echo -e "${NC}"
|
||
echo -e "${GREEN}Сканируйте камерой телефона!${NC}"
|
||
echo ""
|
||
read -p "Нажмите Enter..."
|
||
}
|
||
|
||
show_instructions() {
|
||
local page=1 total_pages=7
|
||
while true; do
|
||
clear
|
||
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${MAGENTA}║ 📚 ИНСТРУКЦИЯ KASKAD PRO v${KASKAD_VERSION} (стр. ${page}/${total_pages}) ║${NC}"
|
||
echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
case $page in
|
||
1) echo -e "${CYAN}═══ ЧТО ТАКОЕ КАСКАД? ═══${NC}\n\nКаскад — 'мост' между устройством и зарубежным VPN/Proxy.\n\n ${WHITE}Клиент${NC} → ${GREEN}Этот сервер (РФ)${NC} → ${CYAN}Зарубежный VPN${NC} → Интернет\n\nПровайдер видит только подключение к серверу в РФ.\n\n${CYAN}═══ ЧТО НУЖНО ═══${NC}\n 1. VPS в РФ\n 2. Зарубежный VPN/Proxy\n 3. IP и порт зарубежного сервера" ;;
|
||
2) echo -e "${CYAN}═══ ПУНКТ 1: AWG/WG (UDP) ═══${NC}\n\n Меню → 1 → IP: 45.10.20.30 → Порт: 51820\n В клиенте Endpoint: ${MY_IP:-185.1.2.3}:51820\n\n${CYAN}═══ ПУНКТ 2: VLESS (TCP) ═══${NC}\n\n Меню → 2 → IP: 67.89.100.200 → Порт: 443\n В v2rayNG/NekoBox замените адрес сервера" ;;
|
||
3) echo -e "${CYAN}═══ ПУНКТ 3: MTProto ═══${NC}\n\n Меню → 3 → IP → Порт: 8443\n Telegram → Прокси → ${MY_IP:-*}:8443\n\n${CYAN}═══ ПУНКТ 4: Custom ═══${NC}\n\n Разные порты входа/выхода, SSH, RDP\n Пример SSH: tcp, IP, вход: 2222, выход: 22\n Подключение: ssh user@${MY_IP:-*} -p 2222" ;;
|
||
4) echo -e "${CYAN}═══ ПРАВИЛА И PING ═══${NC}\n\nПункт 5 — таблица правил с IP каскада и именами серверов\nПункт 6 — Live Ping (1 сек обновление, Ctrl+C стоп)\nПункт 14 — управление именами серверов" ;;
|
||
5) echo -e "${CYAN}═══ МОНИТОРИНГ ═══${NC}\n\nАвтопроверка серверов с Telegram-алертами.\n\n Меню → 7 → Добавить → сервер → интервал → порог → частота уведомл.\n\nСлужба запускается/останавливается автоматически.\nЧастота уведомлений: 10с / 60с / 5мин / 15мин" ;;
|
||
6) echo -e "${CYAN}═══ TELEGRAM BOT ═══${NC}\n\n Шаг A. @BotFather → /newbot → получить токен\n Шаг B. Меню → 8 → 1 → вставить токен\n Шаг C. Меню → 8 → 2 → отправить боту сообщение → Enter\n Шаг D. Меню → 8 → 4 → Запустить\n Шаг E. В Telegram: /start\n\nБот имеет 2 стиля меню: компактный и большой (кнопка внизу)" ;;
|
||
7) echo -e "${CYAN}═══ ВОЗМОЖНОСТИ БОТА ═══${NC}\n\n Добавление/удаление правил через кнопки\n Ping: 1x / 10x (среднее) / 60 сек\n Мониторинг: добавить/удалить/список\n 💻 Система: CPU, RAM, Swap, диск, топ процессов\n 🏢 Хостинг: промокоды партнёров\n Имена серверов отображаются везде\n\n Пункты 9-13: удаление, сброс, обновление, промо, инструкция" ;;
|
||
esac
|
||
echo -e "\n${MAGENTA}──────────────────────────────────────────────────────────────${NC}"
|
||
if [ "$page" -eq 1 ]; then echo -e " ${YELLOW}[N]${NC} Далее ${YELLOW}[0]${NC} Выход"
|
||
elif [ "$page" -eq "$total_pages" ]; then echo -e " ${YELLOW}[P]${NC} Назад ${YELLOW}[0]${NC} Выход"
|
||
else echo -e " ${YELLOW}[P]${NC} Назад ${YELLOW}[N]${NC} Далее ${YELLOW}[0]${NC} Выход"; fi
|
||
read -p " > " nav
|
||
case "$nav" in
|
||
[nN]) (( page < total_pages )) && ((page++)) ;; [pP]) (( page > 1 )) && ((page--)) ;; 0) return ;; [1-7]) page="$nav" ;; esac
|
||
done
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# MAIN MENU
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
show_menu() {
|
||
while true; do
|
||
clear
|
||
echo -e "${MAGENTA}"
|
||
echo "******************************************************"
|
||
echo " anten-ka канал представляет... Kaskad PRO v${KASKAD_VERSION}"
|
||
echo " YouTube: https://www.youtube.com/@antenkaru"
|
||
echo "******************************************************"
|
||
echo -e "${NC}"
|
||
echo -e "${WHITE}IP сервера: ${GREEN}${MY_IP}${NC} ${WHITE}Интерфейс: ${CYAN}${IFACE}${NC}"
|
||
echo -e "${YELLOW}Инструкции:${NC} ${BLUE}https://boosty.to/anten-ka${NC}"
|
||
echo -e "${GREEN}Донат:${NC} https://pay.cloudtips.ru/p/7410814f"
|
||
echo -e "------------------------------------------------------"
|
||
echo -e " 1) Настроить ${CYAN}AmneziaWG / WireGuard${NC} (UDP)"
|
||
echo -e " 2) Настроить ${CYAN}VLESS / XRay${NC} (TCP)"
|
||
echo -e " 3) Настроить ${CYAN}TProxy / MTProto${NC} (TCP)"
|
||
echo -e " 4) 🛠 Создать ${YELLOW}Кастомное правило${NC}"
|
||
echo -e " 5) 📋 Активные правила"
|
||
echo -e " 6) 🏓 ${CYAN}Ping сервера (live)${NC}"
|
||
echo -e " 7) 📊 ${CYAN}Мониторинг${NC}"
|
||
echo -e " 8) 🤖 ${CYAN}Telegram Bot${NC}"
|
||
echo -e " 9) ${RED}Удалить одно правило${NC}"
|
||
echo -e "10) ${RED}Сбросить правила Kaskad${NC}"
|
||
echo -e "11) ${YELLOW}Обновить скрипт${NC}"
|
||
echo -e "12) ${YELLOW}PROMO${NC}"
|
||
echo -e "13) ${MAGENTA}📚 Инструкция${NC}"
|
||
echo -e "14) ${WHITE}Имена серверов${NC}"
|
||
echo -e " 0) Выход"
|
||
echo -e "------------------------------------------------------"
|
||
read -p "Ваш выбор: " choice
|
||
|
||
case $choice in
|
||
1) configure_rule "udp" "AmneziaWG" ;;
|
||
2) configure_rule "tcp" "VLESS" ;;
|
||
3) configure_rule "tcp" "MTProto/TProxy" ;;
|
||
4) configure_custom_rule ;;
|
||
5) list_active_rules ;;
|
||
6) ping_menu ;;
|
||
7) monitoring_menu ;;
|
||
8) bot_menu ;;
|
||
9) delete_single_rule ;;
|
||
10) flush_rules ;;
|
||
11) self_update ;;
|
||
12) show_promo ;;
|
||
13) show_instructions ;;
|
||
14) manage_aliases_menu ;;
|
||
0) exit 0 ;;
|
||
*) ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# ENTRY POINT
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
case "${1:-}" in
|
||
--bot-daemon)
|
||
init_config
|
||
bot_daemon
|
||
;;
|
||
--monitor-daemon)
|
||
init_config
|
||
monitor_daemon
|
||
;;
|
||
*)
|
||
check_root
|
||
init_config
|
||
prepare_system
|
||
detect_interface
|
||
get_my_ip
|
||
show_promo
|
||
show_menu
|
||
;;
|
||
esac
|