#!/bin/bash set -euo pipefail IFS=$'\n\t' # Базовые переменные BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="${BASE_DIR}/profiler.log" STAGE="${BASE_DIR}/staging" CLIENTCONF="${BASE_DIR}/client-config" EASYRSADIR="${BASE_DIR}/easy-rsa" PKIDIR="${EASYRSADIR}/pki" export EASYRSA_PKI="$PKIDIR" # Функция логирования log() { local level="$1" local message="$2" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "${timestamp} - ${level}: ${message}" | tee -a "$LOG_FILE" >&2 } print() { printf "%s\n" "$*" || exit 1 } usage() { print " USAGE: ./profiler COMMAND [option] Список доступных КОМАНД и [опций]: build # создать новый профиль list # отобразить список созданных профилей revoke # отозвать созданный профиль init # выполнить инициализацию окружения PKI clear # выполнить очистку окружения PKI srvconf # создать файл конфигурации сервера " } # Функция для безопасной подстановки в шаблоны safe_sed() { local pattern="$1" local replacement="$2" local file="$3" # Экранируем специальные символы для sed replacement=$(printf '%s\n' "$replacement" | sed -e 's/[\/&]/\\&/g') sed -i "s/$pattern/$replacement/g" "$file" } # Функция преобразования CIDR в маску cidr_to_netmask() { local cidr=$1 local mask="" for i in {1..4}; do if [ "$cidr" -ge 8 ]; then mask+="255" cidr=$((cidr - 8)) else local mask_byte=$((256 - 2**(8 - cidr))) mask+="$mask_byte" cidr=0 fi [ $i -lt 4 ] && mask+="." done echo "$mask" } # Функция генерации строк push "route" для SPLIT туннелирования generate_split_routes() { local routes=("$@") for route in "${routes[@]}"; do [[ -z "$route" || "$route" =~ ^[[:space:]]*# ]] && continue if [[ "$route" == *"/"* ]]; then local ip=$(echo "$route" | cut -d'/' -f1) local cidr_mask=$(echo "$route" | cut -d'/' -f2) local mask=$(cidr_to_netmask "$cidr_mask") echo "push \"route $ip $mask\"" else echo "push \"route $route\"" fi done } # Функция генерации DNS опций generate_dns_options() { local dns_servers=("$@") local output="" for dns in "${dns_servers[@]}"; do # Пропускаем пустые строки и комментарии [[ -z "$dns" || "$dns" =~ ^[[:space:]]*# ]] && continue output+="push \"dhcp-option DNS $dns\"\n" done echo -e "$output" } # Функция получения сетевой информации get_network_info() { local ifid ifaddr extip # Получаем имя сетевого интерфейса ifid=$(ip -f inet route 2>/dev/null | grep default | head -1 | awk '{print $5}' || echo "") if [ -z "$ifid" ]; then log "WARN" "Не удалось определить сетевой интерфейс" return 1 fi # Получаем адрес интерфейса ifaddr=$(ip -f inet addr show "$ifid" 2>/dev/null | grep -E 'inet [0-9]' | head -1 | awk '{print $2}' | cut -d'/' -f1) # Получаем публичный IP адрес (пробуем несколько сервисов) local services=("curlmyip.ru" "ifconfig.me" "icanhazip.com" "api.ipify.org") for service in "${services[@]}"; do extip=$(curl -s --connect-timeout 3 "$service" 2>/dev/null | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' | head -1) [ -n "$extip" ] && break done # Используем статический IP из конфигурации, если задан if [ -n "${EXTIP_CONFIG:-}" ]; then extip="$EXTIP_CONFIG" log "INFO" "Используется статический IP из конфигурации: $extip" fi echo "$ifid,$ifaddr,$extip" } # Функция загрузки конфигурации load_config() { local config_file="${BASE_DIR}/confvars" if [ ! -f "$config_file" ]; then log "ERROR" "Файл конфигурации не найден: $config_file" return 1 fi # Загружаем конфигурацию source "$config_file" # Проверяем обязательные переменные local required_vars=("TPROTO" "TPORT" "TSERNET" "TSERMASK" "TTUNTYPE" "FQDN_SERVER") for var in "${required_vars[@]}"; do if [ -z "${!var:-}" ]; then log "ERROR" "Не задана обязательная переменная: $var" return 1 fi done # Обратная совместимость: преобразование старых форматов в массивы if ! declare -p TTUNNETS &>/dev/null 2>&1; then TTUNNETS=() [ -n "${TTUNNET1:-}" ] && TTUNNETS+=("$TTUNNET1") [ -n "${TTUNNET2:-}" ] && TTUNNETS+=("$TTUNNET2") [ -n "${TTUNNET3:-}" ] && TTUNNETS+=("$TTUNNET3") [ -n "${TTUNNET4:-}" ] && TTUNNETS+=("$TTUNNET4") [ -n "${TTUNNET5:-}" ] && TTUNNETS+=("$TTUNNET5") log "INFO" "Используется старый формат TTUNNET, преобразован в массив" fi if ! declare -p TDNS &>/dev/null 2>&1; then TDNS=() [ -n "${TDNS1:-}" ] && TDNS+=("$TDNS1") [ -n "${TDNS2:-}" ] && TDNS+=("$TDNS2") [ -n "${TDNS3:-}" ] && TDNS+=("$TDNS3") log "INFO" "Используется старый формат TDNS, преобразован в массив" fi # Загрузка маршрутов из файла, если указано if [ -n "${TTUNNETS_FILE:-}" ] && [ -f "${TTUNNETS_FILE}" ]; then log "INFO" "Загружаем маршруты из файла: $TTUNNETS_FILE" mapfile -t TTUNNETS < <(grep -v '^#' "${TTUNNETS_FILE}" | grep -v '^$') fi log "INFO" "Конфигурация загружена успешно" return 0 } # Проверка зависимостей check_dependencies() { local deps=("openvpn" "curl" "ip") local missing_deps=() for dep in "${deps[@]}"; do if ! command -v "$dep" &>/dev/null; then missing_deps+=("$dep") fi done if [ ${#missing_deps[@]} -gt 0 ]; then log "ERROR" "Отсутствуют зависимости: ${missing_deps[*]}" return 1 fi return 0 } build() { local uservar="$1" log "INFO" "Начало сборки профиля" # Загрузка конфигурации load_config || exit 1 # Проверка зависимостей check_dependencies || exit 1 # Получение сетевой информации local network_info network_info=$(get_network_info) IFS=',' read -r IFID IFADDR EXTIP <<< "$network_info" log "INFO" "Сетевой интерфейс: $IFID, Адрес: $IFADDR, Публичный IP: ${EXTIP:-не определен}" # Проверка наличия шаблона пользователя local utemp="${BASE_DIR}/templates/user.txt" if [ ! -f "$utemp" ]; then log "ERROR" "Шаблон пользователя не найден: $utemp" exit 1 fi # Проверка инициализации PKI if [ ! -f "$PKIDIR/index.txt" ]; then log "ERROR" "PKI окружение не инициализировано. Выполните './profiler init'" exit 1 fi # Создание директорий mkdir -p "$STAGE" "$CLIENTCONF" # Подготовка шаблона пользователя log "INFO" "Подготовка шаблона пользователя" cp "$utemp" "$STAGE/user.conf" # Обработка протокола if [ "$TPROTO" == "tcp" ]; then safe_sed "TCL" "tls-client" "$STAGE/user.conf" log "INFO" "Используется TCP протокол" elif [ "$TPROTO" == "udp" ]; then safe_sed "TCL" "#tls-client" "$STAGE/user.conf" log "INFO" "Используется UDP протокол" else error_msg="Ошибка: Неверное значение протокола '$TPROTO'. Допустимые значения: tcp или udp" log "ERROR" "$error_msg" echo "$error_msg" >&2 exit 1 fi safe_sed "TPROTO" "$TPROTO" "$STAGE/user.conf" safe_sed "TREMOTE" "${EXTIP:-$TREMOTE}" "$STAGE/user.conf" safe_sed "TPORT" "$TPORT" "$STAGE/user.conf" # Проверка инкремента if [ ! -f "$PKIDIR/increment.txt" ]; then echo 1 > "$PKIDIR/increment.txt" log "INFO" "Создан файл инкремента" fi local id=$(cat "$PKIDIR/increment.txt") # Запрос имени пользователя, если не передано if [ -z "$uservar" ]; then read -p 'Введите имя пользователя латиницей или его UserID: ' uservar if [ -z "$uservar" ]; then log "ERROR" "Имя пользователя не может быть пустым" exit 1 fi fi local client_name="${id}-${uservar}" # Генерация клиентских сертификатов if [ ! -f "$EASYRSADIR/easyrsa" ]; then log "ERROR" "Компоненты easy-rsa 3 не найдены" exit 1 fi log "INFO" "Генерация сертификатов для клиента: $client_name" "$EASYRSADIR/easyrsa" build-client-full "$client_name" nopass # Обновление инкремента echo $((id + 1)) > "$PKIDIR/increment.txt" # Создание конфигурационного файла клиента local client_config="$CLIENTCONF/${client_name}.ovpn" cp "$STAGE/user.conf" "$client_config" # Добавление ключей и сертификатов { echo -e '' cat "$PKIDIR/ca.crt" echo -e '\n' cat "$PKIDIR/issued/${client_name}.crt" echo -e '\n' cat "$PKIDIR/private/${client_name}.key" echo -e '\n' cat "$PKIDIR/ta.key" echo -e '' } >> "$client_config" log "INFO" "Профиль успешно создан: $client_config" echo "==================================================================" echo "Профиль: $client_name создан успешно!" echo "Файл: $client_config" echo "==================================================================" } list() { log "INFO" "Отображение списка профилей" if [ ! -d "$CLIENTCONF" ]; then log "WARN" "Директория клиентских конфигураций не найдена" echo "Нет созданных профилей" return 0 fi local profiles=("$CLIENTCONF"/*.ovpn) if [ ${#profiles[@]} -eq 0 ] || [ ! -f "${profiles[0]}" ]; then echo "Нет созданных профилей" return 0 fi echo "" echo "Список действующих профилей:" echo "=============================" for profile in "${profiles[@]}"; do local name=$(basename "$profile" .ovpn) local size=$(stat -c%s "$profile" 2>/dev/null || stat -f%z "$profile" 2>/dev/null) local date=$(stat -c%y "$profile" 2>/dev/null || stat -f%Sm "$profile" 2>/dev/null) printf "%-20s | %10s | %s\n" "$name" "$((size/1024)) KB" "$date" done echo "=============================" echo "Всего профилей: ${#profiles[@]}" echo "" } revoke() { log "INFO" "Начало процедуры отзыва профиля" if [ ! -f "$EASYRSADIR/easyrsa" ]; then log "ERROR" "Компоненты easy-rsa 3 не найдены" exit 1 fi read -p 'Введите ID профиля (для получения списка используйте команду list): ' profile_id if [ -z "$profile_id" ]; then log "ERROR" "ID профиля не может быть пустым" exit 1 fi # Проверка существования профиля local profile_file="$CLIENTCONF/${profile_id}.ovpn" if [ ! -f "$profile_file" ]; then log "ERROR" "Профиль $profile_id не найден" exit 1 fi # Отзыв сертификата log "INFO" "Отзыв сертификата: $profile_id" "$EASYRSADIR/easyrsa" revoke "$profile_id" # Удаление конфигурационного файла rm -f "$profile_file" log "INFO" "Удален файл конфигурации: $profile_file" # Обновление списка отозванных сертификатов "$EASYRSADIR/easyrsa" gen-crl log "INFO" "Список отозванных сертификатов обновлен" echo "Профиль $profile_id успешно отозван" } init() { log "INFO" "Инициализация PKI окружения" # Проверка зависимостей check_dependencies || exit 1 # Загрузка конфигурации load_config || exit 1 # Проверка, не была ли выполнена инициализация ранее if [ -f "$PKIDIR/ca.crt" ]; then log "ERROR" "Инициализация PKI уже была выполнена ранее" exit 1 fi # Создание директорий mkdir -p "$STAGE" "$CLIENTCONF" "$PKIDIR" # Инициализация PKI log "INFO" "Создание CA сертификата" "$EASYRSADIR/easyrsa" init-pki "$EASYRSADIR/easyrsa" --req-cn="$FQDN_CA" build-ca nopass if [ ! -f "$PKIDIR/ca.crt" ]; then log "ERROR" "Не удалось создать корневой сертификат" exit 1 fi log "INFO" "Создание сертификата сервера: $FQDN_SERVER" "$EASYRSADIR/easyrsa" build-server-full "$FQDN_SERVER" nopass log "INFO" "Создание DH ключа" "$EASYRSADIR/easyrsa" gen-dh log "INFO" "Создание TLS ключа" openvpn --genkey secret "$PKIDIR/ta.key" log "INFO" "Создание списка отозванных сертификатов" "$EASYRSADIR/easyrsa" gen-crl chmod 644 "$PKIDIR/crl.pem" # Копирование ключей и сертификатов local openvpn_dir="/etc/openvpn" log "INFO" "Копирование ключей в $openvpn_dir" cp "$PKIDIR/issued/$FQDN_SERVER.crt" "$openvpn_dir/server.crt" cp "$PKIDIR/ca.crt" "$openvpn_dir/ca.crt" cp "$PKIDIR/dh.pem" "$openvpn_dir/dh.pem" cp "$PKIDIR/ta.key" "$openvpn_dir/ta.key" cp "$PKIDIR/private/$FQDN_SERVER.key" "$openvpn_dir/server.key" # Проверка копирования local files_to_check=("server.crt" "ca.crt" "dh.pem" "ta.key" "server.key") for file in "${files_to_check[@]}"; do if [ ! -f "$openvpn_dir/$file" ]; then log "ERROR" "Не удалось скопировать: $file" exit 1 fi done log "INFO" "Инициализация PKI успешно завершена" echo "==================================================================" echo "PKI окружение успешно инициализировано!" echo "Сертификаты скопированы в /etc/openvpn/" echo "==================================================================" } clear() { log "WARN" "Запрос на очистку PKI окружения" read -p 'ВНИМАНИЕ: Все PKI данные будут удалены. Для продолжения введите "YES": ' answer if [ "$answer" != "YES" ]; then log "INFO" "Очистка PKI отменена пользователем" exit 0 fi # Очистка директорий rm -rf "$CLIENTCONF"/* 2>/dev/null || true rm -rf "$STAGE"/* 2>/dev/null || true # Инициализация новой PKI "$EASYRSADIR/easyrsa" init-pki log "INFO" "PKI окружение очищено и переинициализировано" echo "PKI окружение успешно очищено" } srvconf() { log "INFO" "Создание конфигурации сервера" # Загрузка конфигурации load_config || exit 1 # Получение сетевой информации local network_info network_info=$(get_network_info) IFS=',' read -r IFID IFADDR EXTIP <<< "$network_info" # Проверка наличия шаблона сервера local stemp="${BASE_DIR}/templates/server.txt" if [ ! -f "$stemp" ]; then log "ERROR" "Шаблон сервера не найден: $stemp" exit 1 fi # Создание директории staging mkdir -p "$STAGE" # Копирование шаблона cp "$stemp" "$STAGE/server.conf" log "INFO" "Заполнение параметров конфигурации сервера" # Обработка протокола if [ "$TPROTO" == "tcp" ]; then safe_sed "TCL" "tls-server" "$STAGE/server.conf" safe_sed "TUDP" "# explicit-exit-notify 1" "$STAGE/server.conf" log "INFO" "Используется TCP протокол" elif [ "$TPROTO" == "udp" ]; then safe_sed "TCL" "#tls-server" "$STAGE/server.conf" safe_sed "TUDP" "explicit-exit-notify 1" "$STAGE/server.conf" log "INFO" "Используется UDP протокол" else error_msg="Ошибка: Неверное значение протокола '$TPROTO'. Допустимые значения: tcp или udp" log "ERROR" "$error_msg" echo "$error_msg" >&2 exit 1 fi # Базовые подстановки safe_sed "TPROTO" "$TPROTO" "$STAGE/server.conf" safe_sed "TLADDR" "$IFADDR" "$STAGE/server.conf" safe_sed "TPORT" "$TPORT" "$STAGE/server.conf" safe_sed "TSERNET" "$TSERNET" "$STAGE/server.conf" safe_sed "TSERMASK" "$TSERMASK" "$STAGE/server.conf" # Режим туннеля if [ "$TTUNTYPE" == "FULL" ]; then safe_sed "TTUNTYPE" "push \"redirect-gateway def1 bypass-dhcp\"" "$STAGE/server.conf" safe_sed "TSPLIT_ROUTES" "# Full tunneling mode - все маршруты через VPN" "$STAGE/server.conf" log "INFO" "Режим: FULL tunneling" else safe_sed "TTUNTYPE" "# Split tunneling mode" "$STAGE/server.conf" split_routes=$(generate_split_routes "${TTUNNETS[@]}") if [ -n "$split_routes" ]; then # Создаем временный файл с маршрутами echo "$split_routes" > "$STAGE/split_routes.tmp" # Заменяем маркер содержимым файла sed -i "/TSPLIT_ROUTES/r $STAGE/split_routes.tmp" "$STAGE/server.conf" sed -i "/TSPLIT_ROUTES/d" "$STAGE/server.conf" rm -f "$STAGE/split_routes.tmp" else sed -i "s/TSPLIT_ROUTES/# No split routes configured/" "$STAGE/server.conf" fi log "INFO" "Режим: SPLIT tunneling, маршрутов: ${#TTUNNETS[@]}" fi # DNS серверы local dns_options=$(generate_dns_options "${TDNS[@]}") safe_sed "TDNS_OPTIONS" "$dns_options" "$STAGE/server.conf" log "INFO" "Настроено DNS серверов: ${#TDNS[@]}" # Клиент-клиент соединения if [ "$TC2C" == "YES" ]; then safe_sed "TC2C" "client-to-client" "$STAGE/server.conf" log "INFO" "Разрешены соединения между клиентами" else safe_sed "TC2C" "# client-to-client disabled" "$STAGE/server.conf" fi # Множественные подключения по одному сертификату if [ "$TDCN" == "YES" ]; then safe_sed "TDCN" "duplicate-cn" "$STAGE/server.conf" log "INFO" "Разрешены множественные подключения по одному сертификату" else safe_sed "TDCN" "# duplicate-cn disabled" "$STAGE/server.conf" fi # Проверка отозванных сертификатов if [ "$TREVO" == "YES" ]; then safe_sed "TREVO" "crl-verify $PKIDIR/crl.pem" "$STAGE/server.conf" log "INFO" "Активирована проверка списка отозванных сертификатов" else safe_sed "TREVO" "# crl-verify disabled" "$STAGE/server.conf" fi # Определение ОС и копирование конфигурации local os_info=$(uname -a) local target_conf if [[ "$os_info" == *"Alpine"* ]]; then target_conf="/etc/openvpn/openvpn.conf" log "INFO" "Обнаружена Alpine Linux" else target_conf="/etc/openvpn/server.conf" log "INFO" "Обнаружена другая дистрибутив Linux" fi # Использовать скрипты для обработки событий подключения и отключения пользователя if [ "$T_CONNSCRIPTS" == "YES" ]; then safe_sed "T_SCRIPTSEC" "script-security 2" "$STAGE/server.conf" safe_sed "T_CONNSCRIPT" "client-connect \"$T_CONNSCRIPT_STRING\"" "$STAGE/server.conf" safe_sed "T_DISCONNSCRIPT" "client-disconnect \"$T_DISCONNSCRIPT_STRING\"" "$STAGE/server.conf" log "INFO" "Настройка скриптов подключения и отключения пользователя применена" else safe_sed "T_SCRIPTSEC" "" "$STAGE/server.conf" safe_sed "T_CONNSCRIPT" "" "$STAGE/server.conf" safe_sed "T_DISCONNSCRIPT" "" "$STAGE/server.conf" fi # Использовать Management Interface if [ "$T_MGMT" == "YES" ]; then safe_sed "T_MGMT_CONF" "management $T_MGMT_ADDR $T_MGMT_PORT" "$STAGE/server.conf" log "INFO" "Management Interface активирован" else safe_sed "T_MGMT_CONF" "" "$STAGE/server.conf" fi # Копирование конфигурации cp "$STAGE/server.conf" "$target_conf" if [ $? -eq 0 ]; then log "INFO" "Конфигурация сервера скопирована в: $target_conf" echo "==================================================================" echo "Конфигурация сервера успешно создана!" echo "Файл: $target_conf" echo "Для применения изменений перезапустите OpenVPN:" echo " systemctl restart openvpn@server # для systemd" echo " или service openvpn restart # для SysVinit" echo "==================================================================" else log "ERROR" "Не удалось скопировать конфигурацию в $target_conf" exit 1 fi } # Обработка команд cmd="${1:-}" shift 2>/dev/null || true case "$cmd" in build) build "${1:-}" ;; list) list ;; revoke) revoke ;; init) init ;; clear) clear ;; srvconf) srvconf ;; help|-h|--help|--usage|"") usage ;; *) echo "Неизвестная команда: $cmd" usage exit 1 ;; esac exit 0