diff --git a/cascad.sh b/cascad.sh new file mode 100755 index 0000000..f776763 --- /dev/null +++ b/cascad.sh @@ -0,0 +1,1028 @@ +#!/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") +} + +# ── Интерактивные функции ─────────────────────────────────────────────────────── + +input_ip() { + local ip + while true; do + echo -ne " IP адрес назначения: " + read -r ip + if validate_ip "$ip"; then + echo "$ip" + return + fi + msg_err "Некорректный IP. Поддерживаются IPv4 и IPv6." + done +} + +input_port() { + local label="$1" port + while true; do + echo -ne " $label: " + read -r port + if validate_port "$port"; then + echo "$port" + return + fi + msg_err "Порт: число 1-65535." + done +} + +input_port_or_range() { + local label="$1" val + while true; do + echo -ne " $label (число или диапазон N:M): " + read -r val + if validate_port_range "$val"; then + echo "$val" + return + fi + msg_err "Формат: одиночный порт (1-65535) или диапазон (start:end)." + done +} + +# ── Пункты меню ───────────────────────────────────────────────────────────────── + +menu_standard_rule() { + local proto="$1" label="$2" + + echo -e "\n${CYAN}── Настройка $label ($proto) ──${NC}\n" + + local target_ip + target_ip=$(input_ip) + + local port + port=$(input_port "Порт (одинаковый вход/выход)") + + 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 + + local target_ip + target_ip=$(input_ip) + + local in_port out_port + in_port=$(input_port "Входящий порт (на этом сервере)") + out_port=$(input_port "Исходящий порт (на целевом сервере)") + + 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 + + local target_ip + target_ip=$(input_ip) + + local port_range + port_range=$(input_port_or_range "Диапазон портов") + + 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 "${CYAN} │${NC} ${WHITE}CASCAD${NC} ${DIM}v${VERSION}${NC} — каскадное NAT-перенаправление ${CYAN}│${NC}" + 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 "$@"