From dac6e6d09262cc4c9bf8717efba5dd70e350878e Mon Sep 17 00:00:00 2001 From: arblark Date: Fri, 10 Apr 2026 22:52:12 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20v2.0.0=20=E2=80=94=20UFW=20backup,=20ro?= =?UTF-8?q?llback,=20lock,=20rotation,=20cross-distro=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backup/restore UFW configuration alongside iptables rules - Backup sysctl.conf before any modifications (preserves original) - Auto-backup before every add, delete, flush operation - Rollback partially applied rules on iptables errors - Save and restore UFW DEFAULT_FORWARD_POLICY on flush/delete - Rotate backups (max 20) and logs (max 1 MB) automatically - Lock file to prevent concurrent script execution - Port conflict detection warns when port is already in use - Support 'both' protocol (TCP+UDP simultaneously) - Cross-distro package manager detection (apt, dnf, yum, pacman) - Strict IPv6 validation via python3/perl with safe fallback - Replace deprecated -m state with -m conntrack --ctstate - Trap EXIT/INT/TERM for clean shutdown - Add 'update' command to self-update from repository - Add 'uninstall' command with full cleanup and config restoration - Persist rules cross-distro (netfilter-persistent, iptables-services, manual) - Extract _apply_rule_core to eliminate rule-application duplication - Show UFW and backup count in status, mark UFW backups in restore list --- README.md | 55 +++- cascad.sh | 742 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 629 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index 6b6e860..7fa7d2f 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,29 @@ ## Возможности -- **Мультипротокольность** — UDP (WireGuard, AmneziaWG), TCP (VLESS, XRay, Reality, MTProto) -- **IPv4 и IPv6** — полная поддержка обоих стеков +- **Мультипротокольность** — UDP (WireGuard, AmneziaWG), TCP (VLESS, XRay, Reality, MTProto), режим `both` (TCP+UDP одновременно) +- **IPv4 и IPv6** — полная поддержка обоих стеков с точной валидацией адресов - **Кастомные правила** — разные порты входа/выхода, любой протокол - **Пакетное создание** — диапазон портов за одну операцию - **Google BBR** — автоматическое ускорение TCP -- **Бэкап / восстановление** — сохранение и откат конфигурации +- **Бэкап / восстановление** — iptables, ip6tables и конфигурация UFW +- **Бэкап sysctl.conf** — оригинальная конфигурация сохраняется перед модификацией +- **Автобэкап** — перед каждым добавлением, удалением и сбросом правил +- **Ротация** — автоматическая очистка старых бэкапов (>20) и ротация лога (>1 MB) - **Проверка доступности** — тест TCP-соединения к целевым серверам +- **Проверка конфликтов портов** — предупреждение если порт уже используется локальным сервисом - **CLI-режим** — управление без интерактивного меню (для скриптов и автоматизации) - **Логирование** — все действия записываются в `/var/log/cascad.log` - **Защита от дубликатов** — проверка перед добавлением правил -- **Автобэкап при сбросе** — невозможно случайно потерять конфигурацию -- **Валидация ввода** — проверка IP-адресов и портов +- **Валидация ввода** — проверка IP-адресов (включая строгую проверку IPv6) и портов +- **Откат при ошибке** — частично применённые правила автоматически удаляются +- **UFW-совместимость** — сохранение и восстановление оригинальной FORWARD policy +- **Lock-файл** — защита от одновременного запуска нескольких экземпляров +- **Обновление** — `cascad update` для обновления до последней версии +- **Удаление** — `cascad uninstall` для полного удаления с восстановлением конфигурации +- **Trap/Cleanup** — корректное завершение при прерывании (Ctrl+C) +- **Кросс-дистрибутив** — поддержка Debian/Ubuntu (apt), CentOS/Fedora (dnf/yum), Arch (pacman) +- **Современный conntrack** — использование `-m conntrack --ctstate` вместо устаревшего `-m state` ## Установка @@ -75,6 +86,9 @@ cascad add tcp 203.0.113.10 443 # Добавить правило с разными портами cascad add udp 203.0.113.10 51820 51821 +# Добавить правило сразу для TCP и UDP +cascad add both 203.0.113.10 443 + # Список правил cascad list @@ -84,7 +98,7 @@ cascad del tcp 203.0.113.10 443 443 # Проверить доступность целей cascad check -# Создать бэкап +# Создать бэкап (iptables + UFW) cascad backup # Восстановить из бэкапа @@ -93,20 +107,41 @@ cascad restore # Статус системы cascad status +# Обновить скрипт +cascad update + # Сбросить все правила cascad flush +# Полностью удалить +cascad uninstall + # Справка cascad help ``` +## Версии + +- **v2.0.0** — бэкап UFW, ротация, lock-файл, rollback, проверка конфликтов портов, `both` протокол, `update`, `uninstall`, кросс-дистрибутив, conntrack +- **v1.0.0** — базовая функциональность: NAT-перенаправление, бэкап iptables, CLI + +Для установки конкретной версии: + +```bash +# v1.0.0 +bash <(curl -sL https://raw.githubusercontent.com/arblark/cascad/v1.0.0/cascad.sh) + +# v2.0.0 (последняя) +bash <(curl -sL https://raw.githubusercontent.com/arblark/cascad/v2.0.0/cascad.sh) +``` + ## Требования -- Linux (Ubuntu / Debian) +- Linux (Debian/Ubuntu, CentOS/Fedora, Arch) - root-доступ - iptables -Зависимости (`iptables-persistent`, `netfilter-persistent`) устанавливаются автоматически при первом запуске. +Зависимости устанавливаются автоматически при первом запуске. ## Структура файлов @@ -114,7 +149,9 @@ cascad help |---|---| | `/usr/local/bin/cascad` | Глобальная команда | | `/var/log/cascad.log` | Лог действий | -| `/etc/cascad/backups/` | Бэкапы правил iptables | +| `/etc/cascad/backups/` | Бэкапы правил iptables + UFW | +| `/etc/cascad/state` | Сохранённое состояние (оригинальные настройки) | +| `/var/run/cascad.lock` | Lock-файл | ## Лицензия diff --git a/cascad.sh b/cascad.sh index f9c4385..722509c 100755 --- a/cascad.sh +++ b/cascad.sh @@ -17,14 +17,44 @@ DIM='\033[2m' NC='\033[0m' # ── Константы ─────────────────────────────────────────────────────────────────── -VERSION="1.0.0" +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() { @@ -32,7 +62,7 @@ log() { local msg="$*" local ts ts=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$ts] [$level] $msg" >> "$LOG_FILE" + echo "[$ts] [$level] $msg" >> "$LOG_FILE" 2>/dev/null || true } msg_ok() { echo -e "${GREEN}[OK]${NC} $*"; } @@ -67,6 +97,20 @@ detect_firewall_backend() { 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) @@ -91,9 +135,17 @@ validate_ip() { 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 + # 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 } @@ -120,12 +172,91 @@ 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" @@ -141,6 +272,16 @@ prepare_system() { 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 @@ -179,20 +320,59 @@ prepare_system() { 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) + local pkg_mgr + pkg_mgr=$(detect_pkg_manager) - 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 + 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() { @@ -213,12 +393,85 @@ rule_exists() { fi } -apply_rules() { - local proto="$1" in_port="$2" out_port="$3" target_ip="$4" label="$5" +# Единое ядро применения правил — устраняет дублирование +_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" @@ -226,42 +479,31 @@ apply_rules() { 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 + check_port_conflict "$in_port" "$proto" || return + + backup_rules "pre-add" 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 + 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 - 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..." } +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" @@ -274,20 +516,37 @@ remove_rule() { 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 + -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 state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null + -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 - netfilter-persistent save >/dev/null 2>&1 + 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=() @@ -361,6 +620,18 @@ input_port_or_range() { 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() { @@ -375,20 +646,20 @@ menu_standard_rule() { local port="$REPLY_VALUE" echo "" - apply_rules "$proto" "$port" "$port" "$target_ip" "$label" + 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" - 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_proto + local proto="$REPLY_VALUE" input_ip local target_ip="$REPLY_VALUE" @@ -400,20 +671,20 @@ menu_custom_rule() { local out_port="$REPLY_VALUE" echo "" - apply_rules "$proto" "$in_port" "$out_port" "$target_ip" "Custom" + 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" - 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_proto + local proto="$REPLY_VALUE" input_ip local target_ip="$REPLY_VALUE" @@ -426,56 +697,40 @@ menu_batch_rule() { local start="${BASH_REMATCH[1]}" local end="${BASH_REMATCH[2]}" local count=$(( end - start + 1 )) + + backup_rules "pre-batch" + 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 + 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 - apply_rules "$proto" "$port_range" "$port_range" "$target_ip" "Batch-single" + 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..." } -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" @@ -533,7 +788,6 @@ menu_delete_rule() { 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]}" @@ -543,8 +797,10 @@ menu_delete_rule() { 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..." @@ -560,7 +816,6 @@ menu_flush() { return fi - # Бэкап перед сбросом backup_rules "pre-flush" iptables -P INPUT ACCEPT @@ -579,7 +834,17 @@ menu_flush() { ip6tables -F 2>/dev/null ip6tables -X 2>/dev/null - netfilter-persistent save >/dev/null 2>&1 + 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 "Полный сброс выполнен." @@ -599,6 +864,21 @@ backup_rules() { 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" } @@ -608,6 +888,11 @@ menu_backup() { local f f=$(backup_rules "manual") msg_ok "Бэкап сохранён: $f" + + if command -v ufw &>/dev/null; then + msg_info "UFW конфигурация также сохранена." + fi + read -p "Нажмите Enter..." } @@ -617,7 +902,7 @@ menu_restore() { local files=() while IFS= read -r -d '' f; do files+=("$f") - done < <(find "$BACKUP_DIR" -name "*.v4" -print0 2>/dev/null | sort -z -r) + done < <(find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" -print0 2>/dev/null | sort -z -r) if (( ${#files[@]} == 0 )); then msg_info "Нет доступных бэкапов." @@ -629,7 +914,10 @@ menu_restore() { for f in "${files[@]}"; do local name name=$(basename "$f") - echo -e " ${YELLOW}[$i]${NC} $name" + 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 @@ -647,6 +935,14 @@ menu_restore() { 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" @@ -654,7 +950,16 @@ menu_restore() { if [[ -f "$selected6" ]]; then ip6tables-restore < "$selected6" 2>/dev/null fi - netfilter-persistent save >/dev/null 2>&1 + 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 @@ -737,16 +1042,29 @@ menu_status() { 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" - printf " %-28s %s\n" "UFW:" "$ufw_status" + 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 "" @@ -770,6 +1088,99 @@ menu_log() { 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() { @@ -806,7 +1217,7 @@ show_menu() { 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}4${NC}) Кастомное правило ${DIM}(tcp/udp/both, разные порты)${NC}" echo -e " ${GREEN}5${NC}) Пакетное создание ${DIM}(диапазон портов)${NC}" echo "" echo -e " ${WHITE}Управление:${NC}" @@ -816,10 +1227,11 @@ show_menu() { echo -e " ${GREEN}9${NC}) Проверить доступность целей" echo "" echo -e " ${WHITE}Сервис:${NC}" - echo -e " ${GREEN}b${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 "" @@ -840,6 +1252,7 @@ show_menu() { 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 ;; *) ;; @@ -857,36 +1270,41 @@ show_help() { echo "Команды:" echo " (без аргументов) Интерактивное меню" echo " add [out_port]" - echo " Добавить правило" + echo " Добавить правило (proto: tcp, udp, both)" echo " list Список активных правил" echo " del " echo " Удалить правило" - echo " flush Сбросить все правила" - echo " backup Создать бэкап" - echo " restore Восстановить из бэкапа" + 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]" + 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." + if [[ "$proto" != "tcp" && "$proto" != "udp" && "$proto" != "both" ]]; then + msg_err "Протокол: tcp, udp или both." exit 1 fi if ! validate_ip "$ip"; then @@ -898,39 +1316,27 @@ cli_add() { exit 1 fi - if rule_exists "$ip" "$proto" "$in_port" "$out_port"; then - msg_warn "Правило уже существует." - exit 0 + check_port_conflict "$in_port" "$proto" || exit 1 + + local protos=() + if [[ "$proto" == "both" ]]; then + protos=(tcp udp) + else + protos=("$proto") 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 + backup_rules "pre-add" - 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" + 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() { @@ -952,8 +1358,10 @@ cli_del() { 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() { @@ -968,7 +1376,17 @@ cli_flush() { 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 + 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." } @@ -1006,29 +1424,35 @@ cli_check() { 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 ;; + 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 - netfilter-persistent save >/dev/null 2>&1 + 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 ;; + 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 ;; + "") prepare_system; show_menu ;; + *) msg_err "Неизвестная команда: $1"; show_help; exit 1 ;; esac }