Files
ovpn-bash-profiler/profiler.sh
2026-01-14 22:27:53 +03:00

639 lines
23 KiB
Bash
Raw 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
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 <UserID> # создать новый профиль
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 -oP 'inet \K[\d.]+' | head -1 || echo "")
# Получаем публичный 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 протокол"
else
safe_sed "TCL" "#tls-client" "$STAGE/user.conf"
log "INFO" "Используется UDP протокол"
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 '<ca>'
cat "$PKIDIR/ca.crt"
echo -e '</ca>\n<cert>'
cat "$PKIDIR/issued/${client_name}.crt"
echo -e '</cert>\n<key>'
cat "$PKIDIR/private/${client_name}.key"
echo -e '</key>\n<tls-auth>'
cat "$PKIDIR/ta.key"
echo -e '</tls-auth>'
} >> "$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 протокол"
else
safe_sed "TCL" "#tls-server" "$STAGE/server.conf"
safe_sed "TUDP" "explicit-exit-notify 1" "$STAGE/server.conf"
log "INFO" "Используется UDP протокол"
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