304 lines
9.3 KiB
Bash
Executable file
304 lines
9.3 KiB
Bash
Executable file
#!/bin/bash
|
||
|
||
# Скрипт для domain-based routing под RHEL с firewalld, ipset, dnsmasq
|
||
# Направляет трафик определённых доменов через отдельный интерфейс/IP-набор
|
||
|
||
set -euo pipefail
|
||
|
||
# ============================================
|
||
# КОНФИГУРАЦИЯ
|
||
# ============================================
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
CONFIG_FILE="$SCRIPT_DIR/domain-routing.env"
|
||
|
||
# Загрузка конфигурации
|
||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||
echo "❌ Ошибка: не найден конфигурационный файл $CONFIG_FILE"
|
||
echo "Создайте файл на основе domain-routing.env.example"
|
||
exit 1
|
||
fi
|
||
|
||
# shellcheck source=domain-routing.env
|
||
source "$CONFIG_FILE"
|
||
|
||
# ============================================
|
||
# ВАЛИДАЦИЯ ПЕРЕМЕННЫХ
|
||
# ============================================
|
||
required_vars=(
|
||
"DOMAINS_FILE"
|
||
"IPSET_NAME"
|
||
"FIREWALL_ZONE"
|
||
"DNSMASQ_CONF_DIR"
|
||
"RESOLV_CONF"
|
||
)
|
||
|
||
for var in "${required_vars[@]}"; do
|
||
[[ -z "${!var:-}" ]] && { echo "❌ Переменная $var не установлена в $CONFIG_FILE"; exit 1; }
|
||
done
|
||
|
||
# ============================================
|
||
# ФУНКЦИИ
|
||
# ============================================
|
||
|
||
log_info() {
|
||
echo "ℹ️ $1"
|
||
logger -t "domain-routing" "$1"
|
||
}
|
||
|
||
log_error() {
|
||
echo "❌ $1"
|
||
logger -t "domain-routing" "ERROR: $1"
|
||
}
|
||
|
||
check_dependencies() {
|
||
log_info "Проверка зависимостей..."
|
||
|
||
local missing=()
|
||
|
||
for cmd in firewall-cmd ipset dnsmasq; do
|
||
if ! command -v "$cmd" &> /dev/null; then
|
||
missing+=("$cmd")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
log_error "Отсутствуют команды: ${missing[*]}"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "Все зависимости установлены"
|
||
}
|
||
|
||
create_ipset() {
|
||
log_info "Создание ipset '$IPSET_NAME'..."
|
||
|
||
# Удаляем старый ipset если есть
|
||
if ipset list "$IPSET_NAME" &> /dev/null; then
|
||
ipset destroy "$IPSET_NAME"
|
||
log_info "Удалён старый ipset '$IPSET_NAME'"
|
||
fi
|
||
|
||
# Создаём новый ipset (hash:net для IP-сетей)
|
||
ipset create "$IPSET_NAME" hash:net hashsize 4096 family inet
|
||
log_info "Создан ipset '$IPSET_NAME'"
|
||
}
|
||
|
||
resolve_domains_to_ips() {
|
||
log_info "Разрешение доменов в IP-адреса..."
|
||
|
||
local temp_file=$(mktemp)
|
||
local resolved_count=0
|
||
|
||
while IFS= read -r domain || [[ -n "$domain" ]]; do
|
||
# Пропускаем комментарии и пустые строки
|
||
[[ -z "$domain" ]] && continue
|
||
[[ "$domain" =~ ^[[:space:]]*# ]] && continue
|
||
|
||
# Очищаем домен от лишних символов
|
||
domain=$(echo "$domain" | tr -d '[:space:]')
|
||
|
||
# Разрешаем домен в IP
|
||
local ips
|
||
ips=$(dig +short "$domain" A 2>/dev/null || true)
|
||
|
||
if [[ -n "$ips" ]]; then
|
||
echo "$ips" | while read -r ip; do
|
||
if [[ -n "$ip" ]]; then
|
||
echo "$ip" >> "$temp_file"
|
||
((resolved_count++)) || true
|
||
fi
|
||
done
|
||
else
|
||
log_error "Не удалось разрешить домен: $domain"
|
||
fi
|
||
done < "$DOMAINS_FILE"
|
||
|
||
# Добавляем IP в ipset
|
||
if [[ -s "$temp_file" ]]; then
|
||
while read -r ip; do
|
||
ipset add "$IPSET_NAME" "$ip"
|
||
done < "$temp_file"
|
||
|
||
log_info "Добавлено ${resolved_count} IP-адресов в ipset '$IPSET_NAME'"
|
||
else
|
||
log_error "Не удалось разрешить ни один домен"
|
||
rm -f "$temp_file"
|
||
exit 1
|
||
fi
|
||
|
||
rm -f "$temp_file"
|
||
}
|
||
|
||
configure_firewall() {
|
||
log_info "Настройка firewalld..."
|
||
|
||
# Проверяем зону
|
||
if ! firewall-cmd --query-zone="$FIREWALL_ZONE" &> /dev/null; then
|
||
log_info "Создание новой зоны '$FIREWALL_ZONE'..."
|
||
firewall-cmd --new-zone="$FIREWALL_ZONE" --permanent
|
||
fi
|
||
|
||
# Добавляем ipset в зону
|
||
firewall-cmd --zone="$FIREWALL_ZONE" --add-ipset="$IPSET_NAME" --permanent
|
||
|
||
# Перезагружаем firewalld
|
||
firewall-cmd --reload
|
||
|
||
log_info "Firewalld настроен для ipset '$IPSET_NAME' в зоне '$FIREWALL_ZONE'"
|
||
}
|
||
|
||
configure_dnsmasq() {
|
||
log_info "Настройка dnsmasq..."
|
||
|
||
# Создаём конфиг для dnsmasq
|
||
local dnsmasq_config="$DNSMASQ_CONF_DIR/domain-routing.conf"
|
||
|
||
mkdir -p "$DNSMASQ_CONF_DIR"
|
||
|
||
# Очищаем старый конфиг
|
||
echo "# Auto-generated domain routing config" > "$dnsmasq_config"
|
||
echo "# Generated: $(date)" >> "$dnsmasq_config"
|
||
|
||
# Добавляем правила для каждого домена
|
||
while IFS= read -r domain || [[ -n "$domain" ]]; do
|
||
[[ -z "$domain" ]] && continue
|
||
[[ "$domain" =~ ^[[:space:]]*# ]] && continue
|
||
|
||
domain=$(echo "$domain" | tr -d '[:space:]')
|
||
|
||
# Генерируем IP для каждого домена
|
||
local ips
|
||
ips=$(dig +short "$domain" A 2>/dev/null || true)
|
||
|
||
if [[ -n "$ips" ]]; then
|
||
echo "$ips" | while read -r ip; do
|
||
if [[ -n "$ip" ]]; then
|
||
echo "server=/$domain/$ip" >> "$dnsmasq_config"
|
||
fi
|
||
done
|
||
fi
|
||
done < "$DOMAINS_FILE"
|
||
|
||
log_info "Конфиг dnsmasq сгенерирован: $dnsmasq_config"
|
||
|
||
# Перезагружаем dnsmasq
|
||
if systemctl is-active --quiet dnsmasq; then
|
||
systemctl reload dnsmasq
|
||
log_info "Dnsmasq перезагружен"
|
||
else
|
||
log_info "Dnsmasq не запущен. Запустите: systemctl start dnsmasq"
|
||
fi
|
||
}
|
||
|
||
save_ipset() {
|
||
log_info "Сохранение ipset..."
|
||
|
||
local save_dir="/etc/ipset"
|
||
mkdir -p "$save_dir"
|
||
|
||
ipset save "$IPSET_NAME" -file "$save_dir/$IPSET_NAME.ipset"
|
||
log_info "Ipset сохранён в $save_dir/$IPSET_NAME.ipset"
|
||
}
|
||
|
||
restore_ipset_on_boot() {
|
||
log_info "Настройка автовосстановления ipset..."
|
||
|
||
# Создаём systemd service для восстановления ipset
|
||
local service_file="/etc/systemd/system/ipset-restore@$IPSET_NAME.service"
|
||
|
||
cat > "$service_file" << EOF
|
||
[Unit]
|
||
Description=Restore ipset $IPSET_NAME on boot
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/sbin/ipset restore < /etc/ipset/$IPSET_NAME.ipset
|
||
RemainAfterExit=yes
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl enable "ipset-restore@$IPSET_NAME.service"
|
||
log_info "Systemd сервис создан и включён"
|
||
}
|
||
|
||
# ============================================
|
||
# ОСНОВНАЯ ЛОГИКА
|
||
# ============================================
|
||
|
||
main() {
|
||
echo "=============================================="
|
||
echo "Domain-based Routing Setup Script"
|
||
echo "=============================================="
|
||
|
||
# Проверка прав суперпользователя
|
||
if [[ $EUID -ne 0 ]]; then
|
||
log_error "Скрипт должен быть запущен от имени root"
|
||
exit 1
|
||
fi
|
||
|
||
# Опции командной строки
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--domains-file)
|
||
DOMAINS_FILE="$2"
|
||
shift 2
|
||
;;
|
||
--ipset-name)
|
||
IPSET_NAME="$2"
|
||
shift 2
|
||
;;
|
||
--firewall-zone)
|
||
FIREWALL_ZONE="$2"
|
||
shift 2
|
||
;;
|
||
--dnsmasq-conf)
|
||
DNSMASQ_CONF_DIR="$2"
|
||
shift 2
|
||
;;
|
||
--help)
|
||
echo "Usage: $0 [OPTIONS]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --domains-file FILE Файл со списком доменов"
|
||
echo " --ipset-name NAME Имя ipset (по умолчанию из config)"
|
||
echo " --firewall-zone ZONE Зона firewalld (по умолчанию из config)"
|
||
echo " --dnsmasq-conf DIR Директория конфигов dnsmasq"
|
||
echo " --help Показать эту справку"
|
||
exit 0
|
||
;;
|
||
*)
|
||
log_error "Неизвестный параметр: $1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
echo ""
|
||
log_info "Конфигурация:"
|
||
echo " Domains file: $DOMAINS_FILE"
|
||
echo " IPset name: $IPSET_NAME"
|
||
echo " Firewall zone: $FIREWALL_ZONE"
|
||
echo " Dnsmasq conf: $DNSMASQ_CONF_DIR"
|
||
echo ""
|
||
|
||
# Шаги выполнения
|
||
check_dependencies
|
||
create_ipset
|
||
resolve_domains_to_ips
|
||
configure_firewall
|
||
configure_dnsmasq
|
||
save_ipset
|
||
restore_ipset_on_boot
|
||
|
||
echo ""
|
||
log_info "=========================================="
|
||
log_info "Domain-based routing успешно настроен!"
|
||
log_info "=========================================="
|
||
}
|
||
|
||
# Запуск основной функции
|
||
main "$@"
|