619 lines
37 KiB
Bash
Executable File
619 lines
37 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
resolve_script_dir() {
|
|
local source_path=${BASH_SOURCE[0]} source_dir
|
|
if [[ $source_path == /* ]]; then
|
|
if [[ $source_path == */* ]]; then source_dir=${source_path%/*}; else source_dir=/; fi
|
|
else
|
|
if [[ $source_path == */* ]]; then source_dir=${PWD}/${source_path%/*}; else source_dir=$PWD; fi
|
|
fi
|
|
[[ -n $source_dir ]] || source_dir=/
|
|
cd -- "$source_dir" && pwd
|
|
}
|
|
SCRIPT_DIR="$(resolve_script_dir)"
|
|
readonly SCRIPT_DIR
|
|
readonly WG_MANAGER_VERSION='0.1.0'
|
|
readonly EXIT_USAGE=64
|
|
readonly EXIT_VALIDATION=65
|
|
readonly EXIT_OPERATION=70
|
|
# shellcheck disable=SC2034
|
|
readonly EXIT_SUCCESS=0
|
|
# shellcheck disable=SC2034
|
|
readonly EXIT_ROLLBACK=71
|
|
# shellcheck disable=SC2034
|
|
readonly EXIT_PERMISSION=77
|
|
readonly EXIT_DEPENDENCY=69
|
|
|
|
CONFIG_PATH=''
|
|
BACKUP_DIR=''
|
|
LOG_DIR=''
|
|
METADATA_DIR=''
|
|
ARCHIVE_DIR=''
|
|
LOCK_FILE=''
|
|
WG_INTERFACE=''
|
|
WG_CONF=''
|
|
CLIENT_DIR=''
|
|
SERVER_ENDPOINT=''
|
|
SERVER_PORT=''
|
|
SERVER_VPN_IP=''
|
|
CLIENT_DNS=''
|
|
VPN_SUBNET_PREFIX=''
|
|
START_HOST=''
|
|
END_HOST=''
|
|
USE_IPV6=''
|
|
IPV6_PREFIX=''
|
|
DEFAULT_CLIENT_MODE=''
|
|
RECENT_HANDSHAKE_SECONDS=''
|
|
BACKUP_RETENTION=''
|
|
declare -A CONFIG_VALUES=()
|
|
|
|
print_usage() {
|
|
cat <<'USAGE'
|
|
Использование:
|
|
wg-manager.sh help
|
|
wg-manager.sh version
|
|
wg-manager.sh {status|list|add|show|config|qr|remove|diagnose|logs|backups|restore|tui}
|
|
|
|
Команды управления WireGuard будут добавлены в следующих версиях.
|
|
USAGE
|
|
}
|
|
|
|
die() {
|
|
local status=$1
|
|
shift
|
|
printf 'Ошибка: %s\n' "$*" >&2
|
|
return "$status"
|
|
}
|
|
|
|
warn() { printf 'Предупреждение: %s\n' "$*" >&2; }
|
|
info() { printf '%s\n' "$*" >&2; }
|
|
debug() {
|
|
if [[ "${WG_MANAGER_DEBUG:-0}" == '1' ]]; then
|
|
printf 'Отладка: %s\n' "$*" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
trim_space() {
|
|
local value=$1
|
|
value="${value#${value%%[!$' \t']*}}"
|
|
value="${value%${value##*[!$' \t']}}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
is_control_free() { [[ $1 != *[$'\001'-$'\037'$'\177']* ]]; }
|
|
is_shell_syntax_free() { [[ $1 != *'$'* && $1 != *'`'* && $1 != *';'* && $1 != *'|'* && $1 != *'&'* && $1 != *'<'* && $1 != *'>'* && $1 != *'('* && $1 != *')'* && $1 != *\\* && $1 != *'*'* && $1 != *'?'* && $1 != *'{'* && $1 != *'}'* ]]; }
|
|
|
|
set_runtime_paths() {
|
|
if [[ ${WG_MANAGER_CONFIG+x} ]]; then CONFIG_PATH=$WG_MANAGER_CONFIG; else CONFIG_PATH=${SCRIPT_DIR}/wg-manager.conf; fi
|
|
if [[ ${WG_MANAGER_BACKUP_DIR+x} ]]; then BACKUP_DIR=$WG_MANAGER_BACKUP_DIR; else BACKUP_DIR=${SCRIPT_DIR}/backups; fi
|
|
if [[ ${WG_MANAGER_LOG_DIR+x} ]]; then LOG_DIR=$WG_MANAGER_LOG_DIR; else LOG_DIR=${SCRIPT_DIR}/logs; fi
|
|
if [[ ${WG_MANAGER_METADATA_DIR+x} ]]; then METADATA_DIR=$WG_MANAGER_METADATA_DIR; else METADATA_DIR=${SCRIPT_DIR}/state/clients; fi
|
|
if [[ ${WG_MANAGER_ARCHIVE_DIR+x} ]]; then ARCHIVE_DIR=$WG_MANAGER_ARCHIVE_DIR; else ARCHIVE_DIR=${SCRIPT_DIR}/state/archive; fi
|
|
if [[ ${WG_MANAGER_LOCK_FILE+x} ]]; then LOCK_FILE=$WG_MANAGER_LOCK_FILE; else LOCK_FILE=${SCRIPT_DIR}/state/wg-manager.lock; fi
|
|
local name value
|
|
for name in CONFIG_PATH BACKUP_DIR LOG_DIR METADATA_DIR ARCHIVE_DIR LOCK_FILE; do
|
|
value=${!name}
|
|
if [[ -z "$value" ]] || ! is_control_free "$value" || ! is_shell_syntax_free "$value" || ! validate_path "$value"; then
|
|
die "$EXIT_VALIDATION" "Некорректный абсолютный путь ${name}" || return $?
|
|
fi
|
|
[[ $value != *'//'* && $value != */./* && $value != */../* && $value != */.. && $value != */. ]] || { die "$EXIT_VALIDATION" "Путь ${name} не нормализован"; return $?; }
|
|
done
|
|
}
|
|
|
|
parse_config_file() {
|
|
local file=$1 line trimmed key raw value line_number=0
|
|
CONFIG_VALUES=()
|
|
[[ -r "$file" ]] || { die "$EXIT_VALIDATION" "Не удаётся прочитать конфигурацию: ${file}"; return $?; }
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
((line_number += 1))
|
|
is_control_free "$line" || { die "$EXIT_VALIDATION" "Недопустимый управляющий символ в строке ${line_number}"; return $?; }
|
|
trimmed=$(trim_space "$line")
|
|
[[ -z "$trimmed" || ${trimmed:0:1} == '#' ]] && continue
|
|
[[ $trimmed =~ ^([A-Z][A-Z0-9_]*)[[:space:]]*=[[:space:]]*(.*)$ ]] || { die "$EXIT_VALIDATION" "Некорректное присваивание в строке ${line_number}"; return $?; }
|
|
key=${BASH_REMATCH[1]}
|
|
raw=$(trim_space "${BASH_REMATCH[2]}")
|
|
case "$key" in
|
|
WG_INTERFACE|WG_CONF|CLIENT_DIR|SERVER_ENDPOINT|SERVER_PORT|SERVER_VPN_IP|CLIENT_DNS|VPN_SUBNET_PREFIX|START_HOST|END_HOST|USE_IPV6|IPV6_PREFIX|DEFAULT_CLIENT_MODE|RECENT_HANDSHAKE_SECONDS|BACKUP_RETENTION) ;;
|
|
*) die "$EXIT_VALIDATION" "Неизвестный параметр конфигурации: ${key}"; return $? ;;
|
|
esac
|
|
[[ -v "CONFIG_VALUES[$key]" ]] && { die "$EXIT_VALIDATION" "Дублирующийся параметр конфигурации: ${key}"; return $?; }
|
|
is_shell_syntax_free "$raw" || { die "$EXIT_VALIDATION" "Недопустимый синтаксис в параметре ${key}"; return $?; }
|
|
if [[ $raw == \"* ]]; then
|
|
[[ $raw == *\" && ${#raw} -ge 2 && ${raw:1:${#raw}-2} != *\"* ]] || { die "$EXIT_VALIDATION" "Некорректные кавычки в параметре ${key}"; return $?; }
|
|
value=${raw:1:${#raw}-2}
|
|
elif [[ $raw == *\"* || $raw == *\'* || $raw == *[[:space:]]* || -z "$raw" ]]; then
|
|
die "$EXIT_VALIDATION" "Некорректное значение параметра ${key}"; return $?
|
|
else value=$raw
|
|
fi
|
|
CONFIG_VALUES["$key"]=$value
|
|
done <"$file"
|
|
}
|
|
|
|
require_config_keys() {
|
|
local key
|
|
for key in WG_INTERFACE WG_CONF CLIENT_DIR SERVER_ENDPOINT SERVER_PORT SERVER_VPN_IP CLIENT_DNS VPN_SUBNET_PREFIX START_HOST END_HOST USE_IPV6 IPV6_PREFIX DEFAULT_CLIENT_MODE RECENT_HANDSHAKE_SECONDS BACKUP_RETENTION; do
|
|
[[ -v "CONFIG_VALUES[$key]" ]] || { die "$EXIT_VALIDATION" "Отсутствует параметр конфигурации: ${key}"; return $?; }
|
|
done
|
|
}
|
|
|
|
is_uint() { [[ $1 =~ ^[0-9]+$ ]]; }
|
|
validate_ipv4() {
|
|
local value=$1 part
|
|
local -a parts=()
|
|
IFS=. read -r -a parts <<<"$value"
|
|
(( ${#parts[@]} == 4 )) || return 1
|
|
for part in "${parts[@]}"; do is_uint "$part" && (( 10#$part <= 255 )) || return 1; done
|
|
}
|
|
|
|
validate_ipv6() {
|
|
local value=$1 part
|
|
local -a parts=()
|
|
[[ $value == *:* && $value != *[^0123456789abcdefABCDEF:]* && $value != *:::* ]] || return 1
|
|
if [[ $value == *'::'* ]]; then
|
|
[[ $value != *'::'*'::'* ]] || return 1
|
|
value=${value//::/:}; value=${value#:}; value=${value%:}
|
|
IFS=: read -r -a parts <<<"$value"
|
|
(( ${#parts[@]} < 8 )) || return 1
|
|
for part in "${parts[@]}"; do [[ -z "$part" || ${#part} -gt 4 ]] && return 1; done
|
|
return 0
|
|
fi
|
|
IFS=: read -r -a parts <<<"$value"
|
|
(( ${#parts[@]} == 8 )) || return 1
|
|
for part in "${parts[@]}"; do [[ -n "$part" && ${#part} -le 4 ]] || return 1; done
|
|
}
|
|
validate_ip() { validate_ipv4 "$1" || validate_ipv6 "$1"; }
|
|
|
|
validate_endpoint() {
|
|
local endpoint=$1 host=$1 label
|
|
local -a endpoint_labels=()
|
|
[[ $endpoint != *[[:space:]]* && $endpoint != *[/\\]* ]] || return 1
|
|
if [[ $endpoint == \[*\] ]]; then
|
|
host=${endpoint:1:${#endpoint}-2}
|
|
validate_ipv6 "$host"
|
|
return
|
|
elif [[ $endpoint == *'['* || $endpoint == *']'* ]]; then return 1; fi
|
|
if [[ $host =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
|
|
validate_ipv4 "$host"
|
|
return
|
|
fi
|
|
if validate_ipv6 "$host"; then return 0; fi
|
|
[[ ${#host} -le 253 && $host =~ ^([A-Za-z0-9-]+\.)*[A-Za-z0-9-]+$ ]] || return 1
|
|
IFS=. read -r -a endpoint_labels <<<"$host"
|
|
for label in "${endpoint_labels[@]}"; do
|
|
[[ ${#label} -le 63 && $label != -* && $label != *- ]] || return 1
|
|
done
|
|
}
|
|
|
|
validate_endpoint_value() {
|
|
local value=$1 host port
|
|
if [[ $value == \[*\]:* ]]; then
|
|
host=${value%%\]:*}; host=${host#\[}; port=${value##*:}
|
|
validate_ipv6 "$host" && is_uint "$port" && (( 10#$port >= 1 && 10#$port <= 65535 ))
|
|
elif [[ $value == *:* && $value != *::* ]]; then
|
|
host=${value%:*}; port=${value##*:}
|
|
validate_endpoint "$host" && is_uint "$port" && (( 10#$port >= 1 && 10#$port <= 65535 ))
|
|
else
|
|
validate_endpoint "$value"
|
|
fi
|
|
}
|
|
|
|
validate_peer_endpoint_value() {
|
|
local value=$1
|
|
if [[ $value == \[*\]:* ]]; then
|
|
validate_endpoint_value "$value"
|
|
elif [[ $value != *:*:* && $value == *:* ]]; then
|
|
validate_endpoint_value "$value"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
is_fwmark() {
|
|
local value=$1 number digits
|
|
if [[ $value =~ ^[0-9]+$ ]]; then
|
|
digits=$value
|
|
while [[ ${#digits} -gt 1 && ${digits:0:1} == 0 ]]; do digits=${digits:1}; done
|
|
(( ${#digits} <= 10 )) || return 1
|
|
(( 10#$digits <= 4294967295 ))
|
|
return
|
|
fi
|
|
[[ $value =~ ^0[xX][0-9A-Fa-f]+$ ]] || return 1
|
|
digits=${value#0x}; digits=${digits#0X}
|
|
(( ${#digits} <= 8 )) || return 1
|
|
number=$((value))
|
|
(( number <= 4294967295 ))
|
|
}
|
|
|
|
validate_path() { [[ $1 == /* && $1 != *[[:space:]]* && $1 != *[$'\001'-$'\037'$'\177']* && $1 != *'['* && $1 != *']'* && $1 != *//* && $1 != */./* && $1 != */../* && $1 != */.. && $1 != */. ]]; }
|
|
|
|
validate_config_values() {
|
|
local value server_host
|
|
WG_INTERFACE=${CONFIG_VALUES[WG_INTERFACE]}; WG_CONF=${CONFIG_VALUES[WG_CONF]}; CLIENT_DIR=${CONFIG_VALUES[CLIENT_DIR]}; SERVER_ENDPOINT=${CONFIG_VALUES[SERVER_ENDPOINT]}; SERVER_PORT=${CONFIG_VALUES[SERVER_PORT]}; SERVER_VPN_IP=${CONFIG_VALUES[SERVER_VPN_IP]}; CLIENT_DNS=${CONFIG_VALUES[CLIENT_DNS]}; VPN_SUBNET_PREFIX=${CONFIG_VALUES[VPN_SUBNET_PREFIX]}; START_HOST=${CONFIG_VALUES[START_HOST]}; END_HOST=${CONFIG_VALUES[END_HOST]}; USE_IPV6=${CONFIG_VALUES[USE_IPV6]}; IPV6_PREFIX=${CONFIG_VALUES[IPV6_PREFIX]}; DEFAULT_CLIENT_MODE=${CONFIG_VALUES[DEFAULT_CLIENT_MODE]}; RECENT_HANDSHAKE_SECONDS=${CONFIG_VALUES[RECENT_HANDSHAKE_SECONDS]}; BACKUP_RETENTION=${CONFIG_VALUES[BACKUP_RETENTION]}
|
|
[[ $WG_INTERFACE =~ ^[A-Za-z0-9_.-]{1,15}$ && $WG_INTERFACE != .* && $WG_INTERFACE != *..* ]] || { die "$EXIT_VALIDATION" 'Некорректное имя интерфейса'; return $?; }
|
|
validate_path "$WG_CONF" && validate_path "$CLIENT_DIR" || { die "$EXIT_VALIDATION" 'Некорректный путь конфигурации или клиентов'; return $?; }
|
|
validate_endpoint "$SERVER_ENDPOINT" || { die "$EXIT_VALIDATION" 'Некорректная конечная точка сервера'; return $?; }
|
|
is_uint "$SERVER_PORT" && (( 10#$SERVER_PORT >= 1 && 10#$SERVER_PORT <= 65535 )) || { die "$EXIT_VALIDATION" 'Некорректный порт сервера'; return $?; }
|
|
validate_ipv4 "$SERVER_VPN_IP" || { die "$EXIT_VALIDATION" 'Некорректный IPv4 сервера'; return $?; }
|
|
[[ -n $CLIENT_DNS && $CLIENT_DNS != *[[:space:]]* && $CLIENT_DNS != ,* && $CLIENT_DNS != *, && $CLIENT_DNS != *,,* ]] || { die "$EXIT_VALIDATION" 'Некорректный список DNS'; return $?; }
|
|
local dns; local -a dns_values=(); IFS=, read -r -a dns_values <<<"$CLIENT_DNS"; for dns in "${dns_values[@]}"; do [[ -n $dns ]] && validate_ip "$dns" || { die "$EXIT_VALIDATION" 'Некорректный адрес DNS'; return $?; }; done
|
|
[[ $VPN_SUBNET_PREFIX =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { die "$EXIT_VALIDATION" 'Некорректный префикс VPN'; return $?; }
|
|
local -a prefix_values=(); IFS=. read -r -a prefix_values <<<"$VPN_SUBNET_PREFIX"; for value in "${prefix_values[@]}"; do is_uint "$value" && (( 10#$value <= 255 )) || { die "$EXIT_VALIDATION" 'Некорректный префикс VPN'; return $?; }; done
|
|
[[ ${SERVER_VPN_IP%.*} == "$VPN_SUBNET_PREFIX" ]] || { die "$EXIT_VALIDATION" 'Префикс VPN не совпадает с адресом сервера'; return $?; }
|
|
is_uint "$START_HOST" && is_uint "$END_HOST" && (( 10#$START_HOST >= 1 && 10#$START_HOST <= 254 && 10#$END_HOST >= 1 && 10#$END_HOST <= 254 && 10#$START_HOST <= 10#$END_HOST )) || { die "$EXIT_VALIDATION" 'Некорректный диапазон адресов клиентов'; return $?; }
|
|
server_host=${SERVER_VPN_IP##*.}; (( 10#$START_HOST > 10#$server_host || 10#$server_host > 10#$END_HOST )) || { die "$EXIT_VALIDATION" 'Диапазон клиентов содержит адрес сервера'; return $?; }
|
|
[[ $USE_IPV6 == 0 || $USE_IPV6 == 1 ]] || { die "$EXIT_VALIDATION" 'USE_IPV6 должен быть 0 или 1'; return $?; }
|
|
validate_ipv6 "$IPV6_PREFIX" || { die "$EXIT_VALIDATION" 'Некорректный IPv6-префикс'; return $?; }
|
|
[[ $DEFAULT_CLIENT_MODE == full-tunnel || $DEFAULT_CLIENT_MODE == split-tunnel ]] || { die "$EXIT_VALIDATION" 'Некорректный режим клиента'; return $?; }
|
|
is_uint "$BACKUP_RETENTION" && (( 10#$BACKUP_RETENTION >= 1 && 10#$BACKUP_RETENTION <= 1000 )) || { die "$EXIT_VALIDATION" 'Некорректное число резервных копий'; return $?; }
|
|
is_uint "$RECENT_HANDSHAKE_SECONDS" && (( 10#$RECENT_HANDSHAKE_SECONDS <= 604800 )) || { die "$EXIT_VALIDATION" 'Некорректный срок handshake'; return $?; }
|
|
}
|
|
|
|
load_config() { set_runtime_paths || return $?; parse_config_file "$CONFIG_PATH" || return $?; require_config_keys || return $?; validate_config_values || return $?; }
|
|
|
|
readonly MANAGED_MARKER_PREFIX='wg-manager-client:'
|
|
declare -a CLIENT_KEYS=() CLIENT_NAMES=() CLIENT_ADDRESSES=() CLIENT_ENDPOINTS=() CLIENT_HANDSHAKES=() CLIENT_RX=() CLIENT_TX=() CLIENT_STATES=() CLIENT_ORIGINS=()
|
|
declare -A DISK_ALLOWED DISK_ENDPOINT DISK_MARKER DISK_LEGACY DISK_IPV4 DISK_IPV6
|
|
declare -A META_NAME META_IPV4 META_IPV6 META_CONFIG META_MODE META_MANAGED META_CREATED
|
|
declare -A RUNTIME_ENDPOINT RUNTIME_HANDSHAKE RUNTIME_RX RUNTIME_TX RUNTIME_ALLOWED
|
|
RUNTIME_AVAILABLE=0
|
|
RUNTIME_LISTEN_PORT='-'
|
|
|
|
is_wireguard_key() { [[ $1 =~ ^[A-Za-z0-9+/]{43}=$ ]]; }
|
|
|
|
ipv4_to_int() {
|
|
local -a octets=() octet
|
|
IFS=. read -r -a octets <<<"${1%/*}"
|
|
printf '%u\n' "$(( 10#${octets[0]} * 16777216 + 10#${octets[1]} * 65536 + 10#${octets[2]} * 256 + 10#${octets[3]} ))"
|
|
}
|
|
|
|
cidr_network_v4() {
|
|
local address=${1%/*} prefix=${1#*/} mask
|
|
mask=$(( prefix == 0 ? 0 : (0xffffffff << (32 - prefix)) & 0xffffffff ))
|
|
printf '%u %u\n' "$(( $(ipv4_to_int "$address") & mask ))" "$prefix"
|
|
}
|
|
|
|
ipv6_groups() {
|
|
local value=$1 left right part missing index
|
|
local -a left_values=() right_values=() groups=()
|
|
if [[ $value == *'::'* ]]; then
|
|
left=${value%%::*}; right=${value##*::}
|
|
[[ -n $left ]] && IFS=: read -r -a left_values <<<"$left"
|
|
[[ -n $right ]] && IFS=: read -r -a right_values <<<"$right"
|
|
missing=$(( 8 - ${#left_values[@]} - ${#right_values[@]} )); (( missing >= 1 )) || return 1
|
|
groups=("${left_values[@]}"); for ((index = 0; index < missing; index += 1)); do groups+=(0); done; groups+=("${right_values[@]}")
|
|
else
|
|
IFS=: read -r -a groups <<<"$value"
|
|
fi
|
|
(( ${#groups[@]} == 8 )) || return 1
|
|
for part in "${groups[@]}"; do printf '%04x\n' "$((16#$part))"; done
|
|
}
|
|
|
|
ipv6_prefix_overlap() {
|
|
local left=$1 right=$2 left_prefix=${1#*/} right_prefix=${2#*/} prefix bit group_index left_group right_group mask
|
|
local -a left_groups=() right_groups=()
|
|
prefix=$left_prefix; (( right_prefix < prefix )) && prefix=$right_prefix
|
|
mapfile -t left_groups < <(ipv6_groups "${left%/*}")
|
|
mapfile -t right_groups < <(ipv6_groups "${right%/*}")
|
|
((${#left_groups[@]} == 8 && ${#right_groups[@]} == 8)) || return 1
|
|
for ((bit = 0; bit < prefix; bit += 1)); do
|
|
group_index=$((bit / 16)); mask=$((0x8000 >> (bit % 16)))
|
|
left_group=$((16#${left_groups[$group_index]})); right_group=$((16#${right_groups[$group_index]}))
|
|
(( (left_group & mask) == (right_group & mask) )) || return 1
|
|
done
|
|
}
|
|
|
|
validate_cidr() {
|
|
local cidr=$1 address prefix
|
|
[[ $cidr == */* && $cidr != *[[:space:]]* ]] || return 1
|
|
address=${cidr%/*}; prefix=${cidr#*/}
|
|
is_uint "$prefix" || return 1
|
|
if validate_ipv4 "$address"; then (( 10#$prefix <= 32 )); return; fi
|
|
validate_ipv6 "$address" && (( 10#$prefix <= 128 ))
|
|
}
|
|
|
|
cidr_family() { validate_ipv4 "${1%/*}" && printf '4' || printf '6'; }
|
|
|
|
cidrs_overlap() {
|
|
local left=$1 right=$2 left_family right_family left_network left_prefix right_network right_prefix
|
|
left_family=$(cidr_family "$left"); right_family=$(cidr_family "$right"); [[ $left_family == "$right_family" ]] || return 1
|
|
if [[ $left_family == 4 ]]; then
|
|
read -r left_network left_prefix <<<"$(cidr_network_v4 "$left")"
|
|
read -r right_network right_prefix <<<"$(cidr_network_v4 "$right")"
|
|
local common=$left_prefix; (( right_prefix < common )) && common=$right_prefix
|
|
local mask=$(( common == 0 ? 0 : (0xffffffff << (32 - common)) & 0xffffffff ))
|
|
(( (left_network & mask) == (right_network & mask) ))
|
|
return
|
|
fi
|
|
ipv6_prefix_overlap "$left" "$right"
|
|
}
|
|
|
|
normalize_allowed_ips() {
|
|
local raw=$1 item existing index
|
|
local -a values=()
|
|
local -A seen=()
|
|
IFS=, read -r -a values <<<"$raw"
|
|
((${#values[@]} > 0)) || return 1
|
|
for index in "${!values[@]}"; do
|
|
values[$index]=$(trim_space "${values[$index]}")
|
|
done
|
|
for item in "${values[@]}"; do
|
|
[[ -n "$item" ]] && validate_cidr "$item" || return 1
|
|
[[ -z ${seen[$item]+x} ]] || return 1
|
|
seen["$item"]=1
|
|
done
|
|
for index in "${!values[@]}"; do
|
|
for existing in "${values[@]:index+1}"; do cidrs_overlap "${values[$index]}" "$existing" && return 1; done
|
|
done
|
|
printf '%s\n' "${values[@]}" | LC_ALL=C sort | paste -sd, -
|
|
}
|
|
|
|
allowed_sets_overlap() {
|
|
local left=$1 right=$2 left_item right_item
|
|
local -a left_values=() right_values=()
|
|
IFS=, read -r -a left_values <<<"$left"; IFS=, read -r -a right_values <<<"$right"
|
|
for left_item in "${left_values[@]}"; do for right_item in "${right_values[@]}"; do cidrs_overlap "$left_item" "$right_item" && return 0; done; done
|
|
return 1
|
|
}
|
|
|
|
extract_client_addresses() {
|
|
local allowed=$1 item address prefix
|
|
local -a values=() v4=() v6=()
|
|
IFS=, read -r -a values <<<"$allowed"
|
|
for item in "${values[@]}"; do
|
|
address=${item%/*}; prefix=${item#*/}
|
|
if [[ $address == *:* ]]; then v6+=("$item"); elif [[ $prefix == 32 ]]; then v4+=("$item"); fi
|
|
done
|
|
printf '%s|%s\n' "$(IFS=,; printf '%s' "${v4[*]}")" "$(IFS=,; printf '%s' "${v6[*]}")"
|
|
}
|
|
|
|
commit_disk_peer() {
|
|
local key=$1 allowed=$2
|
|
[[ -n "$key" && -n "$allowed" ]] || { die "$EXIT_VALIDATION" 'Peer не содержит PublicKey или AllowedIPs'; return $?; }
|
|
is_wireguard_key "$key" || { die "$EXIT_VALIDATION" 'Некорректный PublicKey в конфигурации'; return $?; }
|
|
[[ -z ${DISK_ALLOWED[$key]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся PublicKey в конфигурации'; return $?; }
|
|
local normalized existing_key
|
|
normalized=$(normalize_allowed_ips "$allowed") || { die "$EXIT_VALIDATION" 'Некорректные или пересекающиеся AllowedIPs'; return $?; }
|
|
for existing_key in "${!DISK_ALLOWED[@]}"; do
|
|
allowed_sets_overlap "$normalized" "${DISK_ALLOWED[$existing_key]}" && { die "$EXIT_VALIDATION" 'Пересекающиеся AllowedIPs между peers'; return $?; }
|
|
done
|
|
DISK_ALLOWED["$key"]=$normalized
|
|
local addresses; addresses=$(extract_client_addresses "${DISK_ALLOWED[$key]}")
|
|
DISK_IPV4["$key"]=${addresses%%|*}; DISK_IPV6["$key"]=${addresses#*|}
|
|
DISK_ENDPOINT["$key"]=${peer_endpoint:-'-'}
|
|
DISK_MARKER["$key"]=${peer_marker:-}
|
|
DISK_LEGACY["$key"]=${peer_legacy:-}
|
|
peer_key=''; peer_allowed=''; peer_endpoint='-'; peer_marker=''; peer_legacy=''
|
|
}
|
|
|
|
parse_wireguard_config() {
|
|
local file=$1 line trimmed section='' key value peer_key='' peer_allowed='' peer_endpoint='-' peer_marker='' peer_legacy='' pending_marker='' pending_legacy='' line_number=0
|
|
local -A interface_fields=() peer_fields=()
|
|
DISK_ALLOWED=(); DISK_ENDPOINT=(); DISK_MARKER=(); DISK_LEGACY=(); DISK_IPV4=(); DISK_IPV6=()
|
|
[[ -r "$file" && ! -L "$file" ]] || { die "$EXIT_VALIDATION" 'Конфигурация WireGuard недоступна или является ссылкой'; return $?; }
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
((line_number += 1)); is_control_free "$line" || { die "$EXIT_VALIDATION" "Управляющий символ в конфигурации WireGuard, строка ${line_number}"; return $?; }
|
|
trimmed=$(trim_space "$line"); [[ -z "$trimmed" ]] && continue
|
|
if [[ ${trimmed:0:1} == '#' ]]; then
|
|
if [[ $trimmed =~ ^#[[:space:]]*${MANAGED_MARKER_PREFIX}[[:space:]]*([A-Za-z0-9._-]+)$ ]]; then
|
|
if [[ $section == Peer && ${#peer_fields[@]} -eq 0 ]]; then peer_marker=${BASH_REMATCH[1]}; else pending_marker=${BASH_REMATCH[1]}; fi
|
|
fi
|
|
if [[ $trimmed =~ ^###[[:space:]]Client[[:space:]]([A-Za-z0-9._-]+)$ ]]; then
|
|
if [[ $section == Peer && ${#peer_fields[@]} -eq 0 ]]; then peer_legacy=${BASH_REMATCH[1]}; else pending_legacy=${BASH_REMATCH[1]}; fi
|
|
fi
|
|
continue
|
|
fi
|
|
if [[ $trimmed == '[Interface]' ]]; then
|
|
[[ -z $section ]] || { die "$EXIT_VALIDATION" 'Повторная секция Interface'; return $?; }
|
|
pending_marker=''; pending_legacy=''; section=Interface; continue
|
|
elif [[ $trimmed == '[Peer]' ]]; then
|
|
[[ $section == Interface || $section == Peer ]] || { die "$EXIT_VALIDATION" 'Peer до секции Interface'; return $?; }
|
|
[[ $section != Peer ]] || { commit_disk_peer "$peer_key" "$peer_allowed" || return $?; }
|
|
section=Peer; peer_key=''; peer_allowed=''; peer_endpoint='-'; peer_marker=$pending_marker; peer_legacy=$pending_legacy; pending_marker=''; pending_legacy=''; peer_fields=(); continue
|
|
elif [[ $trimmed == \[*\] ]]; then
|
|
die "$EXIT_VALIDATION" "Неизвестная секция WireGuard, строка ${line_number}"; return $?
|
|
fi
|
|
[[ $section == Interface || $section == Peer ]] || { die "$EXIT_VALIDATION" "Параметр вне секции, строка ${line_number}"; return $?; }
|
|
[[ $trimmed =~ ^([A-Za-z][A-Za-z0-9]*)[[:space:]]*=[[:space:]]*(.*)$ ]] || { die "$EXIT_VALIDATION" "Некорректный параметр WireGuard, строка ${line_number}"; return $?; }
|
|
key=${BASH_REMATCH[1]}; value=$(trim_space "${BASH_REMATCH[2]}"); [[ -n "$value" ]] || { die "$EXIT_VALIDATION" 'Пустой параметр WireGuard'; return $?; }
|
|
[[ $section != Interface || -z $pending_marker$pending_legacy ]] || { pending_marker=''; pending_legacy=''; }
|
|
if [[ $section == Interface ]]; then
|
|
case "$key" in
|
|
PrivateKey|ListenPort|MTU|Table|SaveConfig|FwMark) ;;
|
|
Address|DNS|PostUp|PostDown|PreUp|PreDown) continue ;;
|
|
*) die "$EXIT_VALIDATION" "Неизвестный параметр Interface: ${key}"; return $? ;;
|
|
esac
|
|
[[ -z ${interface_fields[$key]+x} ]] || { die "$EXIT_VALIDATION" "Дублирующийся параметр Interface: ${key}"; return $?; }
|
|
interface_fields["$key"]=1
|
|
if [[ $key == PrivateKey ]]; then is_wireguard_key "$value" || { die "$EXIT_VALIDATION" 'Некорректный ключ Interface'; return $?; }; fi
|
|
if [[ $key == ListenPort ]]; then is_uint "$value" && (( 10#$value <= 65535 )) || { die "$EXIT_VALIDATION" 'Некорректный ListenPort'; return $?; }; fi
|
|
if [[ $key == FwMark ]]; then is_fwmark "$value" || { die "$EXIT_VALIDATION" 'Некорректный FwMark'; return $?; }; fi
|
|
continue
|
|
fi
|
|
case "$key" in
|
|
PublicKey) [[ -z ${peer_fields[PublicKey]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся PublicKey в peer'; return $?; }; is_wireguard_key "$value" || { die "$EXIT_VALIDATION" 'Некорректный PublicKey в peer'; return $?; }; peer_key=$value; peer_fields[PublicKey]=1 ;;
|
|
PresharedKey) [[ -z ${peer_fields[PresharedKey]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся PresharedKey в peer'; return $?; }; is_wireguard_key "$value" || { die "$EXIT_VALIDATION" 'Некорректный PresharedKey'; return $?; }; peer_fields[PresharedKey]=1 ;;
|
|
AllowedIPs) [[ -z ${peer_fields[AllowedIPs]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся AllowedIPs в peer'; return $?; }; peer_allowed=$value; peer_fields[AllowedIPs]=1 ;;
|
|
Endpoint) [[ -z ${peer_fields[Endpoint]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся Endpoint в peer'; return $?; }; validate_peer_endpoint_value "$value" || { die "$EXIT_VALIDATION" 'Некорректный Endpoint'; return $?; }; peer_endpoint=$value; peer_fields[Endpoint]=1 ;;
|
|
PersistentKeepalive) [[ -z ${peer_fields[PersistentKeepalive]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся PersistentKeepalive'; return $?; }; is_uint "$value" && (( 10#$value <= 65535 )) || { die "$EXIT_VALIDATION" 'Некорректный PersistentKeepalive'; return $?; }; peer_fields[PersistentKeepalive]=1 ;;
|
|
*) die "$EXIT_VALIDATION" "Неизвестный параметр Peer: ${key}"; return $? ;;
|
|
esac
|
|
done <"$file"
|
|
[[ $section == Interface || $section == Peer ]] || { die "$EXIT_VALIDATION" 'Отсутствует секция Interface'; return $?; }
|
|
[[ $section != Peer ]] || commit_disk_peer "$peer_key" "$peer_allowed"
|
|
}
|
|
|
|
parse_metadata_file() {
|
|
local file=$1 line key value line_number=0 existing_key existing_name
|
|
local name='' public_key='' ipv4='' ipv6='' created='' config='' mode='' managed=''
|
|
[[ -f "$file" && ! -L "$file" ]] || return 0
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
((line_number += 1)); line=$(trim_space "$line"); [[ -z "$line" ]] && continue
|
|
[[ $line =~ ^([A-Z][A-Z0-9_]*)=(.*)$ ]] || { die "$EXIT_VALIDATION" "Некорректные метаданные: ${file}"; return $?; }
|
|
key=${BASH_REMATCH[1]}; value=${BASH_REMATCH[2]}; [[ -n "$value" ]] || { die "$EXIT_VALIDATION" 'Пустое поле метаданных'; return $?; }
|
|
case "$key" in
|
|
NAME) [[ -z $name ]] || { die "$EXIT_VALIDATION" 'Дубликат NAME в метаданных'; return $?; }; name=$value ;;
|
|
PUBLIC_KEY) [[ -z $public_key ]] || { die "$EXIT_VALIDATION" 'Дубликат PUBLIC_KEY в метаданных'; return $?; }; public_key=$value ;;
|
|
IPV4) [[ -z $ipv4 ]] || { die "$EXIT_VALIDATION" 'Дубликат IPV4 в метаданных'; return $?; }; ipv4=$value ;;
|
|
IPV6) [[ -z $ipv6 ]] || { die "$EXIT_VALIDATION" 'Дубликат IPV6 в метаданных'; return $?; }; ipv6=$value ;;
|
|
CREATED_AT) [[ -z $created ]] || { die "$EXIT_VALIDATION" 'Дубликат CREATED_AT в метаданных'; return $?; }; created=$value ;;
|
|
CONFIG_PATH) [[ -z $config ]] || { die "$EXIT_VALIDATION" 'Дубликат CONFIG_PATH в метаданных'; return $?; }; config=$value ;;
|
|
CLIENT_MODE) [[ -z $mode ]] || { die "$EXIT_VALIDATION" 'Дубликат CLIENT_MODE в метаданных'; return $?; }; mode=$value ;;
|
|
MANAGED) [[ -z $managed ]] || { die "$EXIT_VALIDATION" 'Дубликат MANAGED в метаданных'; return $?; }; managed=$value ;;
|
|
*) die "$EXIT_VALIDATION" "Неизвестное поле метаданных: ${key}"; return $? ;;
|
|
esac
|
|
done <"$file"
|
|
[[ $name =~ ^[A-Za-z0-9._-]+$ && $name != -* && -n $public_key && -n $ipv4 && $created =~ ^[0-9]+$ && $config == /* ]] || { die "$EXIT_VALIDATION" "Некорректные метаданные: ${file}"; return $?; }
|
|
[[ $mode == full-tunnel || $mode == split-tunnel ]] || { die "$EXIT_VALIDATION" "Некорректный режим в метаданных: ${file}"; return $?; }
|
|
[[ $managed == 0 || $managed == 1 ]] || { die "$EXIT_VALIDATION" "Некорректный MANAGED в метаданных: ${file}"; return $?; }
|
|
validate_path "$config" && [[ $config == "${CLIENT_DIR%/}"/* ]] && { [[ ! -e "$config" || ! -L "$config" ]]; } || { die "$EXIT_VALIDATION" "Некорректный путь клиента в метаданных: ${file}"; return $?; }
|
|
is_wireguard_key "$public_key" && validate_ipv4 "$ipv4" || { die "$EXIT_VALIDATION" "Некорректная идентичность в метаданных: ${file}"; return $?; }
|
|
if [[ -n $ipv6 ]] && ! validate_ipv6 "$ipv6"; then die "$EXIT_VALIDATION" "Некорректный IPV6 в метаданных: ${file}"; return $?; fi
|
|
for existing_key in "${!META_NAME[@]}"; do [[ $existing_key == "$public_key" ]] && { die "$EXIT_VALIDATION" 'Дублирующийся PUBLIC_KEY в метаданных'; return $?; }; done
|
|
for existing_name in "${META_NAMES[@]}"; do [[ $existing_name == "$name" ]] && { die "$EXIT_VALIDATION" 'Дублирующееся NAME в метаданных'; return $?; }; done
|
|
META_NAME["$public_key"]=$name; META_NAMES+=("$name"); META_IPV4["$public_key"]=$ipv4; META_IPV6["$public_key"]=$ipv6; META_CREATED["$public_key"]=$created; META_CONFIG["$public_key"]=$config; META_MODE["$public_key"]=$mode; META_MANAGED["$public_key"]=$managed
|
|
}
|
|
|
|
parse_metadata() {
|
|
local file
|
|
META_NAME=(); META_NAMES=(); META_IPV4=(); META_IPV6=(); META_CREATED=(); META_CONFIG=(); META_MODE=(); META_MANAGED=()
|
|
[[ -d "$METADATA_DIR" ]] || return 0
|
|
for file in "$METADATA_DIR"/*; do [[ -e "$file" ]] || continue; parse_metadata_file "$file" || return $?; done
|
|
}
|
|
|
|
parse_runtime_dump() {
|
|
local line fields_count line_number=0 key endpoint allowed handshake rx tx keepalive
|
|
local output=$1
|
|
RUNTIME_AVAILABLE=1; RUNTIME_LISTEN_PORT='-'; RUNTIME_ENDPOINT=(); RUNTIME_HANDSHAKE=(); RUNTIME_RX=(); RUNTIME_TX=(); RUNTIME_ALLOWED=()
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
((line_number += 1)); IFS=$'\t' read -r -a fields <<<"$line"; fields_count=${#fields[@]}
|
|
if (( line_number == 1 )); then
|
|
(( fields_count == 4 )) || { die "$EXIT_VALIDATION" 'Некорректная строка интерфейса wg dump'; return $?; }
|
|
if [[ ${fields[0]} != '(none)' ]] && ! is_wireguard_key "${fields[0]}"; then die "$EXIT_VALIDATION" 'Некорректный ключ интерфейса wg dump'; return $?; fi
|
|
if [[ ${fields[1]} != '(none)' ]] && ! is_wireguard_key "${fields[1]}"; then die "$EXIT_VALIDATION" 'Некорректный ключ интерфейса wg dump'; return $?; fi
|
|
is_uint "${fields[2]}" && (( 10#${fields[2]} <= 65535 )) || { die "$EXIT_VALIDATION" 'Некорректный порт в wg dump'; return $?; }; RUNTIME_LISTEN_PORT=${fields[2]}; continue
|
|
fi
|
|
(( fields_count == 8 )) || { die "$EXIT_VALIDATION" 'Некорректная строка peer в wg dump'; return $?; }
|
|
is_wireguard_key "${fields[0]}" && { [[ ${fields[1]} == '(none)' ]] || is_wireguard_key "${fields[1]}"; } || { die "$EXIT_VALIDATION" 'Некорректный ключ в wg dump'; return $?; }
|
|
key=${fields[0]}; endpoint=${fields[2]}; allowed=${fields[3]}; handshake=${fields[4]}; rx=${fields[5]}; tx=${fields[6]}; keepalive=${fields[7]}
|
|
[[ $endpoint == '(none)' ]] && endpoint='-'
|
|
[[ $allowed == '(none)' ]] && allowed='-'
|
|
is_wireguard_key "$key" || { die "$EXIT_VALIDATION" 'Некорректный PublicKey в wg dump'; return $?; }
|
|
[[ $endpoint == '-' || -z $endpoint ]] || validate_peer_endpoint_value "$endpoint" || { die "$EXIT_VALIDATION" 'Некорректный Endpoint в wg dump'; return $?; }
|
|
[[ -z ${RUNTIME_HANDSHAKE[$key]+x} ]] || { die "$EXIT_VALIDATION" 'Дублирующийся PublicKey в wg dump'; return $?; }
|
|
if [[ $allowed != '-' ]]; then normalize_allowed_ips "$allowed" >/dev/null || { die "$EXIT_VALIDATION" 'Некорректные AllowedIPs в wg dump'; return $?; }; fi
|
|
is_uint "$handshake" && is_uint "$rx" && is_uint "$tx" && is_uint "$keepalive" || { die "$EXIT_VALIDATION" 'Некорректные числа в wg dump'; return $?; }
|
|
RUNTIME_ENDPOINT["$key"]=${endpoint:--}; if [[ $allowed == '-' ]]; then RUNTIME_ALLOWED["$key"]='-'; else RUNTIME_ALLOWED["$key"]=$(normalize_allowed_ips "$allowed"); fi; RUNTIME_HANDSHAKE["$key"]=$handshake; RUNTIME_RX["$key"]=$rx; RUNTIME_TX["$key"]=$tx
|
|
done <<<"$output"
|
|
}
|
|
|
|
load_runtime_state() {
|
|
local output command_status
|
|
RUNTIME_AVAILABLE=0; RUNTIME_LISTEN_PORT='-'
|
|
if ! command -v wg >/dev/null 2>&1; then return 0; fi
|
|
set +e
|
|
output=$(wg show "$WG_INTERFACE" dump 2>/dev/null); command_status=$?
|
|
set -e
|
|
(( command_status == 0 )) || return 0
|
|
[[ -n "$output" ]] || return 0
|
|
parse_runtime_dump "$output"
|
|
}
|
|
|
|
current_epoch() {
|
|
if [[ ${WG_MANAGER_NOW+x} ]]; then
|
|
[[ $WG_MANAGER_NOW =~ ^[0-9]+$ ]] || return "$EXIT_VALIDATION"
|
|
printf '%s\n' "$WG_MANAGER_NOW"
|
|
else
|
|
date +%s
|
|
fi
|
|
}
|
|
|
|
build_client_records() {
|
|
local key addresses name origin handshake now state disk_ipv4 disk_ipv6 expected_allowed
|
|
local -a keys=()
|
|
local -A seen_keys=()
|
|
for key in "${!DISK_ALLOWED[@]}" "${!RUNTIME_HANDSHAKE[@]}"; do [[ -n ${seen_keys[$key]+x} ]] || { seen_keys[$key]=1; keys+=("$key"); }; done
|
|
CLIENT_KEYS=(); CLIENT_NAMES=(); CLIENT_ADDRESSES=(); CLIENT_ENDPOINTS=(); CLIENT_HANDSHAKES=(); CLIENT_RX=(); CLIENT_TX=(); CLIENT_STATES=(); CLIENT_ORIGINS=()
|
|
now=$(current_epoch) || return $?
|
|
for key in "${keys[@]}"; do
|
|
name='-'; origin=unmanaged; addresses=${DISK_ALLOWED[$key]:-${RUNTIME_ALLOWED[$key]:--}}; handshake=${RUNTIME_HANDSHAKE[$key]:-0}
|
|
disk_ipv4=${DISK_IPV4[$key]:-}; disk_ipv4=${disk_ipv4%%/*}; disk_ipv6=${DISK_IPV6[$key]:-}; disk_ipv6=${disk_ipv6%%/*}
|
|
if [[ ${META_NAME[$key]+x} && ${META_MANAGED[$key]} == 1 ]]; then
|
|
expected_allowed="${META_IPV4[$key]}/32"; [[ -n ${META_IPV6[$key]:-} ]] && expected_allowed+=",${META_IPV6[$key]}/128"
|
|
if [[ ${META_IPV4[$key]} == "$disk_ipv4" && ${META_IPV6[$key]:-} == "$disk_ipv6" && ${DISK_ALLOWED[$key]:-} == "$(normalize_allowed_ips "$expected_allowed")" ]]; then name=${META_NAME[$key]}; origin=managed; fi
|
|
fi
|
|
if [[ $origin != managed && -n ${DISK_MARKER[$key]:-}${DISK_LEGACY[$key]:-} ]]; then origin=legacy; name=${DISK_MARKER[$key]:-${DISK_LEGACY[$key]}}; fi
|
|
if [[ $origin == managed ]]; then
|
|
if [[ $handshake == 0 ]]; then state=never; elif (( now >= handshake && now - handshake <= RECENT_HANDSHAKE_SECONDS )); then state=active; else state=inactive; fi
|
|
else state=unmanaged; fi
|
|
CLIENT_KEYS+=("$key"); CLIENT_NAMES+=("$name"); CLIENT_ADDRESSES+=("$addresses"); CLIENT_ENDPOINTS+=("${RUNTIME_ENDPOINT[$key]:-${DISK_ENDPOINT[$key]:--}}"); CLIENT_HANDSHAKES+=("$handshake"); CLIENT_RX+=("${RUNTIME_RX[$key]:-0}"); CLIENT_TX+=("${RUNTIME_TX[$key]:-0}"); CLIENT_STATES+=("$state"); CLIENT_ORIGINS+=("$origin")
|
|
done
|
|
}
|
|
|
|
load_client_model() {
|
|
load_config || return $?; parse_wireguard_config "$WG_CONF" || return $?; parse_metadata || return $?; load_runtime_state || return $?; build_client_records
|
|
}
|
|
|
|
print_status() {
|
|
local managed=0 unmanaged=0 active=0 index
|
|
for index in "${!CLIENT_KEYS[@]}"; do
|
|
if [[ ${CLIENT_ORIGINS[$index]} == managed ]]; then ((managed += 1)); else ((unmanaged += 1)); fi
|
|
if [[ ${CLIENT_STATES[$index]} == active ]]; then ((active += 1)); fi
|
|
done
|
|
printf 'Интерфейс: %s\nСостояние runtime: %s\nПорт: %s\nPeers: %d\nУправляемые: %d\nНеуправляемые: %d\nАктивные: %d\n' "$WG_INTERFACE" "$([[ $RUNTIME_AVAILABLE == 1 ]] && printf 'доступен' || printf 'недоступен')" "${RUNTIME_LISTEN_PORT:--}" "${#CLIENT_KEYS[@]}" "$managed" "$unmanaged" "$active"
|
|
}
|
|
|
|
print_list() {
|
|
local index
|
|
printf 'NAME\tADDRESSES\tPUBLIC_KEY\tENDPOINT\tHANDSHAKE\tRX\tTX\tSTATE\tORIGIN\n'
|
|
for index in "${!CLIENT_KEYS[@]}"; do printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "${CLIENT_NAMES[$index]}" "${CLIENT_ADDRESSES[$index]}" "${CLIENT_KEYS[$index]:0:8}" "${CLIENT_ENDPOINTS[$index]}" "${CLIENT_HANDSHAKES[$index]}" "${CLIENT_RX[$index]}" "${CLIENT_TX[$index]}" "${CLIENT_STATES[$index]}" "${CLIENT_ORIGINS[$index]}"; done | LC_ALL=C sort -t $'\t' -k1,1 -k2,2 -k3,3
|
|
}
|
|
|
|
require_command() {
|
|
local command_name=$1 command_path
|
|
command_path=$(command -v -- "$command_name" 2>/dev/null) || { die "$EXIT_DEPENDENCY" "Не найдена обязательная команда: ${command_name}"; return $?; }
|
|
[[ -x "$command_path" ]] || { die "$EXIT_DEPENDENCY" "Команда недоступна для запуска: ${command_name}"; return $?; }
|
|
printf '%s\n' "$command_path"
|
|
}
|
|
optional_command() { local command_path; command_path=$(command -v -- "$1" 2>/dev/null) || return "$EXIT_DEPENDENCY"; [[ -f "$command_path" && -x "$command_path" ]] && printf '%s\n' "$command_path" || return "$EXIT_DEPENDENCY"; }
|
|
|
|
resolve_gum() {
|
|
local candidate
|
|
if [[ ${WG_MANAGER_GUM+x} ]]; then
|
|
[[ -f ${WG_MANAGER_GUM} && -x ${WG_MANAGER_GUM} ]] || { die "$EXIT_DEPENDENCY" 'WG_MANAGER_GUM задан, но путь недоступен'; return $?; }
|
|
printf '%s\n' "$WG_MANAGER_GUM"; return 0
|
|
fi
|
|
candidate=${SCRIPT_DIR}/bin/gum
|
|
if [[ -f "$candidate" && -x "$candidate" ]]; then printf '%s\n' "$candidate"; return 0; fi
|
|
optional_command gum || { die "$EXIT_DEPENDENCY" 'Не найден gum для TUI'; return $?; }
|
|
}
|
|
|
|
print_version() { printf 'WireGuard Manager %s\n' "$WG_MANAGER_VERSION"; }
|
|
run_placeholder() { load_config || return $?; die "$EXIT_OPERATION" "Команда «$1» пока не реализована"; }
|
|
|
|
main() {
|
|
if (( $# != 1 )); then
|
|
local status
|
|
if die "$EXIT_USAGE" 'Ожидается одна команда'; then status=0; else status=$?; fi
|
|
print_usage >&2
|
|
return "$status"
|
|
fi
|
|
case "$1" in
|
|
help) print_usage ;;
|
|
version) print_version ;;
|
|
status) load_client_model || return $?; print_status ;;
|
|
list) load_client_model || return $?; print_list ;;
|
|
add|show|config|qr|remove|diagnose|logs|backups|restore|tui) run_placeholder "$1" ;;
|
|
*)
|
|
local status
|
|
if die "$EXIT_USAGE" "неизвестная команда: $1"; then status=0; else status=$?; fi
|
|
print_usage >&2
|
|
return "$status"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi
|