Files
cascad-Auto-deployment/cascad.sh
T

1036 lines
38 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#
# cascad — скрипт для настройки каскадных NAT-соединений
# Поддержка: iptables, IPv4/IPv6, диапазоны портов, бэкап, логирование
#
set -euo pipefail
# ── Цвета ───────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
DIM='\033[2m'
NC='\033[0m'
# ── Константы ───────────────────────────────────────────────────────────────────
VERSION="1.0.0"
SCRIPT_NAME="cascad"
GLOBAL_CMD="/usr/local/bin/$SCRIPT_NAME"
LOG_FILE="/var/log/${SCRIPT_NAME}.log"
BACKUP_DIR="/etc/${SCRIPT_NAME}/backups"
CONFIG_DIR="/etc/${SCRIPT_NAME}"
REPO_URL="https://raw.githubusercontent.com/arblark/cascad/main/cascad.sh"
# ── Утилиты ─────────────────────────────────────────────────────────────────────
log() {
local level="$1"; shift
local msg="$*"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$ts] [$level] $msg" >> "$LOG_FILE"
}
msg_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
msg_err() { echo -e "${RED}[ОШИБКА]${NC} $*"; }
msg_warn() { echo -e "${YELLOW}[!]${NC} $*"; }
msg_info() { echo -e "${CYAN}[i]${NC} $*"; }
confirm() {
local prompt="${1:-Продолжить?}"
echo -ne "${YELLOW}${prompt} (y/n): ${NC}"
read -r ans
[[ "$ans" =~ ^[yYдД]$ ]]
}
# ── Проверки ────────────────────────────────────────────────────────────────────
check_root() {
if [[ "$EUID" -ne 0 ]]; then
msg_err "Запустите скрипт с правами root (sudo)."
exit 1
fi
}
detect_firewall_backend() {
if command -v nft &>/dev/null && nft list ruleset &>/dev/null 2>&1; then
echo "nftables"
elif command -v iptables &>/dev/null; then
echo "iptables"
else
msg_err "Не найден ни iptables, ни nftables!"
exit 1
fi
}
detect_interface() {
local iface
iface=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [[ -z "$iface" ]]; then
iface=$(ip -4 route show default | awk '{print $5}' | head -1)
fi
if [[ -z "$iface" ]]; then
msg_err "Не удалось определить сетевой интерфейс."
exit 1
fi
echo "$iface"
}
validate_ip() {
local ip="$1"
# IPv4
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
local IFS='.'
read -ra octets <<< "$ip"
for o in "${octets[@]}"; do
(( o <= 255 )) || return 1
done
return 0
fi
# IPv6 (простая проверка)
if [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$ ]] || [[ "$ip" == *"::"* ]]; then
return 0
fi
return 1
}
validate_port() {
local port="$1"
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 ))
}
validate_port_range() {
local range="$1"
if [[ "$range" =~ ^([0-9]+):([0-9]+)$ ]]; then
local start="${BASH_REMATCH[1]}"
local end="${BASH_REMATCH[2]}"
validate_port "$start" && validate_port "$end" && (( start <= end ))
elif validate_port "$range"; then
return 0
else
return 1
fi
}
is_ipv6() {
[[ "$1" == *":"* ]]
}
# ── Подготовка системы ──────────────────────────────────────────────────────────
prepare_system() {
mkdir -p "$BACKUP_DIR" "$CONFIG_DIR"
touch "$LOG_FILE"
if [[ ! -f "$GLOBAL_CMD" ]] || [[ "$(readlink -f "$0" 2>/dev/null)" != "$GLOBAL_CMD" ]]; then
if [[ -f "$0" && "$0" != "/dev/"* && "$0" != "/proc/"* ]]; then
cp -f "$0" "$GLOBAL_CMD"
else
msg_info "Скачивание скрипта..."
curl -fsSL "$REPO_URL" -o "$GLOBAL_CMD" || {
msg_err "Не удалось скачать скрипт. Проверьте подключение к интернету."
exit 1
}
fi
chmod +x "$GLOBAL_CMD"
msg_info "Команда '$SCRIPT_NAME' установлена глобально."
log INFO "Скрипт установлен в $GLOBAL_CMD"
fi
local changed=0
# IPv4 forwarding
if ! sysctl -n net.ipv4.ip_forward 2>/dev/null | grep -q "1"; then
sysctl -w net.ipv4.ip_forward=1 >/dev/null
changed=1
fi
grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null || {
sed -i 's/^#\?net.ipv4.ip_forward=.*/net.ipv4.ip_forward=1/' /etc/sysctl.conf 2>/dev/null \
|| echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
changed=1
}
# IPv6 forwarding
if ! sysctl -n net.ipv6.conf.all.forwarding 2>/dev/null | grep -q "1"; then
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
fi
grep -q "^net.ipv6.conf.all.forwarding=1" /etc/sysctl.conf 2>/dev/null || {
echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
}
# BBR
if sysctl net.ipv4.tcp_available_congestion_control 2>/dev/null | grep -q bbr; then
grep -q "^net.core.default_qdisc=fq" /etc/sysctl.conf 2>/dev/null || {
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
changed=1
}
grep -q "^net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf 2>/dev/null || {
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
changed=1
}
fi
if (( changed )); then
sysctl -p >/dev/null 2>&1
fi
# Зависимости
export DEBIAN_FRONTEND=noninteractive
local deps_needed=()
command -v iptables &>/dev/null || deps_needed+=(iptables)
dpkg -s iptables-persistent &>/dev/null 2>&1 || deps_needed+=(iptables-persistent netfilter-persistent)
if (( ${#deps_needed[@]} > 0 )); then
msg_info "Установка зависимостей: ${deps_needed[*]}..."
apt-get update -y -qq >/dev/null 2>&1
apt-get install -y -qq "${deps_needed[@]}" >/dev/null 2>&1
fi
log INFO "Система подготовлена."
}
# ── Работа с правилами (iptables) ──────────────────────────────────────────────
ipt_cmd() {
local ip="$1"; shift
if is_ipv6 "$ip"; then
ip6tables "$@"
else
iptables "$@"
fi
}
rule_exists() {
local target_ip="$1" proto="$2" in_port="$3" out_port="$4"
if is_ipv6 "$target_ip"; then
ip6tables -t nat -C PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "[$target_ip]:$out_port" 2>/dev/null
else
iptables -t nat -C PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$target_ip:$out_port" 2>/dev/null
fi
}
apply_rules() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4" label="$5"
local iface
iface=$(detect_interface)
if rule_exists "$target_ip" "$proto" "$in_port" "$out_port"; then
msg_warn "Правило уже существует: $proto $in_port -> $target_ip:$out_port"
log WARN "Дубликат правила: $proto $in_port -> $target_ip:$out_port"
read -p "Нажмите Enter..."
return
fi
local dest_fmt="$target_ip:$out_port"
local fwd_dest="$target_ip"
if is_ipv6 "$target_ip"; then
dest_fmt="[$target_ip]:$out_port"
fwd_dest="$target_ip"
fi
msg_info "Применение правил ($label)..."
ipt_cmd "$target_ip" -A INPUT -p "$proto" --dport "$in_port" -j ACCEPT
ipt_cmd "$target_ip" -t nat -A PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt"
if ! ipt_cmd "$target_ip" -t nat -C POSTROUTING -o "$iface" -j MASQUERADE 2>/dev/null; then
ipt_cmd "$target_ip" -t nat -A POSTROUTING -o "$iface" -j MASQUERADE
fi
ipt_cmd "$target_ip" -A FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
ipt_cmd "$target_ip" -A FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
# UFW
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
sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
fi
netfilter-persistent save >/dev/null 2>&1
msg_ok "$label настроен: $proto вход $in_port -> $target_ip:$out_port"
log INFO "Создано: $label | $proto $in_port -> $target_ip:$out_port | iface=$iface"
read -p "Нажмите Enter..."
}
remove_rule() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4"
local dest_fmt="$target_ip:$out_port"
local fwd_dest="$target_ip"
if is_ipv6 "$target_ip"; then
dest_fmt="[$target_ip]:$out_port"
fi
ipt_cmd "$target_ip" -t nat -D PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt" 2>/dev/null
ipt_cmd "$target_ip" -D INPUT -p "$proto" --dport "$in_port" -j ACCEPT 2>/dev/null
ipt_cmd "$target_ip" -D FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT 2>/dev/null
ipt_cmd "$target_ip" -D FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \
-m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null
# UFW
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
ufw delete allow "$in_port/$proto" >/dev/null 2>&1
fi
netfilter-persistent save >/dev/null 2>&1
log INFO "Удалено: $proto $in_port -> $target_ip:$out_port"
}
# ── Парсер активных правил ──────────────────────────────────────────────────────
declare -a PARSED_RULES=()
parse_active_rules() {
PARSED_RULES=()
local line port proto dest
while read -r line; do
port=$(echo "$line" | grep -oP '(?<=--dport )\S+')
proto=$(echo "$line" | grep -oP '(?<=-p )\w+')
dest=$(echo "$line" | grep -oP '(?<=--to-destination )\S+')
if [[ -n "$port" && -n "$proto" && -n "$dest" ]]; then
PARSED_RULES+=("$port|$proto|$dest")
fi
done < <(iptables -t nat -S PREROUTING 2>/dev/null | grep "DNAT")
# IPv6
while read -r line; do
port=$(echo "$line" | grep -oP '(?<=--dport )\S+')
proto=$(echo "$line" | grep -oP '(?<=-p )\w+')
dest=$(echo "$line" | grep -oP '(?<=--to-destination )\S+')
if [[ -n "$port" && -n "$proto" && -n "$dest" ]]; then
PARSED_RULES+=("$port|$proto|$dest")
fi
done < <(ip6tables -t nat -S PREROUTING 2>/dev/null | grep "DNAT")
}
# ── Интерактивные функции ───────────────────────────────────────────────────────
REPLY_VALUE=""
input_ip() {
REPLY_VALUE=""
local ip
while true; do
echo -ne " IP адрес назначения: " >/dev/tty
read -r ip </dev/tty
if validate_ip "$ip"; then
REPLY_VALUE="$ip"
return
fi
msg_err "Некорректный IP. Поддерживаются IPv4 и IPv6." >/dev/tty
done
}
input_port() {
REPLY_VALUE=""
local label="$1" port
while true; do
echo -ne " $label: " >/dev/tty
read -r port </dev/tty
if validate_port "$port"; then
REPLY_VALUE="$port"
return
fi
msg_err "Порт: число 1-65535." >/dev/tty
done
}
input_port_or_range() {
REPLY_VALUE=""
local label="$1" val
while true; do
echo -ne " $label (число или диапазон N:M): " >/dev/tty
read -r val </dev/tty
if validate_port_range "$val"; then
REPLY_VALUE="$val"
return
fi
msg_err "Формат: одиночный порт (1-65535) или диапазон (start:end)." >/dev/tty
done
}
# ── Пункты меню ─────────────────────────────────────────────────────────────────
menu_standard_rule() {
local proto="$1" label="$2"
echo -e "\n${CYAN}── Настройка $label ($proto) ──${NC}\n"
input_ip
local target_ip="$REPLY_VALUE"
input_port "Порт (одинаковый вход/выход)"
local port="$REPLY_VALUE"
echo ""
apply_rules "$proto" "$port" "$port" "$target_ip" "$label"
}
menu_custom_rule() {
echo -e "\n${CYAN}── Кастомное правило ──${NC}"
echo -e "${DIM}Позволяет задать разные порты входа и выхода, любой протокол.${NC}\n"
local proto
while true; do
echo -ne " Протокол (${YELLOW}tcp${NC}/${YELLOW}udp${NC}): "
read -r proto
[[ "$proto" == "tcp" || "$proto" == "udp" ]] && break
msg_err "Введите tcp или udp."
done
input_ip
local target_ip="$REPLY_VALUE"
input_port "Входящий порт (на этом сервере)"
local in_port="$REPLY_VALUE"
input_port "Исходящий порт (на целевом сервере)"
local out_port="$REPLY_VALUE"
echo ""
apply_rules "$proto" "$in_port" "$out_port" "$target_ip" "Custom"
}
menu_batch_rule() {
echo -e "\n${CYAN}── Пакетное создание правил ──${NC}"
echo -e "${DIM}Создаёт несколько правил за один раз через диапазон портов.${NC}\n"
local proto
while true; do
echo -ne " Протокол (${YELLOW}tcp${NC}/${YELLOW}udp${NC}): "
read -r proto
[[ "$proto" == "tcp" || "$proto" == "udp" ]] && break
msg_err "Введите tcp или udp."
done
input_ip
local target_ip="$REPLY_VALUE"
input_port_or_range "Диапазон портов"
local port_range="$REPLY_VALUE"
echo ""
if [[ "$port_range" =~ ^([0-9]+):([0-9]+)$ ]]; then
local start="${BASH_REMATCH[1]}"
local end="${BASH_REMATCH[2]}"
local count=$(( end - start + 1 ))
msg_info "Создаётся $count правил ($start-$end)..."
for (( p=start; p<=end; p++ )); do
if rule_exists "$target_ip" "$proto" "$p" "$p"; then
msg_warn "Пропуск порта $p (уже существует)"
else
apply_rules_silent "$proto" "$p" "$p" "$target_ip"
fi
done
msg_ok "Пакетное создание завершено ($count портов)."
log INFO "Batch: $proto $start:$end -> $target_ip"
else
apply_rules "$proto" "$port_range" "$port_range" "$target_ip" "Batch-single"
return
fi
read -p "Нажмите Enter..."
}
apply_rules_silent() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4"
local iface
iface=$(detect_interface)
local dest_fmt="$target_ip:$out_port"
local fwd_dest="$target_ip"
if is_ipv6 "$target_ip"; then
dest_fmt="[$target_ip]:$out_port"
fi
ipt_cmd "$target_ip" -A INPUT -p "$proto" --dport "$in_port" -j ACCEPT
ipt_cmd "$target_ip" -t nat -A PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt"
if ! ipt_cmd "$target_ip" -t nat -C POSTROUTING -o "$iface" -j MASQUERADE 2>/dev/null; then
ipt_cmd "$target_ip" -t nat -A POSTROUTING -o "$iface" -j MASQUERADE
fi
ipt_cmd "$target_ip" -A FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
ipt_cmd "$target_ip" -A FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \
-m state --state ESTABLISHED,RELATED -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
echo -e " ${GREEN}+${NC} $proto :$in_port -> $target_ip:$out_port"
log INFO "Batch rule: $proto $in_port -> $target_ip:$out_port"
}
menu_list_rules() {
echo -e "\n${CYAN}── Активные переадресации ──${NC}\n"
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
msg_info "Нет активных правил."
read -p "Нажмите Enter..."
return
fi
printf " ${MAGENTA}%-6s %-8s %-8s %s${NC}\n" "#" "ВХОД" "ПРОТО" "ЦЕЛЬ"
printf " ${DIM}%-6s %-8s %-8s %s${NC}\n" "──" "────" "─────" "────"
local i=1
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
printf " %-6s %-8s %-8s %s\n" "$i" "$port" "$proto" "$dest"
((i++))
done
echo ""
read -p "Нажмите Enter..."
}
menu_delete_rule() {
echo -e "\n${CYAN}── Удаление правила ──${NC}\n"
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
msg_info "Нет активных правил."
read -p "Нажмите Enter..."
return
fi
local i=1
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
echo -e " ${YELLOW}[$i]${NC} $proto :$port -> $dest"
((i++))
done
echo ""
echo -ne " Номер правила для удаления (0 — отмена): "
read -r rule_num
if [[ "$rule_num" == "0" || -z "$rule_num" ]]; then return; fi
if ! [[ "$rule_num" =~ ^[0-9]+$ ]] || (( rule_num < 1 || rule_num > ${#PARSED_RULES[@]} )); then
msg_err "Некорректный номер."
read -p "Нажмите Enter..."
return
fi
local idx=$(( rule_num - 1 ))
IFS='|' read -r d_port d_proto d_dest <<< "${PARSED_RULES[$idx]}"
local target_ip target_port
# Обработка IPv6 формата [ip]:port
if [[ "$d_dest" =~ ^\[(.+)\]:(.+)$ ]]; then
target_ip="${BASH_REMATCH[1]}"
target_port="${BASH_REMATCH[2]}"
else
target_ip="${d_dest%:*}"
target_port="${d_dest##*:}"
fi
if confirm "Удалить правило $d_proto :$d_port -> $d_dest?"; then
remove_rule "$d_proto" "$d_port" "$target_port" "$target_ip"
msg_ok "Правило удалено."
fi
read -p "Нажмите Enter..."
}
menu_flush() {
echo -e "\n${RED}── Полный сброс правил ──${NC}\n"
msg_warn "Будут удалены ВСЕ правила iptables (NAT, FORWARD, INPUT)."
msg_warn "Это может нарушить работу других сервисов!"
echo ""
if ! confirm "Вы уверены?"; then
return
fi
# Бэкап перед сбросом
backup_rules "pre-flush"
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
iptables -t nat -F
iptables -t mangle -F
iptables -F
iptables -X
# IPv6
ip6tables -P INPUT ACCEPT 2>/dev/null
ip6tables -P FORWARD ACCEPT 2>/dev/null
ip6tables -P OUTPUT ACCEPT 2>/dev/null
ip6tables -t nat -F 2>/dev/null
ip6tables -F 2>/dev/null
ip6tables -X 2>/dev/null
netfilter-persistent save >/dev/null 2>&1
msg_ok "Все правила сброшены. Бэкап сохранён в $BACKUP_DIR"
log INFO "Полный сброс выполнен."
read -p "Нажмите Enter..."
}
# ── Бэкап / Восстановление ─────────────────────────────────────────────────────
backup_rules() {
local label="${1:-manual}"
local ts
ts=$(date '+%Y%m%d_%H%M%S')
local file="$BACKUP_DIR/${ts}_${label}.v4"
local file6="$BACKUP_DIR/${ts}_${label}.v6"
iptables-save > "$file" 2>/dev/null
ip6tables-save > "$file6" 2>/dev/null
log INFO "Бэкап создан: $file, $file6"
echo "$file"
}
menu_backup() {
echo -e "\n${CYAN}── Бэкап правил ──${NC}\n"
local f
f=$(backup_rules "manual")
msg_ok "Бэкап сохранён: $f"
read -p "Нажмите Enter..."
}
menu_restore() {
echo -e "\n${CYAN}── Восстановление из бэкапа ──${NC}\n"
local files=()
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$BACKUP_DIR" -name "*.v4" -print0 2>/dev/null | sort -z -r)
if (( ${#files[@]} == 0 )); then
msg_info "Нет доступных бэкапов."
read -p "Нажмите Enter..."
return
fi
local i=1
for f in "${files[@]}"; do
local name
name=$(basename "$f")
echo -e " ${YELLOW}[$i]${NC} $name"
((i++))
done
echo ""
echo -ne " Номер бэкапа для восстановления (0 — отмена): "
read -r choice
if [[ "$choice" == "0" || -z "$choice" ]]; then return; fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#files[@]} )); then
msg_err "Некорректный номер."
read -p "Нажмите Enter..."
return
fi
local idx=$(( choice - 1 ))
local selected="${files[$idx]}"
local selected6="${selected%.v4}.v6"
if confirm "Восстановить из $(basename "$selected")?"; then
backup_rules "pre-restore"
iptables-restore < "$selected" 2>/dev/null
if [[ -f "$selected6" ]]; then
ip6tables-restore < "$selected6" 2>/dev/null
fi
netfilter-persistent save >/dev/null 2>&1
msg_ok "Правила восстановлены."
log INFO "Восстановление из $selected"
fi
read -p "Нажмите Enter..."
}
# ── Проверка доступности целевого сервера ───────────────────────────────────────
menu_check_target() {
echo -e "\n${CYAN}── Проверка доступности ──${NC}\n"
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
msg_info "Нет активных правил для проверки."
read -p "Нажмите Enter..."
return
fi
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
local target_ip target_port
if [[ "$dest" =~ ^\[(.+)\]:(.+)$ ]]; then
target_ip="${BASH_REMATCH[1]}"
target_port="${BASH_REMATCH[2]}"
else
target_ip="${dest%:*}"
target_port="${dest##*:}"
fi
echo -ne " $proto :$port -> $dest ... "
if command -v nc &>/dev/null; then
if [[ "$proto" == "tcp" ]]; then
if nc -z -w3 "$target_ip" "$target_port" 2>/dev/null; then
echo -e "${GREEN}ДОСТУПЕН${NC}"
else
echo -e "${RED}НЕДОСТУПЕН${NC}"
fi
else
echo -e "${DIM}UDP (не проверяется)${NC}"
fi
else
if timeout 3 bash -c "echo >/dev/tcp/$target_ip/$target_port" 2>/dev/null; then
echo -e "${GREEN}ДОСТУПЕН${NC}"
else
echo -e "${RED}НЕДОСТУПЕН${NC}"
fi
fi
done
echo ""
read -p "Нажмите Enter..."
}
# ── Статус системы ──────────────────────────────────────────────────────────────
menu_status() {
echo -e "\n${CYAN}── Статус системы ──${NC}\n"
local iface
iface=$(detect_interface)
local backend
backend=$(detect_firewall_backend)
local bbr_active="нет"
sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr && bbr_active="да"
local fwd_v4
fwd_v4=$(sysctl -n net.ipv4.ip_forward 2>/dev/null)
local fwd_v6
fwd_v6=$(sysctl -n net.ipv6.conf.all.forwarding 2>/dev/null)
parse_active_rules
local rule_count=${#PARSED_RULES[@]}
local ufw_status="не установлен"
if command -v ufw &>/dev/null; then
ufw_status=$(ufw status 2>/dev/null | head -1)
fi
printf " %-28s %s\n" "Версия:" "$VERSION"
printf " %-28s %s\n" "Сетевой интерфейс:" "$iface"
printf " %-28s %s\n" "Firewall backend:" "$backend"
printf " %-28s %s\n" "IPv4 forwarding:" "$fwd_v4"
printf " %-28s %s\n" "IPv6 forwarding:" "${fwd_v6:-N/A}"
printf " %-28s %s\n" "Google BBR:" "$bbr_active"
printf " %-28s %s\n" "UFW:" "$ufw_status"
printf " %-28s %s\n" "Активных правил:" "$rule_count"
printf " %-28s %s\n" "Лог:" "$LOG_FILE"
printf " %-28s %s\n" "Бэкапы:" "$BACKUP_DIR"
echo ""
read -p "Нажмите Enter..."
}
# ── Просмотр лога ──────────────────────────────────────────────────────────────
menu_log() {
echo -e "\n${CYAN}── Последние записи лога ──${NC}\n"
if [[ ! -s "$LOG_FILE" ]]; then
msg_info "Лог пуст."
else
tail -20 "$LOG_FILE" | while IFS= read -r line; do
echo " $line"
done
fi
echo ""
read -p "Нажмите Enter..."
}
# ── Инструкция ──────────────────────────────────────────────────────────────────
show_instructions() {
clear
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Как настроить каскадное соединение ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${WHITE}Принцип:${NC} Клиент -> Этот сервер (мост) -> Зарубежный сервер"
echo ""
echo -e " ${YELLOW}Шаг 1.${NC} Подготовьте IP и порт зарубежного сервера."
echo -e " ${YELLOW}Шаг 2.${NC} Выберите протокол в меню и введите данные."
echo -e " ${YELLOW}Шаг 3.${NC} В клиенте (AmneziaWG, v2rayNG, и т.д.) замените"
echo -e " адрес сервера на IP ${GREEN}этого${NC} сервера."
echo -e " ${YELLOW}Шаг 4.${NC} Если порты разные — укажите входящий порт в клиенте."
echo ""
echo -e " ${DIM}Трафик пойдёт через ядро Linux (NAT iptables) без"
echo -e " дополнительных процессов — максимальная скорость.${NC}"
echo ""
read -p "Нажмите Enter..."
}
# ── Главное меню ────────────────────────────────────────────────────────────────
show_menu() {
while true; do
clear
echo ""
echo -e "${CYAN} ──────────────────────────────────────────${NC}"
echo -e " ${WHITE}CASCAD${NC} ${DIM}v${VERSION}${NC} — каскадное NAT-перенаправление"
echo -e "${CYAN} ──────────────────────────────────────────${NC}"
echo ""
echo -e " ${WHITE}Настройка правил:${NC}"
echo -e " ${GREEN}1${NC}) AmneziaWG / WireGuard ${DIM}(UDP)${NC}"
echo -e " ${GREEN}2${NC}) VLESS / XRay / Reality ${DIM}(TCP)${NC}"
echo -e " ${GREEN}3${NC}) MTProto / TProxy ${DIM}(TCP)${NC}"
echo -e " ${GREEN}4${NC}) Кастомное правило ${DIM}(любой протокол, разные порты)${NC}"
echo -e " ${GREEN}5${NC}) Пакетное создание ${DIM}(диапазон портов)${NC}"
echo ""
echo -e " ${WHITE}Управление:${NC}"
echo -e " ${GREEN}6${NC}) Список активных правил"
echo -e " ${GREEN}7${NC}) Удалить правило"
echo -e " ${GREEN}8${NC}) Сбросить ВСЕ правила ${DIM}(с автобэкапом)${NC}"
echo -e " ${GREEN}9${NC}) Проверить доступность целей"
echo ""
echo -e " ${WHITE}Сервис:${NC}"
echo -e " ${GREEN}b${NC}) Создать бэкап"
echo -e " ${GREEN}r${NC}) Восстановить из бэкапа"
echo -e " ${GREEN}s${NC}) Статус системы"
echo -e " ${GREEN}l${NC}) Просмотр лога"
echo -e " ${GREEN}h${NC}) Инструкция"
echo -e " ${GREEN}0${NC}) Выход"
echo ""
echo -ne " Выбор: "
read -r choice
case $choice in
1) menu_standard_rule "udp" "AmneziaWG / WireGuard" ;;
2) menu_standard_rule "tcp" "VLESS / XRay" ;;
3) menu_standard_rule "tcp" "MTProto / TProxy" ;;
4) menu_custom_rule ;;
5) menu_batch_rule ;;
6) menu_list_rules ;;
7) menu_delete_rule ;;
8) menu_flush ;;
9) menu_check_target ;;
b|B) menu_backup ;;
r|R) menu_restore ;;
s|S) menu_status ;;
l|L) menu_log ;;
h|H) show_instructions ;;
0) echo -e "\n${DIM} До встречи.${NC}\n"; exit 0 ;;
*) ;;
esac
done
}
# ── CLI-режим ───────────────────────────────────────────────────────────────────
show_help() {
echo "cascad v${VERSION} — каскадное NAT-перенаправление"
echo ""
echo "Использование: $SCRIPT_NAME [команда] [аргументы]"
echo ""
echo "Команды:"
echo " (без аргументов) Интерактивное меню"
echo " add <proto> <ip> <in_port> [out_port]"
echo " Добавить правило"
echo " list Список активных правил"
echo " del <proto> <ip> <in_port> <out_port>"
echo " Удалить правило"
echo " flush Сбросить все правила"
echo " backup Создать бэкап"
echo " restore <file> Восстановить из бэкапа"
echo " status Показать статус системы"
echo " check Проверить доступность целей"
echo " help Эта справка"
echo ""
echo "Примеры:"
echo " $SCRIPT_NAME add tcp 203.0.113.10 443"
echo " $SCRIPT_NAME add udp 203.0.113.10 51820 51821"
echo " $SCRIPT_NAME list"
echo " $SCRIPT_NAME del tcp 203.0.113.10 443 443"
}
cli_add() {
local proto="${1:-}" ip="${2:-}" in_port="${3:-}" out_port="${4:-}"
if [[ -z "$proto" || -z "$ip" || -z "$in_port" ]]; then
msg_err "Формат: $SCRIPT_NAME add <tcp|udp> <ip> <in_port> [out_port]"
exit 1
fi
[[ -z "$out_port" ]] && out_port="$in_port"
if [[ "$proto" != "tcp" && "$proto" != "udp" ]]; then
msg_err "Протокол: tcp или udp."
exit 1
fi
if ! validate_ip "$ip"; then
msg_err "Некорректный IP: $ip"
exit 1
fi
if ! validate_port "$in_port" || ! validate_port "$out_port"; then
msg_err "Некорректный порт."
exit 1
fi
if rule_exists "$ip" "$proto" "$in_port" "$out_port"; then
msg_warn "Правило уже существует."
exit 0
fi
local iface dest_fmt fwd_dest
iface=$(detect_interface)
dest_fmt="$ip:$out_port"
fwd_dest="$ip"
if is_ipv6 "$ip"; then
dest_fmt="[$ip]:$out_port"
fi
ipt_cmd "$ip" -A INPUT -p "$proto" --dport "$in_port" -j ACCEPT
ipt_cmd "$ip" -t nat -A PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt"
if ! ipt_cmd "$ip" -t nat -C POSTROUTING -o "$iface" -j MASQUERADE 2>/dev/null; then
ipt_cmd "$ip" -t nat -A POSTROUTING -o "$iface" -j MASQUERADE
fi
ipt_cmd "$ip" -A FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
ipt_cmd "$ip" -A FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \
-m state --state ESTABLISHED,RELATED -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
sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
fi
netfilter-persistent save >/dev/null 2>&1
msg_ok "Добавлено: $proto :$in_port -> $ip:$out_port"
log INFO "CLI add: $proto $in_port -> $ip:$out_port"
}
cli_list() {
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
echo "Нет активных правил."
return
fi
printf "%-8s %-8s %s\n" "ВХОД" "ПРОТО" "ЦЕЛЬ"
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
printf "%-8s %-8s %s\n" "$port" "$proto" "$dest"
done
}
cli_del() {
local proto="${1:-}" ip="${2:-}" in_port="${3:-}" out_port="${4:-}"
if [[ -z "$proto" || -z "$ip" || -z "$in_port" || -z "$out_port" ]]; then
msg_err "Формат: $SCRIPT_NAME del <tcp|udp> <ip> <in_port> <out_port>"
exit 1
fi
remove_rule "$proto" "$in_port" "$out_port" "$ip"
msg_ok "Удалено: $proto :$in_port -> $ip:$out_port"
}
cli_flush() {
if [[ "${1:-}" != "-y" ]]; then
if ! confirm "Сбросить ВСЕ правила iptables?"; then
exit 0
fi
fi
backup_rules "cli-flush"
iptables -P INPUT ACCEPT; iptables -P FORWARD ACCEPT; iptables -P OUTPUT ACCEPT
iptables -t nat -F; iptables -t mangle -F; iptables -F; iptables -X
ip6tables -P INPUT ACCEPT 2>/dev/null; ip6tables -P FORWARD ACCEPT 2>/dev/null
ip6tables -P OUTPUT ACCEPT 2>/dev/null
ip6tables -t nat -F 2>/dev/null; ip6tables -F 2>/dev/null; ip6tables -X 2>/dev/null
netfilter-persistent save >/dev/null 2>&1
msg_ok "Все правила сброшены."
log INFO "CLI flush."
}
cli_check() {
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
echo "Нет активных правил."
return
fi
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
local target_ip target_port
if [[ "$dest" =~ ^\[(.+)\]:(.+)$ ]]; then
target_ip="${BASH_REMATCH[1]}"
target_port="${BASH_REMATCH[2]}"
else
target_ip="${dest%:*}"
target_port="${dest##*:}"
fi
printf "$proto :$port -> $dest ... "
if [[ "$proto" == "tcp" ]]; then
if timeout 3 bash -c "echo >/dev/tcp/$target_ip/$target_port" 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}FAIL${NC}"
fi
else
echo "UDP"
fi
done
}
# ── Точка входа ─────────────────────────────────────────────────────────────────
main() {
check_root
case "${1:-}" in
add) shift; prepare_system; cli_add "$@" ;;
list) cli_list ;;
del) shift; cli_del "$@" ;;
flush) shift; prepare_system; cli_flush "$@" ;;
backup) prepare_system; menu_backup ;;
restore)
shift
prepare_system
if [[ -n "${1:-}" ]]; then
iptables-restore < "$1" 2>/dev/null
netfilter-persistent save >/dev/null 2>&1
msg_ok "Восстановлено из $1"
else
menu_restore
fi
;;
status) prepare_system; menu_status ;;
check) prepare_system; cli_check ;;
help|-h|--help) show_help ;;
"") prepare_system; show_menu ;;
*) msg_err "Неизвестная команда: $1"; show_help; exit 1 ;;
esac
}
main "$@"