1029 lines
38 KiB
Bash
Executable File
1029 lines
38 KiB
Bash
Executable File
#!/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 <proto> <ip> <in_port> [out_port]"
|
||
echo " Добавить правило"
|
||
echo " list Список активных правил"
|
||
echo " del <proto> <ip> <in_port> <out_port>"
|
||
echo " Удалить правило"
|
||
echo " flush Сбросить все правила"
|
||
echo " backup Создать бэкап"
|
||
echo " restore <file> Восстановить из бэкапа"
|
||
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 <tcp|udp> <ip> <in_port> [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 <tcp|udp> <ip> <in_port> <out_port>"
|
||
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 "$@"
|