Files

649 lines
24 KiB
Bash
Raw Permalink Normal View History

2026-01-14 22:27:53 +03:00
#!/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 -E 'inet [0-9]' | head -1 | awk '{print $2}' | cut -d'/' -f1)
2026-01-14 22:27:53 +03:00
# Получаем публичный 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
2026-01-14 22:27:53 +03:00
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
2026-01-14 22:27:53 +03:00
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 протокол"
elif [ "$TPROTO" == "udp" ]; then
2026-01-14 22:27:53 +03:00
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
2026-01-14 22:27:53 +03:00
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