Files
arblark dac6e6d092 feat: v2.0.0 — UFW backup, rollback, lock, rotation, cross-distro support
- 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
2026-04-10 22:52:12 +03:00

1460 lines
54 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#
# cascad — скрипт для настройки каскадных NAT-соединений
# Поддержка: iptables, IPv4/IPv6, диапазоны портов, бэкап, логирование
#
set -euo pipefail
# ── Цвета ───────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
DIM='\033[2m'
NC='\033[0m'
# ── Константы ───────────────────────────────────────────────────────────────────
VERSION="2.0.0"
SCRIPT_NAME="cascad"
GLOBAL_CMD="/usr/local/bin/$SCRIPT_NAME"
LOG_FILE="/var/log/${SCRIPT_NAME}.log"
BACKUP_DIR="/etc/${SCRIPT_NAME}/backups"
CONFIG_DIR="/etc/${SCRIPT_NAME}"
STATE_FILE="/etc/${SCRIPT_NAME}/state"
LOCK_FILE="/var/run/${SCRIPT_NAME}.lock"
REPO_URL="https://raw.githubusercontent.com/arblark/cascad/main/cascad.sh"
MAX_BACKUPS=20
MAX_LOG_SIZE=1048576 # 1 MB
# ── Cleanup & Lock ──────────────────────────────────────────────────────────────
cleanup() {
local exit_code=$?
if [[ -n "${LOCK_FD:-}" ]]; then
eval "exec ${LOCK_FD}>&-" 2>/dev/null
fi
rm -f "$LOCK_FILE" 2>/dev/null
if (( exit_code != 0 )); then
log WARN "Скрипт завершился с кодом $exit_code"
fi
}
trap cleanup EXIT INT TERM
acquire_lock() {
mkdir -p "$(dirname "$LOCK_FILE")"
LOCK_FD=9
eval "exec ${LOCK_FD}>\"$LOCK_FILE\""
if ! flock -n "$LOCK_FD"; then
msg_err "Другой экземпляр $SCRIPT_NAME уже запущен."
exit 1
fi
}
# ── Утилиты ─────────────────────────────────────────────────────────────────────
log() {
local level="$1"; shift
local msg="$*"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$ts] [$level] $msg" >> "$LOG_FILE" 2>/dev/null || true
}
msg_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
msg_err() { echo -e "${RED}[ОШИБКА]${NC} $*"; }
msg_warn() { echo -e "${YELLOW}[!]${NC} $*"; }
msg_info() { echo -e "${CYAN}[i]${NC} $*"; }
confirm() {
local prompt="${1:-Продолжить?}"
echo -ne "${YELLOW}${prompt} (y/n): ${NC}"
read -r ans
[[ "$ans" =~ ^[yYдД]$ ]]
}
# ── Проверки ────────────────────────────────────────────────────────────────────
check_root() {
if [[ "$EUID" -ne 0 ]]; then
msg_err "Запустите скрипт с правами root (sudo)."
exit 1
fi
}
detect_firewall_backend() {
if command -v nft &>/dev/null && nft list ruleset &>/dev/null 2>&1; then
echo "nftables"
elif command -v iptables &>/dev/null; then
echo "iptables"
else
msg_err "Не найден ни iptables, ни nftables!"
exit 1
fi
}
detect_pkg_manager() {
if command -v apt-get &>/dev/null; then
echo "apt"
elif command -v dnf &>/dev/null; then
echo "dnf"
elif command -v yum &>/dev/null; then
echo "yum"
elif command -v pacman &>/dev/null; then
echo "pacman"
else
echo "unknown"
fi
}
detect_interface() {
local iface
iface=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [[ -z "$iface" ]]; then
iface=$(ip -4 route show default | awk '{print $5}' | head -1)
fi
if [[ -z "$iface" ]]; then
msg_err "Не удалось определить сетевой интерфейс."
exit 1
fi
echo "$iface"
}
validate_ip() {
local ip="$1"
# IPv4
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
local IFS='.'
read -ra octets <<< "$ip"
for o in "${octets[@]}"; do
(( o <= 255 )) || return 1
done
return 0
fi
# IPv6 — через python3 или perl для точной валидации
if command -v python3 &>/dev/null; then
python3 -c "import ipaddress; ipaddress.ip_address('$ip')" 2>/dev/null && return 0
elif command -v perl &>/dev/null; then
perl -MSocket -e "exit(defined inet_pton(AF_INET6, '$ip') ? 0 : 1)" 2>/dev/null && return 0
else
# Fallback: базовая проверка формата с защитой от двойного ::
if [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]] && [[ ! "$ip" =~ ::.*:: ]]; then
local colons="${ip//[^:]}"
(( ${#colons} >= 2 && ${#colons} <= 7 )) && return 0
fi
fi
return 1
}
validate_port() {
local port="$1"
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 ))
}
validate_port_range() {
local range="$1"
if [[ "$range" =~ ^([0-9]+):([0-9]+)$ ]]; then
local start="${BASH_REMATCH[1]}"
local end="${BASH_REMATCH[2]}"
validate_port "$start" && validate_port "$end" && (( start <= end ))
elif validate_port "$range"; then
return 0
else
return 1
fi
}
is_ipv6() {
[[ "$1" == *":"* ]]
}
check_port_conflict() {
local port="$1" proto="$2"
local conflict=""
if command -v ss &>/dev/null; then
conflict=$(ss -tlnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {print $NF}' | head -1)
if [[ -z "$conflict" && "$proto" == "udp" ]]; then
conflict=$(ss -ulnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {print $NF}' | head -1)
fi
elif command -v netstat &>/dev/null; then
conflict=$(netstat -tlnp 2>/dev/null | awk -v p=":${port}" '$4 ~ p {print $NF}' | head -1)
fi
if [[ -n "$conflict" ]]; then
msg_warn "Порт $port/$proto уже используется: $conflict"
msg_warn "DNAT-правило перехватит трафик, предназначенный этому сервису!"
if ! confirm "Всё равно продолжить?"; then
return 1
fi
fi
return 0
}
# ── Ротация ─────────────────────────────────────────────────────────────────────
rotate_backups() {
local count
count=$(find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" 2>/dev/null | wc -l)
if (( count > MAX_BACKUPS )); then
local to_delete=$(( count - MAX_BACKUPS ))
find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" -print0 2>/dev/null \
| sort -z | head -z -n "$to_delete" \
| while IFS= read -r -d '' f; do
rm -f "$f" "${f%.v4}.v6" 2>/dev/null
local ufw_dir="${f%.v4}_ufw"
rm -rf "$ufw_dir" 2>/dev/null
done
log INFO "Ротация: удалено $to_delete старых бэкапов"
fi
}
rotate_log() {
if [[ -f "$LOG_FILE" ]]; then
local size
size=$(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null || echo 0)
if (( size > MAX_LOG_SIZE )); then
mv "$LOG_FILE" "${LOG_FILE}.1"
touch "$LOG_FILE"
log INFO "Лог ротирован (предыдущий: ${LOG_FILE}.1)"
fi
fi
}
# ── Состояние (сохранение оригинальных значений) ────────────────────────────────
save_state() {
local key="$1" value="$2"
mkdir -p "$(dirname "$STATE_FILE")"
touch "$STATE_FILE"
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
return
fi
echo "${key}=${value}" >> "$STATE_FILE"
}
load_state() {
local key="$1"
if [[ -f "$STATE_FILE" ]]; then
grep "^${key}=" "$STATE_FILE" 2>/dev/null | tail -1 | cut -d'=' -f2-
fi
}
remove_state() {
local key="$1"
if [[ -f "$STATE_FILE" ]]; then
sed -i "/^${key}=/d" "$STATE_FILE" 2>/dev/null
fi
}
# ── Подготовка системы ──────────────────────────────────────────────────────────
prepare_system() {
mkdir -p "$BACKUP_DIR" "$CONFIG_DIR"
touch "$LOG_FILE"
rotate_log
if [[ ! -f "$GLOBAL_CMD" ]] || [[ "$(readlink -f "$0" 2>/dev/null)" != "$GLOBAL_CMD" ]]; then
if [[ -f "$0" && "$0" != "/dev/"* && "$0" != "/proc/"* ]]; then
cp -f "$0" "$GLOBAL_CMD"
else
msg_info "Скачивание скрипта..."
curl -fsSL "$REPO_URL" -o "$GLOBAL_CMD" || {
msg_err "Не удалось скачать скрипт. Проверьте подключение к интернету."
exit 1
}
fi
chmod +x "$GLOBAL_CMD"
msg_info "Команда '$SCRIPT_NAME' установлена глобально."
log INFO "Скрипт установлен в $GLOBAL_CMD"
fi
# Бэкап sysctl.conf перед любыми модификациями
if [[ -f /etc/sysctl.conf ]]; then
local sysctl_bak="$BACKUP_DIR/sysctl.conf.$(date '+%Y%m%d_%H%M%S').bak"
if [[ ! -f "$BACKUP_DIR/sysctl.conf.original" ]]; then
cp /etc/sysctl.conf "$BACKUP_DIR/sysctl.conf.original"
log INFO "Оригинальный sysctl.conf сохранён"
fi
cp /etc/sysctl.conf "$sysctl_bak"
fi
local changed=0
# IPv4 forwarding
if ! sysctl -n net.ipv4.ip_forward 2>/dev/null | grep -q "1"; then
sysctl -w net.ipv4.ip_forward=1 >/dev/null
changed=1
fi
grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null || {
sed -i 's/^#\?net.ipv4.ip_forward=.*/net.ipv4.ip_forward=1/' /etc/sysctl.conf 2>/dev/null \
|| echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
changed=1
}
# IPv6 forwarding
if ! sysctl -n net.ipv6.conf.all.forwarding 2>/dev/null | grep -q "1"; then
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
fi
grep -q "^net.ipv6.conf.all.forwarding=1" /etc/sysctl.conf 2>/dev/null || {
echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
}
# BBR
if sysctl net.ipv4.tcp_available_congestion_control 2>/dev/null | grep -q bbr; then
grep -q "^net.core.default_qdisc=fq" /etc/sysctl.conf 2>/dev/null || {
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
changed=1
}
grep -q "^net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf 2>/dev/null || {
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
changed=1
}
fi
if (( changed )); then
sysctl -p >/dev/null 2>&1
fi
# Зависимости
local pkg_mgr
pkg_mgr=$(detect_pkg_manager)
case "$pkg_mgr" in
apt)
export DEBIAN_FRONTEND=noninteractive
local deps_needed=()
command -v iptables &>/dev/null || deps_needed+=(iptables)
dpkg -s iptables-persistent &>/dev/null 2>&1 || deps_needed+=(iptables-persistent netfilter-persistent)
if (( ${#deps_needed[@]} > 0 )); then
msg_info "Установка зависимостей: ${deps_needed[*]}..."
apt-get update -y -qq >/dev/null 2>&1
apt-get install -y -qq "${deps_needed[@]}" >/dev/null 2>&1
fi
;;
dnf|yum)
if ! command -v iptables &>/dev/null; then
msg_info "Установка iptables..."
"$pkg_mgr" install -y iptables iptables-services >/dev/null 2>&1
systemctl enable --now iptables 2>/dev/null || true
fi
;;
pacman)
if ! command -v iptables &>/dev/null; then
msg_info "Установка iptables..."
pacman -Sy --noconfirm iptables >/dev/null 2>&1
fi
;;
*)
if ! command -v iptables &>/dev/null; then
msg_err "Не удалось определить пакетный менеджер. Установите iptables вручную."
exit 1
fi
;;
esac
log INFO "Система подготовлена."
}
# ── Сохранение правил (кросс-дистрибутив) ───────────────────────────────────────
persist_rules() {
if command -v netfilter-persistent &>/dev/null; then
netfilter-persistent save >/dev/null 2>&1
elif [[ -f /etc/redhat-release ]] || command -v dnf &>/dev/null || command -v yum &>/dev/null; then
service iptables save 2>/dev/null || true
service ip6tables save 2>/dev/null || true
else
iptables-save > /etc/iptables.rules 2>/dev/null || true
ip6tables-save > /etc/ip6tables.rules 2>/dev/null || true
fi
}
# ── Работа с правилами (iptables) ──────────────────────────────────────────────
ipt_cmd() {
local ip="$1"; shift
if is_ipv6 "$ip"; then
ip6tables "$@"
else
iptables "$@"
fi
}
rule_exists() {
local target_ip="$1" proto="$2" in_port="$3" out_port="$4"
if is_ipv6 "$target_ip"; then
ip6tables -t nat -C PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "[$target_ip]:$out_port" 2>/dev/null
else
iptables -t nat -C PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$target_ip:$out_port" 2>/dev/null
fi
}
# Единое ядро применения правил — устраняет дублирование
_apply_rule_core() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4"
local iface
iface=$(detect_interface)
local dest_fmt="$target_ip:$out_port"
local fwd_dest="$target_ip"
if is_ipv6 "$target_ip"; then
dest_fmt="[$target_ip]:$out_port"
fi
local applied=()
local ok=1
_rollback_rules() {
for (( i=${#applied[@]}-1; i>=0; i-- )); do
eval "${applied[$i]}" 2>/dev/null || true
done
}
ipt_cmd "$target_ip" -A INPUT -p "$proto" --dport "$in_port" -j ACCEPT || { _rollback_rules; ok=0; }
if (( ok )); then
applied+=("ipt_cmd '$target_ip' -D INPUT -p '$proto' --dport '$in_port' -j ACCEPT")
fi
if (( ok )); then
ipt_cmd "$target_ip" -t nat -A PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt" || { _rollback_rules; ok=0; }
if (( ok )); then
applied+=("ipt_cmd '$target_ip' -t nat -D PREROUTING -p '$proto' --dport '$in_port' -j DNAT --to-destination '$dest_fmt'")
fi
fi
if (( ok )); then
if ! ipt_cmd "$target_ip" -t nat -C POSTROUTING -o "$iface" -j MASQUERADE 2>/dev/null; then
ipt_cmd "$target_ip" -t nat -A POSTROUTING -o "$iface" -j MASQUERADE || { _rollback_rules; ok=0; }
fi
fi
if (( ok )); then
ipt_cmd "$target_ip" -A FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT || { _rollback_rules; ok=0; }
if (( ok )); then
applied+=("ipt_cmd '$target_ip' -D FORWARD -p '$proto' -d '$fwd_dest' --dport '$out_port' -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT")
fi
fi
if (( ok )); then
ipt_cmd "$target_ip" -A FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT || { _rollback_rules; ok=0; }
fi
if (( ! ok )); then
msg_err "Ошибка при применении правил. Выполнен откат."
log ERROR "Rollback: $proto $in_port -> $target_ip:$out_port"
return 1
fi
# UFW
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
ufw allow "$in_port/$proto" >/dev/null 2>&1
local current_policy
current_policy=$(grep '^DEFAULT_FORWARD_POLICY=' /etc/default/ufw 2>/dev/null | cut -d'"' -f2)
if [[ "$current_policy" != "ACCEPT" ]]; then
save_state "ufw_forward_policy" "$current_policy"
sed -i 's/DEFAULT_FORWARD_POLICY=".*"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
fi
fi
persist_rules
return 0
}
apply_rules() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4" label="$5"
if rule_exists "$target_ip" "$proto" "$in_port" "$out_port"; then
msg_warn "Правило уже существует: $proto $in_port -> $target_ip:$out_port"
log WARN "Дубликат правила: $proto $in_port -> $target_ip:$out_port"
read -p "Нажмите Enter..."
return
fi
check_port_conflict "$in_port" "$proto" || return
backup_rules "pre-add"
msg_info "Применение правил ($label)..."
if _apply_rule_core "$proto" "$in_port" "$out_port" "$target_ip"; then
msg_ok "$label настроен: $proto вход $in_port -> $target_ip:$out_port"
log INFO "Создано: $label | $proto $in_port -> $target_ip:$out_port | iface=$(detect_interface)"
fi
read -p "Нажмите Enter..."
}
apply_rules_silent() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4"
if _apply_rule_core "$proto" "$in_port" "$out_port" "$target_ip"; then
echo -e " ${GREEN}+${NC} $proto :$in_port -> $target_ip:$out_port"
log INFO "Batch rule: $proto $in_port -> $target_ip:$out_port"
else
echo -e " ${RED}✗${NC} $proto :$in_port -> $target_ip:$out_port (ошибка)"
fi
}
remove_rule() {
local proto="$1" in_port="$2" out_port="$3" target_ip="$4"
local dest_fmt="$target_ip:$out_port"
local fwd_dest="$target_ip"
if is_ipv6 "$target_ip"; then
dest_fmt="[$target_ip]:$out_port"
fi
ipt_cmd "$target_ip" -t nat -D PREROUTING -p "$proto" --dport "$in_port" -j DNAT --to-destination "$dest_fmt" 2>/dev/null
ipt_cmd "$target_ip" -D INPUT -p "$proto" --dport "$in_port" -j ACCEPT 2>/dev/null
ipt_cmd "$target_ip" -D FORWARD -p "$proto" -d "$fwd_dest" --dport "$out_port" \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT 2>/dev/null
ipt_cmd "$target_ip" -D FORWARD -p "$proto" -s "$fwd_dest" --sport "$out_port" \
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null
# UFW
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
ufw delete allow "$in_port/$proto" >/dev/null 2>&1
fi
persist_rules
log INFO "Удалено: $proto $in_port -> $target_ip:$out_port"
}
# ── Восстановление UFW forward policy при удалении последнего правила ──────────
maybe_restore_ufw_policy() {
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
local original_policy
original_policy=$(load_state "ufw_forward_policy")
if [[ -n "$original_policy" ]]; then
sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
remove_state "ufw_forward_policy"
msg_info "UFW FORWARD policy восстановлена: $original_policy"
log INFO "UFW FORWARD policy восстановлена: $original_policy"
fi
fi
}
# ── Парсер активных правил ──────────────────────────────────────────────────────
declare -a PARSED_RULES=()
parse_active_rules() {
PARSED_RULES=()
local line port proto dest
while read -r line; do
port=$(echo "$line" | grep -oP '(?<=--dport )\S+')
proto=$(echo "$line" | grep -oP '(?<=-p )\w+')
dest=$(echo "$line" | grep -oP '(?<=--to-destination )\S+')
if [[ -n "$port" && -n "$proto" && -n "$dest" ]]; then
PARSED_RULES+=("$port|$proto|$dest")
fi
done < <(iptables -t nat -S PREROUTING 2>/dev/null | grep "DNAT")
# IPv6
while read -r line; do
port=$(echo "$line" | grep -oP '(?<=--dport )\S+')
proto=$(echo "$line" | grep -oP '(?<=-p )\w+')
dest=$(echo "$line" | grep -oP '(?<=--to-destination )\S+')
if [[ -n "$port" && -n "$proto" && -n "$dest" ]]; then
PARSED_RULES+=("$port|$proto|$dest")
fi
done < <(ip6tables -t nat -S PREROUTING 2>/dev/null | grep "DNAT")
}
# ── Интерактивные функции ───────────────────────────────────────────────────────
REPLY_VALUE=""
input_ip() {
REPLY_VALUE=""
local ip
while true; do
echo -ne " IP адрес назначения: " >/dev/tty
read -r ip </dev/tty
if validate_ip "$ip"; then
REPLY_VALUE="$ip"
return
fi
msg_err "Некорректный IP. Поддерживаются IPv4 и IPv6." >/dev/tty
done
}
input_port() {
REPLY_VALUE=""
local label="$1" port
while true; do
echo -ne " $label: " >/dev/tty
read -r port </dev/tty
if validate_port "$port"; then
REPLY_VALUE="$port"
return
fi
msg_err "Порт: число 1-65535." >/dev/tty
done
}
input_port_or_range() {
REPLY_VALUE=""
local label="$1" val
while true; do
echo -ne " $label (число или диапазон N:M): " >/dev/tty
read -r val </dev/tty
if validate_port_range "$val"; then
REPLY_VALUE="$val"
return
fi
msg_err "Формат: одиночный порт (1-65535) или диапазон (start:end)." >/dev/tty
done
}
input_proto() {
REPLY_VALUE=""
local proto
while true; do
echo -ne " Протокол (${YELLOW}tcp${NC}/${YELLOW}udp${NC}/${YELLOW}both${NC}): "
read -r proto
[[ "$proto" == "tcp" || "$proto" == "udp" || "$proto" == "both" ]] && break
msg_err "Введите tcp, udp или both."
done
REPLY_VALUE="$proto"
}
# ── Пункты меню ─────────────────────────────────────────────────────────────────
menu_standard_rule() {
local proto="$1" label="$2"
echo -e "\n${CYAN}── Настройка $label ($proto) ──${NC}\n"
input_ip
local target_ip="$REPLY_VALUE"
input_port "Порт (одинаковый вход/выход)"
local port="$REPLY_VALUE"
echo ""
if [[ "$proto" == "both" ]]; then
apply_rules "tcp" "$port" "$port" "$target_ip" "$label (TCP)"
apply_rules "udp" "$port" "$port" "$target_ip" "$label (UDP)"
else
apply_rules "$proto" "$port" "$port" "$target_ip" "$label"
fi
}
menu_custom_rule() {
echo -e "\n${CYAN}── Кастомное правило ──${NC}"
echo -e "${DIM}Позволяет задать разные порты входа и выхода, любой протокол.${NC}\n"
input_proto
local proto="$REPLY_VALUE"
input_ip
local target_ip="$REPLY_VALUE"
input_port "Входящий порт (на этом сервере)"
local in_port="$REPLY_VALUE"
input_port "Исходящий порт (на целевом сервере)"
local out_port="$REPLY_VALUE"
echo ""
if [[ "$proto" == "both" ]]; then
apply_rules "tcp" "$in_port" "$out_port" "$target_ip" "Custom (TCP)"
apply_rules "udp" "$in_port" "$out_port" "$target_ip" "Custom (UDP)"
else
apply_rules "$proto" "$in_port" "$out_port" "$target_ip" "Custom"
fi
}
menu_batch_rule() {
echo -e "\n${CYAN}── Пакетное создание правил ──${NC}"
echo -e "${DIM}Создаёт несколько правил за один раз через диапазон портов.${NC}\n"
input_proto
local proto="$REPLY_VALUE"
input_ip
local target_ip="$REPLY_VALUE"
input_port_or_range "Диапазон портов"
local port_range="$REPLY_VALUE"
echo ""
if [[ "$port_range" =~ ^([0-9]+):([0-9]+)$ ]]; then
local start="${BASH_REMATCH[1]}"
local end="${BASH_REMATCH[2]}"
local count=$(( end - start + 1 ))
backup_rules "pre-batch"
msg_info "Создаётся $count правил ($start-$end)..."
local protos=()
if [[ "$proto" == "both" ]]; then
protos=(tcp udp)
else
protos=("$proto")
fi
for p_proto in "${protos[@]}"; do
for (( p=start; p<=end; p++ )); do
if rule_exists "$target_ip" "$p_proto" "$p" "$p"; then
msg_warn "Пропуск $p_proto :$p (уже существует)"
else
apply_rules_silent "$p_proto" "$p" "$p" "$target_ip"
fi
done
done
msg_ok "Пакетное создание завершено ($count портов)."
log INFO "Batch: $proto $start:$end -> $target_ip"
else
if [[ "$proto" == "both" ]]; then
apply_rules "tcp" "$port_range" "$port_range" "$target_ip" "Batch-single (TCP)"
apply_rules "udp" "$port_range" "$port_range" "$target_ip" "Batch-single (UDP)"
else
apply_rules "$proto" "$port_range" "$port_range" "$target_ip" "Batch-single"
fi
return
fi
read -p "Нажмите Enter..."
}
menu_list_rules() {
echo -e "\n${CYAN}── Активные переадресации ──${NC}\n"
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
msg_info "Нет активных правил."
read -p "Нажмите Enter..."
return
fi
printf " ${MAGENTA}%-6s %-8s %-8s %s${NC}\n" "#" "ВХОД" "ПРОТО" "ЦЕЛЬ"
printf " ${DIM}%-6s %-8s %-8s %s${NC}\n" "──" "────" "─────" "────"
local i=1
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
printf " %-6s %-8s %-8s %s\n" "$i" "$port" "$proto" "$dest"
((i++))
done
echo ""
read -p "Нажмите Enter..."
}
menu_delete_rule() {
echo -e "\n${CYAN}── Удаление правила ──${NC}\n"
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
msg_info "Нет активных правил."
read -p "Нажмите Enter..."
return
fi
local i=1
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
echo -e " ${YELLOW}[$i]${NC} $proto :$port -> $dest"
((i++))
done
echo ""
echo -ne " Номер правила для удаления (0 — отмена): "
read -r rule_num
if [[ "$rule_num" == "0" || -z "$rule_num" ]]; then return; fi
if ! [[ "$rule_num" =~ ^[0-9]+$ ]] || (( rule_num < 1 || rule_num > ${#PARSED_RULES[@]} )); then
msg_err "Некорректный номер."
read -p "Нажмите Enter..."
return
fi
local idx=$(( rule_num - 1 ))
IFS='|' read -r d_port d_proto d_dest <<< "${PARSED_RULES[$idx]}"
local target_ip target_port
if [[ "$d_dest" =~ ^\[(.+)\]:(.+)$ ]]; then
target_ip="${BASH_REMATCH[1]}"
target_port="${BASH_REMATCH[2]}"
else
target_ip="${d_dest%:*}"
target_port="${d_dest##*:}"
fi
if confirm "Удалить правило $d_proto :$d_port -> $d_dest?"; then
backup_rules "pre-delete"
remove_rule "$d_proto" "$d_port" "$target_port" "$target_ip"
msg_ok "Правило удалено."
maybe_restore_ufw_policy
fi
read -p "Нажмите Enter..."
}
menu_flush() {
echo -e "\n${RED}── Полный сброс правил ──${NC}\n"
msg_warn "Будут удалены ВСЕ правила iptables (NAT, FORWARD, INPUT)."
msg_warn "Это может нарушить работу других сервисов!"
echo ""
if ! confirm "Вы уверены?"; then
return
fi
backup_rules "pre-flush"
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
iptables -t nat -F
iptables -t mangle -F
iptables -F
iptables -X
# IPv6
ip6tables -P INPUT ACCEPT 2>/dev/null
ip6tables -P FORWARD ACCEPT 2>/dev/null
ip6tables -P OUTPUT ACCEPT 2>/dev/null
ip6tables -t nat -F 2>/dev/null
ip6tables -F 2>/dev/null
ip6tables -X 2>/dev/null
persist_rules
# Восстановление UFW forward policy
local original_policy
original_policy=$(load_state "ufw_forward_policy")
if [[ -n "$original_policy" ]] && command -v ufw &>/dev/null; then
sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
remove_state "ufw_forward_policy"
msg_info "UFW FORWARD policy восстановлена: $original_policy"
fi
msg_ok "Все правила сброшены. Бэкап сохранён в $BACKUP_DIR"
log INFO "Полный сброс выполнен."
read -p "Нажмите Enter..."
}
# ── Бэкап / Восстановление ─────────────────────────────────────────────────────
backup_rules() {
local label="${1:-manual}"
local ts
ts=$(date '+%Y%m%d_%H%M%S')
local file="$BACKUP_DIR/${ts}_${label}.v4"
local file6="$BACKUP_DIR/${ts}_${label}.v6"
iptables-save > "$file" 2>/dev/null
ip6tables-save > "$file6" 2>/dev/null
# Бэкап UFW конфигурации
if command -v ufw &>/dev/null; then
local ufw_dir="$BACKUP_DIR/${ts}_${label}_ufw"
mkdir -p "$ufw_dir"
cp /etc/default/ufw "$ufw_dir/" 2>/dev/null || true
cp /etc/ufw/before.rules "$ufw_dir/" 2>/dev/null || true
cp /etc/ufw/after.rules "$ufw_dir/" 2>/dev/null || true
cp /etc/ufw/before6.rules "$ufw_dir/" 2>/dev/null || true
cp /etc/ufw/after6.rules "$ufw_dir/" 2>/dev/null || true
cp /etc/ufw/user.rules "$ufw_dir/" 2>/dev/null || true
cp /etc/ufw/user6.rules "$ufw_dir/" 2>/dev/null || true
fi
rotate_backups
log INFO "Бэкап создан: $file, $file6"
echo "$file"
}
menu_backup() {
echo -e "\n${CYAN}── Бэкап правил ──${NC}\n"
local f
f=$(backup_rules "manual")
msg_ok "Бэкап сохранён: $f"
if command -v ufw &>/dev/null; then
msg_info "UFW конфигурация также сохранена."
fi
read -p "Нажмите Enter..."
}
menu_restore() {
echo -e "\n${CYAN}── Восстановление из бэкапа ──${NC}\n"
local files=()
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" -print0 2>/dev/null | sort -z -r)
if (( ${#files[@]} == 0 )); then
msg_info "Нет доступных бэкапов."
read -p "Нажмите Enter..."
return
fi
local i=1
for f in "${files[@]}"; do
local name
name=$(basename "$f")
local ufw_marker=""
local ufw_dir="${f%.v4}_ufw"
[[ -d "$ufw_dir" ]] && ufw_marker=" ${DIM}(+UFW)${NC}"
echo -e " ${YELLOW}[$i]${NC} $name$ufw_marker"
((i++))
done
echo ""
echo -ne " Номер бэкапа для восстановления (0 — отмена): "
read -r choice
if [[ "$choice" == "0" || -z "$choice" ]]; then return; fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#files[@]} )); then
msg_err "Некорректный номер."
read -p "Нажмите Enter..."
return
fi
local idx=$(( choice - 1 ))
local selected="${files[$idx]}"
local selected6="${selected%.v4}.v6"
local selected_ufw="${selected%.v4}_ufw"
local restore_ufw=0
if [[ -d "$selected_ufw" ]]; then
if confirm "Также восстановить конфигурацию UFW?"; then
restore_ufw=1
fi
fi
if confirm "Восстановить из $(basename "$selected")?"; then
backup_rules "pre-restore"
iptables-restore < "$selected" 2>/dev/null
if [[ -f "$selected6" ]]; then
ip6tables-restore < "$selected6" 2>/dev/null
fi
if (( restore_ufw )) && [[ -d "$selected_ufw" ]]; then
cp "$selected_ufw"/ufw "$selected_ufw"/* /etc/default/ 2>/dev/null || true
# Восстановление файлов правил UFW
for uf in before.rules after.rules before6.rules after6.rules user.rules user6.rules; do
[[ -f "$selected_ufw/$uf" ]] && cp "$selected_ufw/$uf" "/etc/ufw/$uf" 2>/dev/null
done
ufw reload >/dev/null 2>&1
msg_ok "Конфигурация UFW восстановлена."
fi
persist_rules
msg_ok "Правила восстановлены."
log INFO "Восстановление из $selected"
fi
read -p "Нажмите Enter..."
}
# ── Проверка доступности целевого сервера ───────────────────────────────────────
menu_check_target() {
echo -e "\n${CYAN}── Проверка доступности ──${NC}\n"
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
msg_info "Нет активных правил для проверки."
read -p "Нажмите Enter..."
return
fi
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
local target_ip target_port
if [[ "$dest" =~ ^\[(.+)\]:(.+)$ ]]; then
target_ip="${BASH_REMATCH[1]}"
target_port="${BASH_REMATCH[2]}"
else
target_ip="${dest%:*}"
target_port="${dest##*:}"
fi
echo -ne " $proto :$port -> $dest ... "
if command -v nc &>/dev/null; then
if [[ "$proto" == "tcp" ]]; then
if nc -z -w3 "$target_ip" "$target_port" 2>/dev/null; then
echo -e "${GREEN}ДОСТУПЕН${NC}"
else
echo -e "${RED}НЕДОСТУПЕН${NC}"
fi
else
echo -e "${DIM}UDP (не проверяется)${NC}"
fi
else
if timeout 3 bash -c "echo >/dev/tcp/$target_ip/$target_port" 2>/dev/null; then
echo -e "${GREEN}ДОСТУПЕН${NC}"
else
echo -e "${RED}НЕДОСТУПЕН${NC}"
fi
fi
done
echo ""
read -p "Нажмите Enter..."
}
# ── Статус системы ──────────────────────────────────────────────────────────────
menu_status() {
echo -e "\n${CYAN}── Статус системы ──${NC}\n"
local iface
iface=$(detect_interface)
local backend
backend=$(detect_firewall_backend)
local bbr_active="нет"
sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr && bbr_active="да"
local fwd_v4
fwd_v4=$(sysctl -n net.ipv4.ip_forward 2>/dev/null)
local fwd_v6
fwd_v6=$(sysctl -n net.ipv6.conf.all.forwarding 2>/dev/null)
parse_active_rules
local rule_count=${#PARSED_RULES[@]}
local ufw_status="не установлен"
if command -v ufw &>/dev/null; then
ufw_status=$(ufw status 2>/dev/null | head -1)
local saved_policy
saved_policy=$(load_state "ufw_forward_policy")
if [[ -n "$saved_policy" ]]; then
ufw_status="$ufw_status ${DIM}(original FORWARD: $saved_policy)${NC}"
fi
fi
local backup_count
backup_count=$(find "$BACKUP_DIR" -maxdepth 1 -name "*.v4" 2>/dev/null | wc -l)
local pkg_mgr
pkg_mgr=$(detect_pkg_manager)
printf " %-28s %s\n" "Версия:" "$VERSION"
printf " %-28s %s\n" "Сетевой интерфейс:" "$iface"
printf " %-28s %s\n" "Firewall backend:" "$backend"
printf " %-28s %s\n" "Пакетный менеджер:" "$pkg_mgr"
printf " %-28s %s\n" "IPv4 forwarding:" "$fwd_v4"
printf " %-28s %s\n" "IPv6 forwarding:" "${fwd_v6:-N/A}"
printf " %-28s %s\n" "Google BBR:" "$bbr_active"
echo -e " $(printf '%-28s' "UFW:") $ufw_status"
printf " %-28s %s\n" "Активных правил:" "$rule_count"
printf " %-28s %s\n" "Бэкапов:" "$backup_count"
printf " %-28s %s\n" "Лог:" "$LOG_FILE"
printf " %-28s %s\n" "Бэкапы:" "$BACKUP_DIR"
echo ""
read -p "Нажмите Enter..."
}
# ── Просмотр лога ──────────────────────────────────────────────────────────────
menu_log() {
echo -e "\n${CYAN}── Последние записи лога ──${NC}\n"
if [[ ! -s "$LOG_FILE" ]]; then
msg_info "Лог пуст."
else
tail -20 "$LOG_FILE" | while IFS= read -r line; do
echo " $line"
done
fi
echo ""
read -p "Нажмите Enter..."
}
# ── Обновление скрипта ──────────────────────────────────────────────────────────
cli_update() {
msg_info "Проверка обновлений..."
local tmp
tmp=$(mktemp)
curl -fsSL "$REPO_URL" -o "$tmp" || {
msg_err "Не удалось скачать обновление. Проверьте подключение."
rm -f "$tmp"
return 1
}
local remote_ver
remote_ver=$(grep '^VERSION=' "$tmp" | head -1 | cut -d'"' -f2)
if [[ -z "$remote_ver" ]]; then
msg_err "Не удалось определить версию в загруженном файле."
rm -f "$tmp"
return 1
fi
if [[ "$remote_ver" == "$VERSION" ]]; then
msg_info "Уже установлена последняя версия ($VERSION)."
rm -f "$tmp"
return 0
fi
msg_info "Доступна новая версия: $VERSION -> $remote_ver"
if confirm "Обновить?"; then
backup_rules "pre-update"
cp "$tmp" "$GLOBAL_CMD"
chmod +x "$GLOBAL_CMD"
rm -f "$tmp"
msg_ok "Обновлено: $VERSION -> $remote_ver"
log INFO "Обновление: $VERSION -> $remote_ver"
msg_info "Перезапустите скрипт для применения."
else
rm -f "$tmp"
fi
}
# ── Удаление скрипта ────────────────────────────────────────────────────────────
cli_uninstall() {
echo -e "\n${RED}── Удаление $SCRIPT_NAME ──${NC}\n"
msg_warn "Будут удалены:"
msg_warn " - $GLOBAL_CMD"
msg_warn " - $CONFIG_DIR (включая бэкапы)"
msg_warn " - $LOG_FILE"
echo ""
local flush_rules=0
if confirm "Также сбросить все правила iptables?"; then
flush_rules=1
fi
if ! confirm "Вы уверены, что хотите удалить $SCRIPT_NAME?"; then
return
fi
if (( flush_rules )); then
backup_rules "pre-uninstall"
iptables -P INPUT ACCEPT; iptables -P FORWARD ACCEPT; iptables -P OUTPUT ACCEPT
iptables -t nat -F; iptables -t mangle -F; iptables -F; iptables -X
ip6tables -P INPUT ACCEPT 2>/dev/null; ip6tables -P FORWARD ACCEPT 2>/dev/null
ip6tables -P OUTPUT ACCEPT 2>/dev/null
ip6tables -t nat -F 2>/dev/null; ip6tables -F 2>/dev/null; ip6tables -X 2>/dev/null
persist_rules
local original_policy
original_policy=$(load_state "ufw_forward_policy")
if [[ -n "$original_policy" ]] && command -v ufw &>/dev/null; then
sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
fi
msg_ok "Правила iptables сброшены."
fi
# Восстановление sysctl.conf если есть оригинал
if [[ -f "$BACKUP_DIR/sysctl.conf.original" ]]; then
if confirm "Восстановить оригинальный sysctl.conf?"; then
cp "$BACKUP_DIR/sysctl.conf.original" /etc/sysctl.conf
sysctl -p >/dev/null 2>&1
msg_ok "sysctl.conf восстановлен."
fi
fi
rm -f "$GLOBAL_CMD"
rm -rf "$CONFIG_DIR"
rm -f "$LOG_FILE" "${LOG_FILE}.1"
msg_ok "$SCRIPT_NAME полностью удалён."
log INFO "Uninstall выполнен." 2>/dev/null || true
}
# ── Инструкция ──────────────────────────────────────────────────────────────────
show_instructions() {
clear
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Как настроить каскадное соединение ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${WHITE}Принцип:${NC} Клиент -> Этот сервер (мост) -> Зарубежный сервер"
echo ""
echo -e " ${YELLOW}Шаг 1.${NC} Подготовьте IP и порт зарубежного сервера."
echo -e " ${YELLOW}Шаг 2.${NC} Выберите протокол в меню и введите данные."
echo -e " ${YELLOW}Шаг 3.${NC} В клиенте (AmneziaWG, v2rayNG, и т.д.) замените"
echo -e " адрес сервера на IP ${GREEN}этого${NC} сервера."
echo -e " ${YELLOW}Шаг 4.${NC} Если порты разные — укажите входящий порт в клиенте."
echo ""
echo -e " ${DIM}Трафик пойдёт через ядро Linux (NAT iptables) без"
echo -e " дополнительных процессов — максимальная скорость.${NC}"
echo ""
read -p "Нажмите Enter..."
}
# ── Главное меню ────────────────────────────────────────────────────────────────
show_menu() {
while true; do
clear
echo ""
echo -e "${CYAN} ──────────────────────────────────────────${NC}"
echo -e " ${WHITE}CASCAD${NC} ${DIM}v${VERSION}${NC} — каскадное NAT-перенаправление"
echo -e "${CYAN} ──────────────────────────────────────────${NC}"
echo ""
echo -e " ${WHITE}Настройка правил:${NC}"
echo -e " ${GREEN}1${NC}) AmneziaWG / WireGuard ${DIM}(UDP)${NC}"
echo -e " ${GREEN}2${NC}) VLESS / XRay / Reality ${DIM}(TCP)${NC}"
echo -e " ${GREEN}3${NC}) MTProto / TProxy ${DIM}(TCP)${NC}"
echo -e " ${GREEN}4${NC}) Кастомное правило ${DIM}(tcp/udp/both, разные порты)${NC}"
echo -e " ${GREEN}5${NC}) Пакетное создание ${DIM}(диапазон портов)${NC}"
echo ""
echo -e " ${WHITE}Управление:${NC}"
echo -e " ${GREEN}6${NC}) Список активных правил"
echo -e " ${GREEN}7${NC}) Удалить правило"
echo -e " ${GREEN}8${NC}) Сбросить ВСЕ правила ${DIM}(с автобэкапом)${NC}"
echo -e " ${GREEN}9${NC}) Проверить доступность целей"
echo ""
echo -e " ${WHITE}Сервис:${NC}"
echo -e " ${GREEN}b${NC}) Создать бэкап ${DIM}(iptables + UFW)${NC}"
echo -e " ${GREEN}r${NC}) Восстановить из бэкапа"
echo -e " ${GREEN}s${NC}) Статус системы"
echo -e " ${GREEN}l${NC}) Просмотр лога"
echo -e " ${GREEN}u${NC}) Обновить скрипт"
echo -e " ${GREEN}h${NC}) Инструкция"
echo -e " ${GREEN}0${NC}) Выход"
echo ""
echo -ne " Выбор: "
read -r choice
case $choice in
1) menu_standard_rule "udp" "AmneziaWG / WireGuard" ;;
2) menu_standard_rule "tcp" "VLESS / XRay" ;;
3) menu_standard_rule "tcp" "MTProto / TProxy" ;;
4) menu_custom_rule ;;
5) menu_batch_rule ;;
6) menu_list_rules ;;
7) menu_delete_rule ;;
8) menu_flush ;;
9) menu_check_target ;;
b|B) menu_backup ;;
r|R) menu_restore ;;
s|S) menu_status ;;
l|L) menu_log ;;
u|U) cli_update; read -p "Нажмите Enter..." ;;
h|H) show_instructions ;;
0) echo -e "\n${DIM} До встречи.${NC}\n"; exit 0 ;;
*) ;;
esac
done
}
# ── CLI-режим ───────────────────────────────────────────────────────────────────
show_help() {
echo "cascad v${VERSION} — каскадное NAT-перенаправление"
echo ""
echo "Использование: $SCRIPT_NAME [команда] [аргументы]"
echo ""
echo "Команды:"
echo " (без аргументов) Интерактивное меню"
echo " add <proto> <ip> <in_port> [out_port]"
echo " Добавить правило (proto: tcp, udp, both)"
echo " list Список активных правил"
echo " del <proto> <ip> <in_port> <out_port>"
echo " Удалить правило"
echo " flush [-y] Сбросить все правила"
echo " backup Создать бэкап (iptables + UFW)"
echo " restore [file] Восстановить из бэкапа"
echo " status Показать статус системы"
echo " check Проверить доступность целей"
echo " update Обновить скрипт до последней версии"
echo " uninstall Полностью удалить $SCRIPT_NAME"
echo " help Эта справка"
echo ""
echo "Примеры:"
echo " $SCRIPT_NAME add tcp 203.0.113.10 443"
echo " $SCRIPT_NAME add udp 203.0.113.10 51820 51821"
echo " $SCRIPT_NAME add both 203.0.113.10 443"
echo " $SCRIPT_NAME list"
echo " $SCRIPT_NAME del tcp 203.0.113.10 443 443"
echo " $SCRIPT_NAME update"
echo " $SCRIPT_NAME uninstall"
}
cli_add() {
local proto="${1:-}" ip="${2:-}" in_port="${3:-}" out_port="${4:-}"
if [[ -z "$proto" || -z "$ip" || -z "$in_port" ]]; then
msg_err "Формат: $SCRIPT_NAME add <tcp|udp|both> <ip> <in_port> [out_port]"
exit 1
fi
[[ -z "$out_port" ]] && out_port="$in_port"
if [[ "$proto" != "tcp" && "$proto" != "udp" && "$proto" != "both" ]]; then
msg_err "Протокол: tcp, udp или both."
exit 1
fi
if ! validate_ip "$ip"; then
msg_err "Некорректный IP: $ip"
exit 1
fi
if ! validate_port "$in_port" || ! validate_port "$out_port"; then
msg_err "Некорректный порт."
exit 1
fi
check_port_conflict "$in_port" "$proto" || exit 1
local protos=()
if [[ "$proto" == "both" ]]; then
protos=(tcp udp)
else
protos=("$proto")
fi
backup_rules "pre-add"
for p in "${protos[@]}"; do
if rule_exists "$ip" "$p" "$in_port" "$out_port"; then
msg_warn "Правило $p уже существует."
continue
fi
if _apply_rule_core "$p" "$in_port" "$out_port" "$ip"; then
msg_ok "Добавлено: $p :$in_port -> $ip:$out_port"
log INFO "CLI add: $p $in_port -> $ip:$out_port"
fi
done
}
cli_list() {
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
echo "Нет активных правил."
return
fi
printf "%-8s %-8s %s\n" "ВХОД" "ПРОТО" "ЦЕЛЬ"
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
printf "%-8s %-8s %s\n" "$port" "$proto" "$dest"
done
}
cli_del() {
local proto="${1:-}" ip="${2:-}" in_port="${3:-}" out_port="${4:-}"
if [[ -z "$proto" || -z "$ip" || -z "$in_port" || -z "$out_port" ]]; then
msg_err "Формат: $SCRIPT_NAME del <tcp|udp> <ip> <in_port> <out_port>"
exit 1
fi
backup_rules "pre-delete"
remove_rule "$proto" "$in_port" "$out_port" "$ip"
msg_ok "Удалено: $proto :$in_port -> $ip:$out_port"
maybe_restore_ufw_policy
}
cli_flush() {
if [[ "${1:-}" != "-y" ]]; then
if ! confirm "Сбросить ВСЕ правила iptables?"; then
exit 0
fi
fi
backup_rules "cli-flush"
iptables -P INPUT ACCEPT; iptables -P FORWARD ACCEPT; iptables -P OUTPUT ACCEPT
iptables -t nat -F; iptables -t mangle -F; iptables -F; iptables -X
ip6tables -P INPUT ACCEPT 2>/dev/null; ip6tables -P FORWARD ACCEPT 2>/dev/null
ip6tables -P OUTPUT ACCEPT 2>/dev/null
ip6tables -t nat -F 2>/dev/null; ip6tables -F 2>/dev/null; ip6tables -X 2>/dev/null
persist_rules
local original_policy
original_policy=$(load_state "ufw_forward_policy")
if [[ -n "$original_policy" ]] && command -v ufw &>/dev/null; then
sed -i "s/DEFAULT_FORWARD_POLICY=\".*\"/DEFAULT_FORWARD_POLICY=\"$original_policy\"/" /etc/default/ufw 2>/dev/null
ufw reload >/dev/null 2>&1
remove_state "ufw_forward_policy"
msg_info "UFW FORWARD policy восстановлена: $original_policy"
fi
msg_ok "Все правила сброшены."
log INFO "CLI flush."
}
cli_check() {
parse_active_rules
if (( ${#PARSED_RULES[@]} == 0 )); then
echo "Нет активных правил."
return
fi
for rule in "${PARSED_RULES[@]}"; do
IFS='|' read -r port proto dest <<< "$rule"
local target_ip target_port
if [[ "$dest" =~ ^\[(.+)\]:(.+)$ ]]; then
target_ip="${BASH_REMATCH[1]}"
target_port="${BASH_REMATCH[2]}"
else
target_ip="${dest%:*}"
target_port="${dest##*:}"
fi
printf "$proto :$port -> $dest ... "
if [[ "$proto" == "tcp" ]]; then
if timeout 3 bash -c "echo >/dev/tcp/$target_ip/$target_port" 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}FAIL${NC}"
fi
else
echo "UDP"
fi
done
}
# ── Точка входа ─────────────────────────────────────────────────────────────────
main() {
check_root
acquire_lock
case "${1:-}" in
add) shift; prepare_system; cli_add "$@" ;;
list) cli_list ;;
del) shift; cli_del "$@" ;;
flush) shift; prepare_system; cli_flush "$@" ;;
backup) prepare_system; menu_backup ;;
restore)
shift
prepare_system
if [[ -n "${1:-}" ]]; then
backup_rules "pre-restore"
iptables-restore < "$1" 2>/dev/null
local v6="${1%.v4}.v6"
[[ -f "$v6" ]] && ip6tables-restore < "$v6" 2>/dev/null
persist_rules
msg_ok "Восстановлено из $1"
else
menu_restore
fi
;;
status) prepare_system; menu_status ;;
check) prepare_system; cli_check ;;
update) prepare_system; cli_update ;;
uninstall) prepare_system; cli_uninstall ;;
help|-h|--help) show_help ;;
"") prepare_system; show_menu ;;
*) msg_err "Неизвестная команда: $1"; show_help; exit 1 ;;
esac
}
main "$@"