feat(clients): add client details and secure config export
This commit is contained in:
+151
-8
@@ -54,7 +54,7 @@ print_usage() {
|
||||
Использование:
|
||||
wg-manager.sh help
|
||||
wg-manager.sh version
|
||||
wg-manager.sh {status|list|add NAME [--mode MODE] [--endpoint HOST]|show|config|qr|remove|diagnose|logs|backups|restore|tui}
|
||||
wg-manager.sh {status|list|add NAME [--mode MODE] [--endpoint HOST]|show NAME|config NAME [--show]|qr NAME|remove|diagnose|logs|backups|restore|tui}
|
||||
|
||||
Команды управления WireGuard будут добавлены в следующих версиях.
|
||||
USAGE
|
||||
@@ -262,7 +262,7 @@ 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
|
||||
DISK_LISTEN_PORT='-'
|
||||
declare -A META_NAME META_IPV4 META_IPV6 META_CONFIG META_MODE META_MANAGED META_CREATED
|
||||
declare -A META_NAME META_IPV4 META_IPV6 META_CONFIG META_MODE META_MANAGED META_CREATED META_FILE
|
||||
declare -A RUNTIME_ENDPOINT RUNTIME_HANDSHAKE RUNTIME_RX RUNTIME_TX RUNTIME_ALLOWED
|
||||
RUNTIME_AVAILABLE=0
|
||||
RUNTIME_LISTEN_PORT='-'
|
||||
@@ -480,12 +480,12 @@ parse_metadata_file() {
|
||||
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
|
||||
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; META_FILE["$public_key"]=$file
|
||||
}
|
||||
|
||||
parse_metadata() {
|
||||
local file
|
||||
META_NAME=(); META_NAMES=(); META_IPV4=(); META_IPV6=(); META_CREATED=(); META_CONFIG=(); META_MODE=(); META_MANAGED=()
|
||||
META_NAME=(); META_NAMES=(); META_IPV4=(); META_IPV6=(); META_CREATED=(); META_CONFIG=(); META_MODE=(); META_MANAGED=(); META_FILE=()
|
||||
[[ -d "$METADATA_DIR" ]] || return 0
|
||||
for file in "$METADATA_DIR"/*; do [[ -e "$file" ]] || continue; parse_metadata_file "$file" || return $?; done
|
||||
}
|
||||
@@ -517,12 +517,13 @@ parse_runtime_dump() {
|
||||
}
|
||||
|
||||
load_runtime_state() {
|
||||
local output command_status
|
||||
local output command_status had_errexit=0
|
||||
RUNTIME_AVAILABLE=0; RUNTIME_LISTEN_PORT='-'
|
||||
if ! command -v wg >/dev/null 2>&1; then return 0; fi
|
||||
[[ $- == *e* ]] && had_errexit=1
|
||||
set +e
|
||||
output=$(wg show "$WG_INTERFACE" dump 2>/dev/null); command_status=$?
|
||||
set -e
|
||||
(( had_errexit == 1 )) && set -e || set +e
|
||||
(( command_status == 0 )) || return 0
|
||||
[[ -n "$output" ]] || return 0
|
||||
parse_runtime_dump "$output"
|
||||
@@ -547,7 +548,7 @@ build_client_records() {
|
||||
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
|
||||
if [[ -n ${DISK_ALLOWED[$key]+x} && ${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
|
||||
@@ -1395,6 +1396,133 @@ resolve_gum() {
|
||||
print_version() { printf 'WireGuard Manager %s\n' "$WG_MANAGER_VERSION"; }
|
||||
run_placeholder() { load_config || return $?; die "$EXIT_OPERATION" "Команда «$1» пока не реализована"; }
|
||||
|
||||
resolve_named_client() {
|
||||
local name=$1 index count=0 found=-1
|
||||
for index in "${!CLIENT_NAMES[@]}"; do
|
||||
[[ ${CLIENT_NAMES[$index]} == "$name" ]] || continue
|
||||
((count += 1)); found=$index
|
||||
done
|
||||
(( count == 1 )) || { die "$EXIT_VALIDATION" "Клиент не найден или имя неоднозначно: ${name}"; return $?; }
|
||||
printf '%s\n' "$found"
|
||||
}
|
||||
|
||||
validate_client_config_path() {
|
||||
local key=$1 name=$2 path metadata_file mode links owner metadata_owner client_dir_owner metadata_dir_owner server_owner client_dir_mode metadata_dir_mode
|
||||
[[ ${CLIENT_ORIGINS[$3]:-} == managed ]] || { die "$EXIT_VALIDATION" 'Клиент не имеет подтверждённой конфигурации'; return $?; }
|
||||
path=${META_CONFIG[$key]:-}; metadata_file=${META_FILE[$key]:-}
|
||||
[[ -n $path && -n $metadata_file && ${path##*/} == "${name}.conf" && ${path%/*} == "${CLIENT_DIR%/}" ]] || { die "$EXIT_VALIDATION" 'Некорректный путь конфигурации клиента'; return $?; }
|
||||
path_components_safe "$path" && path_components_safe "$metadata_file" || { die "$EXIT_VALIDATION" 'Путь конфигурации содержит символическую ссылку'; return $?; }
|
||||
[[ -f $path && ! -L $path && -f $metadata_file && ! -L $metadata_file ]] || { die "$EXIT_VALIDATION" 'Файл конфигурации клиента недоступен'; return $?; }
|
||||
[[ -d $CLIENT_DIR && ! -L $CLIENT_DIR && -d $METADATA_DIR && ! -L $METADATA_DIR ]] || { die "$EXIT_VALIDATION" 'Каталог клиентских файлов недоступен'; return $?; }
|
||||
path_components_safe "$CLIENT_DIR" && path_components_safe "$METADATA_DIR" || { die "$EXIT_VALIDATION" 'Каталог клиентских файлов содержит символическую ссылку'; return $?; }
|
||||
client_dir_mode=$(stat -c '%a' -- "$CLIENT_DIR") || { die "$EXIT_OPERATION" 'Не удалось проверить каталог клиентов'; return $?; }
|
||||
metadata_dir_mode=$(stat -c '%a' -- "$METADATA_DIR") || { die "$EXIT_OPERATION" 'Не удалось проверить каталог метаданных'; return $?; }
|
||||
client_dir_owner=$(stat -c '%u:%g' -- "$CLIENT_DIR") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца каталога клиентов'; return $?; }
|
||||
metadata_dir_owner=$(stat -c '%u:%g' -- "$METADATA_DIR") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца каталога метаданных'; return $?; }
|
||||
server_owner=$(stat -c '%u:%g' -- "$WG_CONF") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца серверной конфигурации'; return $?; }
|
||||
(( (8#$client_dir_mode & 077) == 0 && (8#$metadata_dir_mode & 077) == 0 )) || { die "$EXIT_VALIDATION" 'Каталог клиентских файлов доступен группе или другим'; return $?; }
|
||||
[[ $client_dir_owner == "$metadata_dir_owner" && $client_dir_owner == "$server_owner" ]] || { die "$EXIT_VALIDATION" 'Владельцы каталогов и серверной конфигурации не совпадают'; return $?; }
|
||||
mode=$(stat -c '%a' -- "$path") || { die "$EXIT_OPERATION" 'Не удалось проверить режим конфигурации клиента'; return $?; }
|
||||
(( (8#$mode & 077) == 0 )) || { die "$EXIT_VALIDATION" 'Файл конфигурации доступен группе или другим'; return $?; }
|
||||
links=$(stat -c '%h' -- "$path") || { die "$EXIT_OPERATION" 'Не удалось проверить ссылки конфигурации клиента'; return $?; }
|
||||
(( links == 1 )) || { die "$EXIT_VALIDATION" 'Файл конфигурации имеет лишние жёсткие ссылки'; return $?; }
|
||||
owner=$(stat -c '%u:%g' -- "$path") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца конфигурации клиента'; return $?; }
|
||||
metadata_owner=$(stat -c '%u:%g' -- "$metadata_file") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца метаданных клиента'; return $?; }
|
||||
local metadata_mode metadata_links
|
||||
metadata_mode=$(stat -c '%a' -- "$metadata_file") || { die "$EXIT_OPERATION" 'Не удалось проверить режим метаданных клиента'; return $?; }
|
||||
metadata_links=$(stat -c '%h' -- "$metadata_file") || { die "$EXIT_OPERATION" 'Не удалось проверить ссылки метаданных клиента'; return $?; }
|
||||
(( (8#$metadata_mode & 077) == 0 && metadata_links == 1 )) || { die "$EXIT_VALIDATION" 'Метаданные клиента небезопасны'; return $?; }
|
||||
[[ $owner == "$metadata_owner" && $owner == "$server_owner" ]] || { die "$EXIT_VALIDATION" 'Владелец файлов клиента не совпадает с серверной конфигурацией'; return $?; }
|
||||
printf '%s\n' "$path"
|
||||
}
|
||||
|
||||
open_validated_client_config() {
|
||||
local path=$1 expected_identity opened_identity fd
|
||||
expected_identity=$(stat -c '%d:%i:%a:%u:%g:%h' -- "$path") || return "$EXIT_OPERATION"
|
||||
exec {fd}<"$path" || return "$EXIT_OPERATION"
|
||||
opened_identity=$(stat -Lc '%d:%i:%a:%u:%g:%h' -- "/proc/self/fd/${fd}") || {
|
||||
exec {fd}<&-
|
||||
return "$EXIT_OPERATION"
|
||||
}
|
||||
if [[ $opened_identity != "$expected_identity" ]]; then
|
||||
exec {fd}<&-
|
||||
return "$EXIT_VALIDATION"
|
||||
fi
|
||||
CLIENT_CONFIG_FD=$fd
|
||||
}
|
||||
|
||||
print_client_details() {
|
||||
local name=$1 index=$2 key=${CLIENT_KEYS[$2]} config_path='-'
|
||||
if [[ ${CLIENT_ORIGINS[$index]} == managed ]]; then
|
||||
if ! config_path=$(validate_client_config_path "$key" "$name" "$index" 2>/dev/null); then config_path='(недоступен)'; fi
|
||||
fi
|
||||
printf 'Имя: %s\nПубличный ключ: %s\nАдреса: %s\nEndpoint: %s\nПоследний handshake: %s\nRX: %s\nTX: %s\nСостояние: %s\nПроисхождение: %s\n' "$name" "$key" "${CLIENT_ADDRESSES[$index]}" "${CLIENT_ENDPOINTS[$index]}" "${CLIENT_HANDSHAKES[$index]}" "${CLIENT_RX[$index]}" "${CLIENT_TX[$index]}" "${CLIENT_STATES[$index]}" "${CLIENT_ORIGINS[$index]}"
|
||||
if [[ ${CLIENT_ORIGINS[$index]} == managed ]]; then printf 'Режим: %s\nСоздан: %s\nПуть конфигурации: %s\n' "${META_MODE[$key]}" "${META_CREATED[$key]}" "$config_path"; fi
|
||||
}
|
||||
|
||||
export_client_config() {
|
||||
local name=$1 show=${2:-0} index key path status=0
|
||||
[[ $show == 0 || $show == 1 ]] || { die "$EXIT_USAGE" 'Недопустимый режим экспорта конфигурации'; return $?; }
|
||||
load_client_model || return $?
|
||||
index=$(resolve_named_client "$name") || return $?
|
||||
[[ ${CLIENT_ORIGINS[$index]} == managed ]] || { die "$EXIT_VALIDATION" 'Для этого клиента нет безопасной managed-конфигурации'; return $?; }
|
||||
key=${CLIENT_KEYS[$index]}
|
||||
path=$(validate_client_config_path "$key" "$name" "$index") || return $?
|
||||
if (( show == 0 )); then printf '%s\n' "$path"; return 0; fi
|
||||
if open_validated_client_config "$path"; then :; else
|
||||
status=$?
|
||||
(( status == EXIT_VALIDATION )) || status=$EXIT_OPERATION
|
||||
die "$status" 'Не удалось безопасно открыть конфигурацию клиента'
|
||||
return "$status"
|
||||
fi
|
||||
if [[ $- == *x* ]]; then
|
||||
set +x
|
||||
if ! cat <&"$CLIENT_CONFIG_FD"; then status=$EXIT_OPERATION; fi
|
||||
set -x
|
||||
elif ! cat <&"$CLIENT_CONFIG_FD"; then
|
||||
status=$EXIT_OPERATION
|
||||
fi
|
||||
exec {CLIENT_CONFIG_FD}<&-
|
||||
if (( status != 0 )); then die "$EXIT_OPERATION" 'Не удалось прочитать конфигурацию клиента'; return $?; fi
|
||||
}
|
||||
|
||||
show_client() {
|
||||
local name=$1 index
|
||||
load_client_model || return $?
|
||||
index=$(resolve_named_client "$name") || return $?
|
||||
print_client_details "$name" "$index"
|
||||
}
|
||||
|
||||
qr_client() {
|
||||
local name=$1 index key path qr_path status
|
||||
load_client_model || return $?
|
||||
index=$(resolve_named_client "$name") || return $?
|
||||
[[ ${CLIENT_ORIGINS[$index]} == managed ]] || { die "$EXIT_VALIDATION" 'Для этого клиента нет безопасной managed-конфигурации'; return $?; }
|
||||
key=${CLIENT_KEYS[$index]}
|
||||
path=$(validate_client_config_path "$key" "$name" "$index") || return $?
|
||||
if qr_path=$(command -v qrencode 2>/dev/null); then :; else qr_path=''; fi
|
||||
if [[ -z $qr_path || ! -f $qr_path || -L $qr_path || ! -x $qr_path ]]; then
|
||||
printf '%s\n' "$path"
|
||||
printf 'Предупреждение: qrencode не найден, используйте путь конфигурации выше\n' >&2
|
||||
return "$EXIT_DEPENDENCY"
|
||||
fi
|
||||
if open_validated_client_config "$path"; then :; else
|
||||
status=$?
|
||||
(( status == EXIT_VALIDATION )) || status=$EXIT_OPERATION
|
||||
die "$status" 'Не удалось безопасно открыть конфигурацию клиента'
|
||||
return "$status"
|
||||
fi
|
||||
if [[ $- == *x* ]]; then
|
||||
set +x
|
||||
if "$qr_path" -t ansiutf8 <&"$CLIENT_CONFIG_FD"; then status=0; else status=$?; fi
|
||||
set -x
|
||||
else
|
||||
if "$qr_path" -t ansiutf8 <&"$CLIENT_CONFIG_FD"; then status=0; else status=$?; fi
|
||||
fi
|
||||
exec {CLIENT_CONFIG_FD}<&-
|
||||
(( status == 0 )) || { die "$EXIT_OPERATION" 'Не удалось сформировать QR-код'; return "$EXIT_OPERATION"; }
|
||||
}
|
||||
|
||||
main() {
|
||||
local status
|
||||
if (( $# == 0 )); then
|
||||
@@ -1412,6 +1540,21 @@ main() {
|
||||
add_client "$@"
|
||||
return $?
|
||||
fi
|
||||
if [[ $1 == show || $1 == config || $1 == qr ]]; then
|
||||
[[ $# -ge 2 ]] || { die "$EXIT_USAGE" "Для команды $1 требуется имя клиента"; print_usage >&2; return $?; }
|
||||
local command=$1 name=$2
|
||||
shift 2
|
||||
case $command in
|
||||
show|qr) (($# == 0)) || { die "$EXIT_USAGE" 'Лишние аргументы команды'; return $?; } ;;
|
||||
config)
|
||||
if (($# == 0)); then export_client_config "$name" 0; return $?; fi
|
||||
[[ $# == 1 && $1 == --show ]] || { die "$EXIT_USAGE" 'Разрешён только флаг config --show'; return $?; }
|
||||
export_client_config "$name" 1; return $?
|
||||
;;
|
||||
esac
|
||||
case $command in show) show_client "$name" ;; qr) qr_client "$name" ;; esac
|
||||
return $?
|
||||
fi
|
||||
if (( $# != 1 )); then
|
||||
if die "$EXIT_USAGE" 'Ожидается одна команда'; then status=0; else status=$?; fi
|
||||
print_usage >&2
|
||||
@@ -1422,7 +1565,7 @@ main() {
|
||||
version) print_version ;;
|
||||
status) load_client_model || return $?; print_status ;;
|
||||
list) load_client_model || return $?; print_list ;;
|
||||
show|config|qr|remove|diagnose|logs|backups|restore|tui) run_placeholder "$1" ;;
|
||||
remove|diagnose|logs|backups|restore|tui) run_placeholder "$1" ;;
|
||||
*)
|
||||
if die "$EXIT_USAGE" "неизвестная команда: $1"; then status=0; else status=$?; fi
|
||||
print_usage >&2
|
||||
|
||||
Reference in New Issue
Block a user