#!/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 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 } # ── Пункты меню ───────────────────────────────────────────────────────────────── 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 [out_port]" echo " Добавить правило" echo " list Список активных правил" echo " del " echo " Удалить правило" echo " flush Сбросить все правила" echo " backup Создать бэкап" echo " restore Восстановить из бэкапа" 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 [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 " 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 "$@"