#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${SCRIPT_DIR}/config" DATA_DIR="${SCRIPT_DIR}/data" CLIENT_DIR="${SCRIPT_DIR}/clients" REGISTRY="${DATA_DIR}/clients.json" die() { printf "ERROR: %s\n" "$*" >&2 exit 1 } info() { printf "INFO: %s\n" "$*" } require_binary() { command -v "$1" >/dev/null 2>&1 \ || die "$1 not installed" } load_config() { [[ -f "$CONFIG_FILE" ]] || die "Config not found" # shellcheck source=/dev/null source "$CONFIG_FILE" } init_storage() { mkdir -p "$DATA_DIR" mkdir -p "$CLIENT_DIR" if [[ ! -f "$REGISTRY" ]]; then echo "[]" > "$REGISTRY" fi # Проверка валидности JSON jq empty "$REGISTRY" \ || die "Registry JSON is invalid" } sanitize() { local v="$1" v="${v//[^a-zA-Z0-9._-]/_}" printf "%s" "$v" } get_next_id() { local id id="$(jq -r ' if length == 0 then 1 else (map(.id) | max) + 1 end ' "$REGISTRY")" [[ "$id" =~ ^[0-9]+$ ]] \ || die "Failed to calculate next ID" echo "$id" } get_last_ip() { jq -r ' if length == 0 then empty else (sort_by(.id) | last.ip) end ' "$REGISTRY" } get_first_client_ip() { local network="${SERVER_NETWORK}" # Удаляем CIDR suffix network="${network%%/*}" IFS='.' read -r o1 o2 o3 o4 <<< "$network" # Первый клиент всегда .2 echo "${o1}.${o2}.${o3}.2" } increment_ip() { local current_ip="$1" # Если клиентов еще нет — # выдаем первый IP из SERVER_NETWORK if [[ -z "$current_ip" ]]; then get_first_client_ip return fi IFS='.' read -r o1 o2 o3 o4 <<< "$current_ip" ((o4++)) # Защита от выхода за пределы /24 if ((o4 >= 255)); then die "IP pool exhausted" fi echo "${o1}.${o2}.${o3}.${o4}" } generate_client_keys() { CLIENT_PRIV="$(wg genkey)" CLIENT_PUB="$( printf "%s" "$CLIENT_PRIV" | wg pubkey )" CLIENT_PSK="$(wg genpsk)" } create_client_config() { local id="$1" local name="$2" local ip="$3" local filename="${CLIENT_DIR}/${name}.conf" cat > "$filename" < "$tmp_file" \ || die "Failed to update registry" mv "$tmp_file" "$REGISTRY" # Проверка после записи jq empty "$REGISTRY" \ || die "Registry corrupted after write" } create_client() { local raw_name="" local apply_ros=false while [[ $# -gt 0 ]]; do case "$1" in --apply_ros) apply_ros=true shift ;; *) if [[ -z "$raw_name" ]]; then raw_name="$1" else die "Unexpected argument: $1" fi shift ;; esac done [[ -n "$raw_name" ]] \ || die "Client name required" local name name="$(sanitize "$raw_name")" [[ -n "$name" ]] \ || die "Sanitized name is empty" local id id="$(get_next_id)" local last_ip last_ip="$(get_last_ip)" local next_ip next_ip="$(increment_ip "$last_ip")" generate_client_keys create_client_config "$id" "$name" "$next_ip" registry_add "$id" "$name" "$next_ip" info "Client registered: id=$id name=$name ip=$next_ip" print_mikrotik_commands info "RouterOS Configuration Script has been Created: id=$id name=$name" if [[ "$apply_ros" == true ]]; then apply_mikrotik_commands info "RouterOS WG Peer has been Configured: id=$id name=$name" else info "RouterOS WG Peer was not Configured (use --apply_ros to apply configuration)" fi } list_clients() { jq -r ' if length == 0 then "No clients" else .[] | "ID=\(.id) NAME=\(.name) IP=\(.ip) STATUS=\(.is_enabled)" end ' "$REGISTRY" } disable_client() { local id="${1:-}" [[ "$id" =~ ^[0-9]+$ ]] \ || die "Invalid ID" local current_status name read -r current_status name < <(jq -r --argjson id "$id" '.[] | select(.id == $id) | "\(.is_enabled) \(.name)"' "$REGISTRY" 2>/dev/null) if [[ -z "$name" ]]; then die "Client with ID $id not found" fi local new_status if [[ "$current_status" == "ACTIVE" ]]; then new_status="DISABLED" else new_status="ACTIVE" fi local tmp_file tmp_file="$(mktemp)" jq \ --argjson id "$id" \ --arg new_status "$new_status" \ ' map( if .id == $id then .is_enabled = $new_status else . end ) ' "$REGISTRY" > "$tmp_file" \ || { rm -f "$tmp_file"; die "Failed to update registry status"; } mv "$tmp_file" "$REGISTRY" info "Client $id ($name) status changed from $current_status to $new_status" } enable_client() { local id="${1:-}" [[ "$id" =~ ^[0-9]+$ ]] \ || die "Invalid ID" local current_status name read -r current_status name < <(jq -r --argjson id "$id" '.[] | select(.id == $id) | "\(.is_enabled) \(.name)"' "$REGISTRY" 2>/dev/null) if [[ -z "$name" ]]; then die "Client with ID $id not found" fi local new_status if [[ "$current_status" == "DISABLED" ]]; then new_status="ACTIVE" else new_status="DISABLED" fi local tmp_file tmp_file="$(mktemp)" jq \ --argjson id "$id" \ --arg new_status "$new_status" \ ' map( if .id == $id then .is_enabled = $new_status else . end ) ' "$REGISTRY" > "$tmp_file" \ || { rm -f "$tmp_file"; die "Failed to update registry status"; } mv "$tmp_file" "$REGISTRY" info "Client $id ($name) status changed from $current_status to $new_status" } delete_client() { local id="${1:-}" [[ "$id" =~ ^[0-9]+$ ]] \ || die "Invalid ID" local name name="$(jq -r --argjson id "$id" '.[] | select(.id == $id) | .name' "$REGISTRY" 2>/dev/null)" if [[ -z "$name" ]]; then die "Client with ID $id not found" fi local tmp_file tmp_file="$(mktemp)" jq \ --argjson id "$id" \ 'map(select(.id != $id))' \ "$REGISTRY" > "$tmp_file" \ || { rm -f "$tmp_file"; die "Failed to update registry"; } mv "$tmp_file" "$REGISTRY" if [[ -n "$name" ]]; then rm -f "${CLIENT_DIR}/${name}.conf" rm -f "${CLIENT_DIR}/${name}.png" rm -f "${CLIENT_DIR}/${name}.rsc" fi info "Client $id ($name) removed" } print_mikrotik_commands() { client_ip=$(jq -r --arg val "$name" '.[] | select(.name == $val) | .ip' "$REGISTRY") local mt_filename="${CLIENT_DIR}/${name}.rsc" cat > "$mt_filename" <