#!/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="2.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}" STATE_FILE="/etc/${SCRIPT_NAME}/state" LOCK_FILE="/var/run/${SCRIPT_NAME}.lock" REPO_URL="https://raw.githubusercontent.com/arblark/cascad/main/cascad.sh" MAX_BACKUPS=20 MAX_LOG_SIZE=1048576 # 1 MB # ── Cleanup & Lock ────────────────────────────────────────────────────────────── cleanup() { local exit_code=$? if [[ -n "${LOCK_FD:-}" ]]; then eval "exec ${LOCK_FD}>&-" 2>/dev/null fi rm -f "$LOCK_FILE" 2>/dev/null if (( exit_code != 0 )); then log WARN "Скрипт завершился с кодом $exit_code" fi } trap cleanup EXIT INT TERM acquire_lock() { mkdir -p "$(dirname "$LOCK_FILE")" LOCK_FD=9 eval "exec ${LOCK_FD}>\"$LOCK_FILE\"" if ! flock -n "$LOCK_FD"; then msg_err "Другой экземпляр $SCRIPT_NAME уже запущен." exit 1 fi } # ── Утилиты ───────────────────────────────────────────────────────────────────── log() { local level="$1"; shift local msg="$*" local ts ts=$(date '+%Y-%m-%d %H:%M:%S') echo "[$ts] [$level] $msg" >> "$LOG_FILE" 2>/dev/null || true } 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_pkg_manager() { if command -v apt-get &>/dev/null; then echo "apt" elif command -v dnf &>/dev/null; then echo "dnf" elif command -v yum &>/dev/null; then echo "yum" elif command -v pacman &>/dev/null; then echo "pacman" else echo "unknown" 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 — через python3 или perl для точной валидации if command -v python3 &>/dev/null; then python3 -c "import ipaddress; ipaddress.ip_address('$ip')" 2>/dev/null && return 0 elif command -v perl &>/dev/null; then perl -MSocket -e "exit(defined inet_pton(AF_INET6, '$ip') ? 0 : 1)" 2>/dev/null && return 0 else # Fallback: базовая проверка формата с защитой от двойного :: if [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]] && [[ ! "$ip" =~ ::.*:: ]]; then local colons="${ip//[^:]}" (( ${#colons} >= 2 && ${#colons} <= 7 )) && return 0 fi 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" == *":"* ]] } check_port_conflict() { local port="$1" proto="$2" local conflict="" if command -v ss &>/dev/null; then conflict=$(ss -tlnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {print $NF}' | head -1) if [[ -z "$conflict" && "$proto" == "udp" ]]; then conflict=$(ss -ulnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {print $NF}' | head -1) fi elif command -v netstat &>/dev/null; then conflict=$(netstat -tlnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {print $NF}' | head -1) fi if [[ -n "$conflict" ]]; then msg_warn "Порт $port/$proto уже используется: $conflict" msg_warn "DNAT-правило перехватит трафик, предназначенный этому сервису!" if ! confirm "Всё равно продолжить?"; then return 1 fi fi return 0 } # ── Ротация ───────────────────────────────────────────────────────────────────── rotate_backups() { local count count=$(find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" 2>/dev/null | wc -l) if (( count > MAX_BACKUPS )); then local to_delete=$(( count - MAX_BACKUPS )) find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" -print0 2>/dev/null \ | sort -z | head -z -n "$to_delete" \ | while IFS= read -r -d '' f; do rm -f "$f" "${f%.v4}.v6" 2>/dev/null local ufw_dir="${f%.v4}_ufw" rm -rf "$ufw_dir" 2>/dev/null done log INFO "Ротация: удалено $to_delete старых бэкапов" fi } rotate_log() { if [[ -f "$LOG_FILE" ]]; then local size size=$(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null || echo 0) if (( size > MAX_LOG_SIZE )); then mv "$LOG_FILE" "${LOG_FILE}.1" touch "$LOG_FILE" log INFO "Лог ротирован (предыдущий: ${LOG_FILE}.1)" fi fi } # ── Состояние (сохранение оригинальных значений) ──────────────────────────────── save_state() { local key="$1" value="$2" mkdir -p "$(dirname "$STATE_FILE")" touch "$STATE_FILE" if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then return fi echo "${key}=${value}" >> "$STATE_FILE" } load_state() { local key="$1" if [[ -f "$STATE_FILE" ]]; then grep "^${key}=" "$STATE_FILE" 2>/dev/null | tail -1 | cut -d'=' -f2- fi } remove_state() { local key="$1" if [[ -f "$STATE_FILE" ]]; then sed -i "/^${key}=/d" "$STATE_FILE" 2>/dev/null fi } # ── Подготовка системы ────────────────────────────────────────────────────────── prepare_system() { mkdir -p "$BACKUP_DIR" "$CONFIG_DIR" touch "$LOG_FILE" rotate_log 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 # Бэкап sysctl.conf перед любыми модификациями if [[ -f /etc/sysctl.conf ]]; then local sysctl_bak="$BACKUP_DIR/sysctl.conf.$(date '+%Y%m%d_%H%M%S').bak" if [[ ! -f "$BACKUP_DIR/sysctl.conf.original" ]]; then cp /etc/sysctl.conf "$BACKUP_DIR/sysctl.conf.original" log INFO "Оригинальный sysctl.conf сохранён" fi cp /etc/sysctl.conf "$sysctl_bak" 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 # Зависимости local pkg_mgr pkg_mgr=$(detect_pkg_manager) case "$pkg_mgr" in apt) 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 ;; dnf|yum) if ! command -v iptables &>/dev/null; then msg_info "Установка iptables..." "$pkg_mgr" install -y iptables iptables-services >/dev/null 2>&1 systemctl enable --now iptables 2>/dev/null || true fi ;; pacman) if ! command -v iptables &>/dev/null; then msg_info "Установка iptables..." pacman -Sy --noconfirm iptables >/dev/null 2>&1 fi ;; *) if ! command -v iptables &>/dev/null; then msg_err "Не удалось определить пакетный менеджер. Установите iptables вручную." exit 1 fi ;; esac log INFO "Система подготовлена." } # ── Сохранение правил (кросс-дистрибутив) ─────────────────────────────────────── persist_rules() { if command -v netfilter-persistent &>/dev/null; then netfilter-persistent save >/dev/null 2>&1 elif [[ -f /etc/redhat-release ]] || command -v dnf &>/dev/null || command -v yum &>/dev/null; then service iptables save 2>/dev/null || true service ip6tables save 2>/dev/null || true else iptables-save > /etc/iptables.rules 2>/dev/null || true ip6tables-save > /etc/ip6tables.rules 2>/dev/null || true fi } # ── Работа с правилами (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_rule_core() { 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 local applied=() local ok=1 _rollback_rules() { for (( i=${#applied[@]}-1; i>=0; i-- )); do eval "${applied[$i]}" 2>/dev/null || true done } ipt_cmd "$target_ip" -A INPUT -p "$proto" --dport "$in_port" -j ACCEPT || { _rollback_rules; ok=0; } if (( ok )); then applied+=("ipt_cmd '$target_ip' -D INPUT -p '$proto' --dport '$in_port' -j ACCEPT") fi if (( ok )); then ipt_cmd "$target_ip" -t nat -A PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt" || { _rollback_rules; ok=0; } if (( ok )); then applied+=("ipt_cmd '$target_ip' -t nat -D PREROUTING -p '$proto' --dport '$in_port' -j DNAT --to-destination '$dest_fmt'") fi fi if (( ok )); then 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 || { _rollback_rules; ok=0; } fi fi if (( ok )); then ipt_cmd "$target_ip" -A FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \ -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT || { _rollback_rules; ok=0; } if (( ok )); then applied+=("ipt_cmd '$target_ip' -D FORWARD -p '$proto' -d '$fwd_dest' --dport '$out_port' -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT") fi fi if (( ok )); then ipt_cmd "$target_ip" -A FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \ -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT || { _rollback_rules; ok=0; } fi if (( ! ok )); then msg_err "Ошибка при применении правил. Выполнен откат." log ERROR "Rollback: $proto $in_port -> $target_ip:$out_port" return 1 fi # 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 local current_policy current_policy=$(grep '^DEFAULT_FORWARD_POLICY=' /etc/default/ufw 2>/dev/null | cut -d'"' -f2) if [[ "$current_policy" != "ACCEPT" ]]; then save_state "ufw_forward_policy" "$current_policy" sed -i 's/DEFAULT_FORWARD_POLICY=".*"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw 2>/dev/null ufw reload >/dev/null 2>&1 fi fi persist_rules return 0 } apply_rules() { local proto="$1" in_port="$2" out_port="$3" target_ip="$4" label="$5" 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 check_port_conflict "$in_port" "$proto" || return backup_rules "pre-add" msg_info "Применение правил ($label)..." if _apply_rule_core "$proto" "$in_port" "$out_port" "$target_ip"; then msg_ok "$label настроен: $proto вход $in_port -> $target_ip:$out_port" log INFO "Создано: $label | $proto $in_port -> $target_ip:$out_port | iface=$(detect_interface)" fi read -p "Нажмите Enter..." } apply_rules_silent() { local proto="$1" in_port="$2" out_port="$3" target_ip="$4" if _apply_rule_core "$proto" "$in_port" "$out_port" "$target_ip"; then echo -e " ${GREEN}+${NC} $proto :$in_port -> $target_ip:$out_port" log INFO "Batch rule: $proto $in_port -> $target_ip:$out_port" else echo -e " ${RED}✗${NC} $proto :$in_port -> $target_ip:$out_port (ошибка)" fi } 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 conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT 2>/dev/null ipt_cmd "$target_ip" -D FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \ -m conntrack --ctstate 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 persist_rules log INFO "Удалено: $proto $in_port -> $target_ip:$out_port" } # ── Восстановление UFW forward policy при удалении последнего правила ────────── maybe_restore_ufw_policy() { parse_active_rules if (( ${#PARSED_RULES[@]} == 0 )); then local original_policy original_policy=$(load_state "ufw_forward_policy") if [[ -n "$original_policy" ]]; then sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null ufw reload >/dev/null 2>&1 remove_state "ufw_forward_policy" msg_info "UFW FORWARD policy восстановлена: $original_policy" log INFO "UFW FORWARD policy восстановлена: $original_policy" fi fi } # ── Парсер активных правил ────────────────────────────────────────────────────── 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 done } input_port() { REPLY_VALUE="" local label="$1" port while true; do echo -ne " $label: " >/dev/tty read -r port /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 done } input_proto() { REPLY_VALUE="" local proto while true; do echo -ne " Протокол (${YELLOW}tcp${NC}/${YELLOW}udp${NC}/${YELLOW}both${NC}): " read -r proto [[ "$proto" == "tcp" || "$proto" == "udp" || "$proto" == "both" ]] && break msg_err "Введите tcp, udp или both." done REPLY_VALUE="$proto" } # ── Пункты меню ───────────────────────────────────────────────────────────────── 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 "" if [[ "$proto" == "both" ]]; then apply_rules "tcp" "$port" "$port" "$target_ip" "$label (TCP)" apply_rules "udp" "$port" "$port" "$target_ip" "$label (UDP)" else apply_rules "$proto" "$port" "$port" "$target_ip" "$label" fi } menu_custom_rule() { echo -e "\n${CYAN}── Кастомное правило ──${NC}" echo -e "${DIM}Позволяет задать разные порты входа и выхода, любой протокол.${NC}\n" input_proto local proto="$REPLY_VALUE" input_ip local target_ip="$REPLY_VALUE" input_port "Входящий порт (на этом сервере)" local in_port="$REPLY_VALUE" input_port "Исходящий порт (на целевом сервере)" local out_port="$REPLY_VALUE" echo "" if [[ "$proto" == "both" ]]; then apply_rules "tcp" "$in_port" "$out_port" "$target_ip" "Custom (TCP)" apply_rules "udp" "$in_port" "$out_port" "$target_ip" "Custom (UDP)" else apply_rules "$proto" "$in_port" "$out_port" "$target_ip" "Custom" fi } menu_batch_rule() { echo -e "\n${CYAN}── Пакетное создание правил ──${NC}" echo -e "${DIM}Создаёт несколько правил за один раз через диапазон портов.${NC}\n" input_proto local proto="$REPLY_VALUE" 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 )) backup_rules "pre-batch" msg_info "Создаётся $count правил ($start-$end)..." local protos=() if [[ "$proto" == "both" ]]; then protos=(tcp udp) else protos=("$proto") fi for p_proto in "${protos[@]}"; do for (( p=start; p<=end; p++ )); do if rule_exists "$target_ip" "$p_proto" "$p" "$p"; then msg_warn "Пропуск $p_proto :$p (уже существует)" else apply_rules_silent "$p_proto" "$p" "$p" "$target_ip" fi done done msg_ok "Пакетное создание завершено ($count портов)." log INFO "Batch: $proto $start:$end -> $target_ip" else if [[ "$proto" == "both" ]]; then apply_rules "tcp" "$port_range" "$port_range" "$target_ip" "Batch-single (TCP)" apply_rules "udp" "$port_range" "$port_range" "$target_ip" "Batch-single (UDP)" else apply_rules "$proto" "$port_range" "$port_range" "$target_ip" "Batch-single" fi return fi read -p "Нажмите Enter..." } 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 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 backup_rules "pre-delete" remove_rule "$d_proto" "$d_port" "$target_port" "$target_ip" msg_ok "Правило удалено." maybe_restore_ufw_policy 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 persist_rules # Восстановление UFW forward policy local original_policy original_policy=$(load_state "ufw_forward_policy") if [[ -n "$original_policy" ]] && command -v ufw &>/dev/null; then sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null ufw reload >/dev/null 2>&1 remove_state "ufw_forward_policy" msg_info "UFW FORWARD policy восстановлена: $original_policy" fi 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 # Бэкап UFW конфигурации if command -v ufw &>/dev/null; then local ufw_dir="$BACKUP_DIR/${ts}_${label}_ufw" mkdir -p "$ufw_dir" cp /etc/default/ufw "$ufw_dir/" 2>/dev/null || true cp /etc/ufw/before.rules "$ufw_dir/" 2>/dev/null || true cp /etc/ufw/after.rules "$ufw_dir/" 2>/dev/null || true cp /etc/ufw/before6.rules "$ufw_dir/" 2>/dev/null || true cp /etc/ufw/after6.rules "$ufw_dir/" 2>/dev/null || true cp /etc/ufw/user.rules "$ufw_dir/" 2>/dev/null || true cp /etc/ufw/user6.rules "$ufw_dir/" 2>/dev/null || true fi rotate_backups log INFO "Бэкап создан: $file, $file6" echo "$file" } menu_backup() { echo -e "\n${CYAN}── Бэкап правил ──${NC}\n" local f f=$(backup_rules "manual") msg_ok "Бэкап сохранён: $f" if command -v ufw &>/dev/null; then msg_info "UFW конфигурация также сохранена." fi 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" -maxdepth 1 -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") local ufw_marker="" local ufw_dir="${f%.v4}_ufw" [[ -d "$ufw_dir" ]] && ufw_marker=" ${DIM}(+UFW)${NC}" echo -e " ${YELLOW}[$i]${NC} $name$ufw_marker" ((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" local selected_ufw="${selected%.v4}_ufw" local restore_ufw=0 if [[ -d "$selected_ufw" ]]; then if confirm "Также восстановить конфигурацию UFW?"; then restore_ufw=1 fi fi 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 if (( restore_ufw )) && [[ -d "$selected_ufw" ]]; then cp "$selected_ufw"/ufw "$selected_ufw"/* /etc/default/ 2>/dev/null || true # Восстановление файлов правил UFW for uf in before.rules after.rules before6.rules after6.rules user.rules user6.rules; do [[ -f "$selected_ufw/$uf" ]] && cp "$selected_ufw/$uf" "/etc/ufw/$uf" 2>/dev/null done ufw reload >/dev/null 2>&1 msg_ok "Конфигурация UFW восстановлена." fi persist_rules 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) local saved_policy saved_policy=$(load_state "ufw_forward_policy") if [[ -n "$saved_policy" ]]; then ufw_status="$ufw_status ${DIM}(original FORWARD: $saved_policy)${NC}" fi fi local backup_count backup_count=$(find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" 2>/dev/null | wc -l) local pkg_mgr pkg_mgr=$(detect_pkg_manager) printf " %-28s %s\n" "Версия:" "$VERSION" printf " %-28s %s\n" "Сетевой интерфейс:" "$iface" printf " %-28s %s\n" "Firewall backend:" "$backend" printf " %-28s %s\n" "Пакетный менеджер:" "$pkg_mgr" 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" echo -e " $(printf '%-28s' "UFW:") $ufw_status" printf " %-28s %s\n" "Активных правил:" "$rule_count" printf " %-28s %s\n" "Бэкапов:" "$backup_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..." } # ── Обновление скрипта ────────────────────────────────────────────────────────── cli_update() { msg_info "Проверка обновлений..." local tmp tmp=$(mktemp) curl -fsSL "$REPO_URL" -o "$tmp" || { msg_err "Не удалось скачать обновление. Проверьте подключение." rm -f "$tmp" return 1 } local remote_ver remote_ver=$(grep '^VERSION=' "$tmp" | head -1 | cut -d'"' -f2) if [[ -z "$remote_ver" ]]; then msg_err "Не удалось определить версию в загруженном файле." rm -f "$tmp" return 1 fi if [[ "$remote_ver" == "$VERSION" ]]; then msg_info "Уже установлена последняя версия ($VERSION)." rm -f "$tmp" return 0 fi msg_info "Доступна новая версия: $VERSION -> $remote_ver" if confirm "Обновить?"; then backup_rules "pre-update" cp "$tmp" "$GLOBAL_CMD" chmod +x "$GLOBAL_CMD" rm -f "$tmp" msg_ok "Обновлено: $VERSION -> $remote_ver" log INFO "Обновление: $VERSION -> $remote_ver" msg_info "Перезапустите скрипт для применения." else rm -f "$tmp" fi } # ── Удаление скрипта ──────────────────────────────────────────────────────────── cli_uninstall() { echo -e "\n${RED}── Удаление $SCRIPT_NAME ──${NC}\n" msg_warn "Будут удалены:" msg_warn " - $GLOBAL_CMD" msg_warn " - $CONFIG_DIR (включая бэкапы)" msg_warn " - $LOG_FILE" echo "" local flush_rules=0 if confirm "Также сбросить все правила iptables?"; then flush_rules=1 fi if ! confirm "Вы уверены, что хотите удалить $SCRIPT_NAME?"; then return fi if (( flush_rules )); then backup_rules "pre-uninstall" 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 persist_rules local original_policy original_policy=$(load_state "ufw_forward_policy") if [[ -n "$original_policy" ]] && command -v ufw &>/dev/null; then sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null ufw reload >/dev/null 2>&1 fi msg_ok "Правила iptables сброшены." fi # Восстановление sysctl.conf если есть оригинал if [[ -f "$BACKUP_DIR/sysctl.conf.original" ]]; then if confirm "Восстановить оригинальный sysctl.conf?"; then cp "$BACKUP_DIR/sysctl.conf.original" /etc/sysctl.conf sysctl -p >/dev/null 2>&1 msg_ok "sysctl.conf восстановлен." fi fi rm -f "$GLOBAL_CMD" rm -rf "$CONFIG_DIR" rm -f "$LOG_FILE" "${LOG_FILE}.1" msg_ok "$SCRIPT_NAME полностью удалён." log INFO "Uninstall выполнен." 2>/dev/null || true } # ── Инструкция ────────────────────────────────────────────────────────────────── 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}(tcp/udp/both, разные порты)${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}) Создать бэкап ${DIM}(iptables + UFW)${NC}" echo -e " ${GREEN}r${NC}) Восстановить из бэкапа" echo -e " ${GREEN}s${NC}) Статус системы" echo -e " ${GREEN}l${NC}) Просмотр лога" echo -e " ${GREEN}u${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 ;; u|U) cli_update; read -p "Нажмите Enter..." ;; 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 [out_port]" echo " Добавить правило (proto: tcp, udp, both)" echo " list Список активных правил" echo " del " echo " Удалить правило" echo " flush [-y] Сбросить все правила" echo " backup Создать бэкап (iptables + UFW)" echo " restore [file] Восстановить из бэкапа" echo " status Показать статус системы" echo " check Проверить доступность целей" echo " update Обновить скрипт до последней версии" echo " uninstall Полностью удалить $SCRIPT_NAME" 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 add both 203.0.113.10 443" echo " $SCRIPT_NAME list" echo " $SCRIPT_NAME del tcp 203.0.113.10 443 443" echo " $SCRIPT_NAME update" echo " $SCRIPT_NAME uninstall" } 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 [out_port]" exit 1 fi [[ -z "$out_port" ]] && out_port="$in_port" if [[ "$proto" != "tcp" && "$proto" != "udp" && "$proto" != "both" ]]; then msg_err "Протокол: tcp, udp или both." 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 check_port_conflict "$in_port" "$proto" || exit 1 local protos=() if [[ "$proto" == "both" ]]; then protos=(tcp udp) else protos=("$proto") fi backup_rules "pre-add" for p in "${protos[@]}"; do if rule_exists "$ip" "$p" "$in_port" "$out_port"; then msg_warn "Правило $p уже существует." continue fi if _apply_rule_core "$p" "$in_port" "$out_port" "$ip"; then msg_ok "Добавлено: $p :$in_port -> $ip:$out_port" log INFO "CLI add: $p $in_port -> $ip:$out_port" fi done } 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 " exit 1 fi backup_rules "pre-delete" remove_rule "$proto" "$in_port" "$out_port" "$ip" msg_ok "Удалено: $proto :$in_port -> $ip:$out_port" maybe_restore_ufw_policy } 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 persist_rules local original_policy original_policy=$(load_state "ufw_forward_policy") if [[ -n "$original_policy" ]] && command -v ufw &>/dev/null; then sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null ufw reload >/dev/null 2>&1 remove_state "ufw_forward_policy" msg_info "UFW FORWARD policy восстановлена: $original_policy" fi 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 acquire_lock 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 backup_rules "pre-restore" iptables-restore < "$1" 2>/dev/null local v6="${1%.v4}.v6" [[ -f "$v6" ]] && ip6tables-restore < "$v6" 2>/dev/null persist_rules msg_ok "Восстановлено из $1" else menu_restore fi ;; status) prepare_system; menu_status ;; check) prepare_system; cli_check ;; update) prepare_system; cli_update ;; uninstall) prepare_system; cli_uninstall ;; help|-h|--help) show_help ;; "") prepare_system; show_menu ;; *) msg_err "Неизвестная команда: $1"; show_help; exit 1 ;; esac } main "$@"