feat(safety): add atomic WireGuard mutation and rollback engine

This commit is contained in:
2026-10-12 23:04:22 +03:00
parent e5e3064885
commit 4e3c5347f2
10 changed files with 1122 additions and 6 deletions
+372 -2
View File
@@ -247,7 +247,7 @@ validate_config_values() {
[[ $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 "$BACKUP_RETENTION" && (( 10#$BACKUP_RETENTION >= 0 && 10#$BACKUP_RETENTION <= 1000 )) || { die "$EXIT_VALIDATION" 'Некорректное число резервных копий'; return $?; }
is_uint "$RECENT_HANDSHAKE_SECONDS" && (( 10#$RECENT_HANDSHAKE_SECONDS <= 604800 )) || { die "$EXIT_VALIDATION" 'Некорректный срок handshake'; return $?; }
}
@@ -256,6 +256,7 @@ load_config() { set_runtime_paths || return $?; parse_config_file "$CONFIG_PATH"
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 RUNTIME_ENDPOINT RUNTIME_HANDSHAKE RUNTIME_RX RUNTIME_TX RUNTIME_ALLOWED
RUNTIME_AVAILABLE=0
@@ -392,6 +393,7 @@ 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=()
DISK_LISTEN_PORT='-'
[[ -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 $?; }
@@ -428,7 +430,7 @@ parse_wireguard_config() {
[[ -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 == ListenPort ]]; then is_uint "$value" && (( 10#$value <= 65535 )) || { die "$EXIT_VALIDATION" 'Некорректный ListenPort'; return $?; }; DISK_LISTEN_PORT=$value; fi
if [[ $key == FwMark ]]; then is_fwmark "$value" || { die "$EXIT_VALIDATION" 'Некорректный FwMark'; return $?; }; fi
continue
fi
@@ -556,6 +558,374 @@ load_client_model() {
load_config || return $?; parse_wireguard_config "$WG_CONF" || return $?; parse_metadata || return $?; load_runtime_state || return $?; build_client_records
}
# Internal contract: run_transaction OPERATION TARGET BUILDER [METADATA_FILE...]. BUILDER is
# called only after the lock and a fresh config/metadata read, with one staging
# path. It must write the complete proposed config and return nonzero to abort.
TRANSACTION_TEMP_PATHS=()
TRANSACTION_BACKUP_PATH=''
transaction_test_hooks_enabled() {
[[ ${WG_MANAGER_TEST_MODE:-0} == 1 && -n ${TEST_ROOT:-} && $TEST_ROOT == /* ]] || return 1
is_control_free "$TEST_ROOT" && path_components_safe "$TEST_ROOT"
}
transaction_cleanup() {
local incoming_status=$? path
for path in "${TRANSACTION_TEMP_PATHS[@]:-}"; do
if [[ -n "$path" && ( -e "$path" || -L "$path" ) ]]; then
if ! rm -f -- "$path"; then
printf 'Предупреждение: не удалось удалить временный файл\n' >&2
fi
fi
done
TRANSACTION_TEMP_PATHS=()
return "$incoming_status"
}
transaction_signal() {
local signal=$1
transaction_cleanup
printf 'Ошибка: операция прервана сигналом %s\n' "$signal" >&2
exit "$EXIT_OPERATION"
}
TRANSACTION_CREATED_TEMP=''
create_transaction_temp() {
local kind=$1 path
TRANSACTION_CREATED_TEMP=''
path=$(mktemp "${WG_CONF%/*}/.wg-manager.${kind}.XXXXXX") || return $?
if [[ ! -f "$path" || -L "$path" ]] || ! chmod 600 -- "$path"; then
rm -f -- "$path"
return "$EXIT_OPERATION"
fi
TRANSACTION_TEMP_PATHS+=("$path")
TRANSACTION_CREATED_TEMP=$path
}
transaction_stat() {
local path=$1 field=$2
case "$field" in
mode) stat -c '%a' -- "$path" ;;
uid) stat -c '%u' -- "$path" ;;
gid) stat -c '%g' -- "$path" ;;
*) return 1 ;;
esac
}
copy_restricted() {
local source=$1 destination=$2
if [[ ${TRANSACTION_METADATA_COPY_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled && [[ $destination == "${BACKUP_DIR:-}"/* && ${destination##*/} != wg.conf ]]; then return 1; fi
[[ -f "$source" && ! -L "$source" ]] || return 1
cp -- "$source" "$destination" && chmod 600 -- "$destination"
}
restore_file_metadata() {
local path=$1 mode=$2 uid=$3 gid=$4
chmod "$mode" -- "$path" || return 1
if (( EUID == 0 )); then
chown "$uid:$gid" -- "$path" || return 1
fi
}
create_backup() {
local source=$1 operation=$2 target=$3 timestamp counter backup allocated_backup='' manifest metadata mode uid gid now metadata_name backup_name backup_prefix backup_counter
local -A metadata_seen=()
if [[ ${TRANSACTION_BACKUP_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; then die "$EXIT_OPERATION" 'Не удалось создать резервную копию'; return $?; fi
[[ -f "$source" && ! -L "$source" ]] || { die "$EXIT_OPERATION" 'Исходный конфиг недоступен для резервного копирования'; return $?; }
[[ ! -L "$BACKUP_DIR" ]] || { die "$EXIT_OPERATION" 'Каталог резервных копий является ссылкой'; return $?; }
if ! mkdir -p -- "$BACKUP_DIR" || ! chmod 700 -- "$BACKUP_DIR"; then die "$EXIT_OPERATION" 'Не удалось подготовить каталог резервных копий'; return $?; fi
now=$(current_epoch) || { die "$EXIT_OPERATION" 'Не удалось получить время для резервной копии'; return $?; }
timestamp=$(date -u -d "@${now}" '+%Y%m%dT%H%M%SZ') || { die "$EXIT_OPERATION" 'Не удалось получить UTC-время для резервной копии'; return $?; }
mode=$(transaction_stat "$source" mode) || return "$EXIT_OPERATION"
uid=$(transaction_stat "$source" uid) || return "$EXIT_OPERATION"
gid=$(transaction_stat "$source" gid) || return "$EXIT_OPERATION"
for metadata in "${@:4}"; do
[[ -f "$metadata" && ! -L "$metadata" && "$metadata" == "${METADATA_DIR%/}"/* && ${metadata%/*} == "${METADATA_DIR%/}" && "$metadata" != *'/../'* && "$metadata" != */.. ]] || { die "$EXIT_OPERATION" 'Некорректный файл метаданных для резервной копии'; return $?; }
path_components_safe "$metadata" || { die "$EXIT_OPERATION" 'Некорректный путь метаданных для резервной копии'; return $?; }
metadata_name=${metadata##*/}
[[ $metadata_name != '' && $metadata_name != wg.conf && $metadata_name != manifest && $metadata_name != */* && $metadata_name =~ ^[A-Za-z0-9._-]+$ && -z ${metadata_seen[$metadata_name]+x} ]] || { die "$EXIT_OPERATION" 'Некорректное имя метаданных для резервной копии'; return $?; }
metadata_seen["$metadata_name"]=1
done
counter=0
for backup in "$BACKUP_DIR/${timestamp}-${WG_INTERFACE}-"*; do
[[ -d "$backup" && ! -L "$backup" ]] || continue
backup_name=${backup##*/}
backup_prefix="${timestamp}-${WG_INTERFACE}-"
[[ $backup_name == "$backup_prefix"* ]] || continue
backup_counter=${backup_name#"$backup_prefix"}
[[ $backup_counter =~ ^[0-9]{3}$ ]] || continue
if (( 10#$backup_counter >= counter )); then counter=$((10#$backup_counter + 1)); fi
done
for ((counter = counter; counter <= 999; counter++)); do
backup=$(printf '%s/%s-%s-%03d' "$BACKUP_DIR" "$timestamp" "$WG_INTERFACE" "$counter")
if mkdir -- "$backup" 2>/dev/null; then allocated_backup=$backup; break; fi
done
[[ -n "$allocated_backup" ]] || { die "$EXIT_OPERATION" 'Не удалось создать уникальную резервную копию'; return $?; }
backup=$allocated_backup
if { [[ ${TRANSACTION_BACKUP_DIR_CHMOD_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; } || ! chmod 700 -- "$backup"; then
rm -rf -- "$backup"
die "$EXIT_OPERATION" 'Не удалось защитить резервную копию'
return $?
fi
copy_restricted "$source" "${backup}/wg.conf" || { rm -rf -- "$backup"; die "$EXIT_OPERATION" 'Не удалось сохранить конфиг в резервную копию'; return $?; }
manifest=${backup}/manifest
if { [[ ${TRANSACTION_BACKUP_MANIFEST_WRITE_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; } || ! {
printf 'INTERFACE=%s\n' "$WG_INTERFACE"
printf 'TIMESTAMP=%s\nOPERATION=%s\nTARGET=%s\n' "$timestamp" "$operation" "$target"
printf 'SOURCE_MODE=%s\nSOURCE_UID=%s\nSOURCE_GID=%s\n' "$mode" "$uid" "$gid"
printf 'CONFIG=wg.conf\n'
for metadata in "${@:4}"; do
metadata_name=${metadata##*/}
copy_restricted "$metadata" "${backup}/${metadata_name}" || { rm -rf -- "$backup"; die "$EXIT_OPERATION" 'Не удалось сохранить метаданные в резервную копию'; return $?; }
printf 'METADATA=%s\n' "$metadata_name"
done
} >"$manifest"; then rm -rf -- "$backup"; die "$EXIT_OPERATION" 'Не удалось записать манифест резервной копии'; return $?; fi
if { [[ ${TRANSACTION_BACKUP_MANIFEST_CHMOD_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; } || ! chmod 600 -- "$manifest"; then
rm -rf -- "$backup"
die "$EXIT_OPERATION" 'Не удалось защитить манифест резервной копии'
return $?
fi
TRANSACTION_BACKUP_PATH=$backup
printf '%s\n' "$backup"
}
validate_backup_manifest() {
local backup=$1 line key value manifest_interface='' manifest_timestamp='' manifest_operation='' manifest_target='' manifest_config='' metadata_name backup_name backup_prefix backup_counter
local -A seen=() metadata_seen=()
[[ -d "$backup" && ! -L "$backup" && -f "$backup/manifest" && ! -L "$backup/manifest" && -f "$backup/wg.conf" && ! -L "$backup/wg.conf" ]] || return 1
[[ $(stat -c '%a' -- "$backup") == 700 && $(stat -c '%a' -- "$backup/manifest") == 600 && $(stat -c '%a' -- "$backup/wg.conf") == 600 ]] || return 1
while IFS= read -r line || [[ -n "$line" ]]; do
[[ $line == *=* ]] || return 1
key=${line%%=*}; value=${line#*=}
case $key in
INTERFACE|TIMESTAMP|OPERATION|TARGET|SOURCE_MODE|SOURCE_UID|SOURCE_GID|CONFIG) ;;
METADATA) metadata_name=$value; [[ $metadata_name =~ ^[A-Za-z0-9._-]+$ && $metadata_name != wg.conf && $metadata_name != manifest && -z ${metadata_seen[$metadata_name]+x} ]] || return 1; metadata_seen[$metadata_name]=1; continue ;;
*) return 1 ;;
esac
[[ -z ${seen[$key]+x} ]] || return 1
seen[$key]=1
case $key in
INTERFACE) manifest_interface=$value ;;
TIMESTAMP) manifest_timestamp=$value ;;
OPERATION) manifest_operation=$value ;;
TARGET) manifest_target=$value ;;
SOURCE_MODE) [[ $value == 400 || $value == 600 ]] || return 1 ;;
SOURCE_UID|SOURCE_GID) [[ $value =~ ^[0-9]+$ ]] || return 1 ;;
CONFIG) manifest_config=$value ;;
esac
done <"$backup/manifest"
[[ ${seen[INTERFACE]+x} && ${seen[TIMESTAMP]+x} && ${seen[OPERATION]+x} && ${seen[TARGET]+x} && ${seen[SOURCE_MODE]+x} && ${seen[SOURCE_UID]+x} && ${seen[SOURCE_GID]+x} && ${seen[CONFIG]+x} ]] || return 1
[[ $manifest_interface == "$WG_INTERFACE" && $manifest_timestamp =~ ^[0-9]{8}T[0-9]{6}Z$ && $manifest_operation =~ ^[A-Za-z][A-Za-z0-9._:-]{0,63}$ && $manifest_target =~ ^[A-Za-z][A-Za-z0-9._:/-]{0,127}$ && $manifest_config == wg.conf ]] || return 1
backup_name=${backup##*/}
backup_prefix="${manifest_timestamp}-${WG_INTERFACE}-"
[[ $backup_name == "$backup_prefix"* ]] || return 1
backup_counter=${backup_name#"$backup_prefix"}
[[ $backup_counter =~ ^[0-9]{3}$ ]] || return 1
for metadata_name in "${!metadata_seen[@]}"; do
[[ -f "$backup/$metadata_name" && ! -L "$backup/$metadata_name" && $(stat -c '%a' -- "$backup/$metadata_name") == 600 ]] || return 1
done
for line in "$backup"/*; do
[[ -e "$line" || -L "$line" ]] || continue
metadata_name=${line##*/}
case $metadata_name in
manifest|wg.conf) ;;
*) [[ -n ${metadata_seen[$metadata_name]+x} ]] || return 1 ;;
esac
done
(parse_wireguard_config "$backup/wg.conf") >/dev/null 2>&1
}
prune_backups() {
local backup candidate count=0 retention=$BACKUP_RETENTION
local -a candidates=()
(( retention < 1 )) && retention=1
for backup in "${BACKUP_DIR}"/*-"${WG_INTERFACE}"-*; do
[[ -d "$backup" && ! -L "$backup" && -f "$backup/manifest" && ! -L "$backup/manifest" && -f "$backup/wg.conf" && ! -L "$backup/wg.conf" ]] || continue
validate_backup_manifest "$backup" || continue
candidates+=("$backup")
done
((${#candidates[@]} > 1)) || return 0
while IFS= read -r candidate; do candidates[count]=$candidate; ((count += 1)); done < <(printf '%s\n' "${candidates[@]}" | LC_ALL=C sort -r)
count=0
for backup in "${candidates[@]}"; do
((count += 1))
(( count <= retention )) && continue
[[ "$backup" == "${BACKUP_DIR}"/*-"${WG_INTERFACE}"-* ]] || continue
rm -rf -- "$backup"
done
}
verify_expected_dump() {
local expected_config=$1 dump=$2 key
local -A expected=() actual=()
parse_runtime_dump "$dump" || return $?
parse_wireguard_config "$expected_config" || return $?
[[ $DISK_LISTEN_PORT == '-' || $RUNTIME_LISTEN_PORT == "$DISK_LISTEN_PORT" ]] || return 1
for key in "${!DISK_ALLOWED[@]}"; do expected["$key"]=${DISK_ALLOWED[$key]}; done
for key in "${!RUNTIME_ALLOWED[@]}"; do actual["$key"]=${RUNTIME_ALLOWED[$key]}; done
for key in "${!expected[@]}"; do [[ ${actual[$key]:-} == "${expected[$key]}" ]] || return 1; done
((${#expected[@]} == ${#actual[@]}))
}
run_syncconf() {
local interface=$1 stripped=$2
wg syncconf "$interface" "$stripped"
}
require_transaction_commands() {
local command_name
for command_name in flock wg wg-quick; do
command -v "$command_name" >/dev/null 2>&1 || { die "$EXIT_DEPENDENCY" "Не найдена обязательная команда: ${command_name}"; return $?; }
done
}
path_components_safe() {
local path=$1 component current='/'
[[ $path == /* && $path != *'//' && $path != */./* && $path != */../* && $path != */.. && $path != */. ]] || return 1
is_control_free "$path" || return 1
local -a components=()
IFS=/ read -r -a components <<<"${path#/}"
for component in "${components[@]}"; do
[[ -n $component ]] || continue
current+="${component}/"
[[ ! -L ${current%/} ]] || return 1
done
}
transaction_subshell_exit() {
local status=$?
transaction_cleanup
if [[ -n ${TRANSACTION_RESULT_FILE:-} ]]; then
printf 'STATUS=%s\nBACKUP=%s\n' "$status" "${TRANSACTION_BACKUP_PATH:-}" >"$TRANSACTION_RESULT_FILE"
if ! chmod 600 -- "$TRANSACTION_RESULT_FILE"; then
printf 'Предупреждение: не удалось защитить transaction result\n' >&2
fi
fi
exit "$status"
}
run_transaction_core() {
local operation=$1 target=$2 builder=$3 metadata lock_parent lock_fd status=0 rollback_status stage new_stripped old_stripped original_copy prior_dump current_dump
local source_mode source_uid source_gid backup_path
local transaction_path
shift 3
TRANSACTION_TEMP_PATHS=(); TRANSACTION_BACKUP_PATH=''
[[ $operation =~ ^[A-Za-z][A-Za-z0-9._:-]{0,63}$ && $target =~ ^[A-Za-z][A-Za-z0-9._:/-]{0,127}$ ]] || { die "$EXIT_VALIDATION" 'Некорректная операция или цель транзакции'; return $?; }
is_control_free "$operation" && is_control_free "$target" || { die "$EXIT_VALIDATION" 'Некорректная операция или цель транзакции'; return $?; }
[[ $builder =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || { die "$EXIT_VALIDATION" 'Некорректный builder транзакции'; return $?; }
declare -F "$builder" >/dev/null 2>&1 || { die "$EXIT_VALIDATION" 'Builder транзакции не является функцией'; return $?; }
set_runtime_paths || return $?
for transaction_path in "$WG_CONF" "$LOCK_FILE" "$BACKUP_DIR" "$METADATA_DIR"; do
path_components_safe "$transaction_path" || { die "$EXIT_VALIDATION" 'Путь транзакции содержит символическую ссылку'; return $?; }
done
require_transaction_commands || return $?
lock_parent=${LOCK_FILE%/*}; [[ "$lock_parent" != "$LOCK_FILE" ]] && mkdir -p -- "$lock_parent" && chmod 700 -- "$lock_parent"
[[ ! -L "$LOCK_FILE" ]] || { die "$EXIT_VALIDATION" 'Lock-файл является ссылкой'; return $?; }
exec {lock_fd}>"$LOCK_FILE" || { die "$EXIT_OPERATION" 'Не удалось открыть lock-файл'; return $?; }
chmod 600 -- "$LOCK_FILE" || { exec {lock_fd}>&-; die "$EXIT_OPERATION" 'Не удалось защитить lock-файл'; return $?; }
if ! flock -n "$lock_fd"; then
exec {lock_fd}>&-
die "$EXIT_OPERATION" 'Другая операция уже выполняется'
return $?
fi
trap 'transaction_subshell_exit' EXIT
trap 'transaction_signal INT' INT
trap 'transaction_signal TERM' TERM
load_config || { status=$?; transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
[[ $LOCK_FILE == "${TRANSACTION_PRE_LOCK_FILE:-$LOCK_FILE}" && $WG_CONF == "${TRANSACTION_PRE_WG_CONF:-$WG_CONF}" && $BACKUP_DIR == "${TRANSACTION_PRE_BACKUP_DIR:-$BACKUP_DIR}" && $METADATA_DIR == "${TRANSACTION_PRE_METADATA_DIR:-$METADATA_DIR}" && $WG_INTERFACE == "${TRANSACTION_PRE_INTERFACE:-$WG_INTERFACE}" ]] || { die "$EXIT_VALIDATION" 'Конфигурация ресурсов изменилась после lock'; status=$?; transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
parse_wireguard_config "$WG_CONF" || { status=$?; transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
parse_metadata || { status=$?; transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
source_mode=$(transaction_stat "$WG_CONF" mode) || status=$?
source_uid=$(transaction_stat "$WG_CONF" uid) || status=$?
source_gid=$(transaction_stat "$WG_CONF" gid) || status=$?
[[ $status == 0 && ( $source_mode == 400 || $source_mode == 600 ) ]] || status=$EXIT_VALIDATION
stage=''
if ((status == 0)); then
create_transaction_temp stage || status=$?
stage=$TRANSACTION_CREATED_TEMP
fi
[[ $status == 0 ]] || { transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
export TRANSACTION_IN_CRITICAL=1
if "$builder" "$stage"; then :; else status=$?; fi
unset TRANSACTION_IN_CRITICAL
if ((status != 0)); then transaction_cleanup; exec {lock_fd}>&-; return "$status"; fi
[[ -s "$stage" && -f "$stage" && ! -L "$stage" ]] || { die "$EXIT_VALIDATION" 'Builder не создал безопасный staged config'; status=$?; transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
create_transaction_temp strip.new || status=$?; new_stripped=$TRANSACTION_CREATED_TEMP
if ((status == 0)); then create_transaction_temp strip.old || status=$?; old_stripped=$TRANSACTION_CREATED_TEMP; fi
if ((status == 0)); then create_transaction_temp original || status=$?; original_copy=$TRANSACTION_CREATED_TEMP; fi
((status == 0)) || { transaction_cleanup; exec {lock_fd}>&-; return "$status"; }
if ! wg-quick strip "$stage" >"$new_stripped"; then status=$EXIT_VALIDATION; fi
if ((status == 0)) && ! wg-quick strip "$WG_CONF" >"$old_stripped"; then status=$EXIT_OPERATION; fi
if ((status == 0)) && ! copy_restricted "$WG_CONF" "$original_copy"; then status=$EXIT_OPERATION; fi
if ((status == 0)); then
backup_path=$(create_backup "$WG_CONF" "$operation" "$target" "$@") || status=$?
TRANSACTION_BACKUP_PATH=${backup_path:-}
if [[ $status == 0 ]]; then
if [[ ${TRANSACTION_ATOMIC_REPLACE_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; then status=$EXIT_OPERATION
elif ! chmod 600 -- "$stage" || ! mv -- "$stage" "$WG_CONF"; then status=$EXIT_OPERATION; fi
fi
fi
if ((status == 0)); then
restore_file_metadata "$WG_CONF" "$source_mode" "$source_uid" "$source_gid" || status=$EXIT_OPERATION
fi
if ((status == 0)) && ! run_syncconf "$WG_INTERFACE" "$new_stripped"; then status=$EXIT_OPERATION; fi
if ((status == 0)); then
current_dump=$(wg show "$WG_INTERFACE" dump 2>/dev/null) || status=$EXIT_OPERATION
[[ $status == 0 ]] && verify_expected_dump "$WG_CONF" "$current_dump" || status=$EXIT_OPERATION
fi
if ((status != 0)) && [[ -n ${backup_path:-} ]]; then
if ! cp -- "$original_copy" "$stage" || ! chmod 600 -- "$stage" || { [[ ${TRANSACTION_ROLLBACK_REPLACE_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; } || ! mv -- "$stage" "$WG_CONF" || ! restore_file_metadata "$WG_CONF" "$source_mode" "$source_uid" "$source_gid"; then
printf 'КРИТИЧНО: откат диска не выполнен\n' >&2; status=$EXIT_ROLLBACK
elif ! run_syncconf "$WG_INTERFACE" "$old_stripped"; then
printf 'КРИТИЧНО: откат runtime не выполнен\n' >&2; status=$EXIT_ROLLBACK
else
rollback_status=0
prior_dump=$(wg show "$WG_INTERFACE" dump 2>/dev/null) || rollback_status=$EXIT_ROLLBACK
if ((rollback_status == 0)) && ! verify_expected_dump "$WG_CONF" "$prior_dump"; then rollback_status=$EXIT_ROLLBACK; fi
if ((rollback_status != 0)); then printf 'КРИТИЧНО: проверка отката не выполнена\n' >&2; status=$EXIT_ROLLBACK; else status=$EXIT_OPERATION; fi
fi
fi
if (( status == 0 )) && [[ -n ${backup_path:-} ]]; then
if ! prune_backups; then
printf 'Предупреждение: не удалось ограничить число резервных копий\n' >&2
fi
fi
transaction_cleanup; exec {lock_fd}>&-; return "$status"
}
run_transaction() {
if (($# < 3)); then
die "$EXIT_USAGE" 'Ожидаются operation, target и builder транзакции'
return $?
fi
local operation=$1 target=$2 builder=$3 result_file result_status result_backup had_errexit=0
shift 3
[[ -n ${WG_MANAGER_CONFIG:-} ]] || { die "$EXIT_VALIDATION" 'Не задан manager config'; return $?; }
set_runtime_paths || return $?
load_config || return $?
result_file=$(mktemp "${WG_CONF%/*}/.wg-manager.result.XXXXXX") || { die "$EXIT_OPERATION" 'Не удалось создать transaction result'; return $?; }
if ! chmod 600 -- "$result_file"; then
rm -f -- "$result_file"
die "$EXIT_OPERATION" 'Не удалось защитить transaction result'
return $?
fi
export TRANSACTION_RESULT_FILE=$result_file TRANSACTION_PRE_LOCK_FILE=$LOCK_FILE TRANSACTION_PRE_WG_CONF=$WG_CONF TRANSACTION_PRE_BACKUP_DIR=$BACKUP_DIR TRANSACTION_PRE_METADATA_DIR=$METADATA_DIR TRANSACTION_PRE_INTERFACE=$WG_INTERFACE
[[ $- == *e* ]] && had_errexit=1
set +e
( trap - EXIT INT TERM; trap 'transaction_subshell_exit' EXIT; trap 'transaction_signal INT' INT; trap 'transaction_signal TERM' TERM; run_transaction_core "$operation" "$target" "$builder" "$@" )
result_status=$?
(( had_errexit == 1 )) && set -e || set +e
if [[ -r $result_file ]]; then
while IFS='=' read -r key value; do
case $key in STATUS) result_status=$value ;; BACKUP) result_backup=$value ;; esac
done <"$result_file"
fi
rm -f -- "$result_file"
TRANSACTION_BACKUP_PATH=${result_backup:-}
unset TRANSACTION_RESULT_FILE TRANSACTION_PRE_LOCK_FILE TRANSACTION_PRE_WG_CONF TRANSACTION_PRE_BACKUP_DIR TRANSACTION_PRE_METADATA_DIR TRANSACTION_PRE_INTERFACE
return "$result_status"
}
print_status() {
local managed=0 unmanaged=0 active=0 index
for index in "${!CLIENT_KEYS[@]}"; do