feat(clients): add safe client removal and archival

This commit is contained in:
2026-10-13 21:05:43 +03:00
parent 2bec75aa9d
commit 75f7547add
4 changed files with 775 additions and 11 deletions
+453 -3
View File
@@ -11,7 +11,7 @@ source "${PROJECT_DIR}/wg-manager.sh"
case $# in
0) ;;
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output|remove-client|remove-cancel|remove-exact-block|remove-rollback|archive-client|archive-path-safety|remove-preserves-neighbors) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
*) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;;
esac
test_group=${1-}
@@ -111,11 +111,11 @@ run_config_tests() {
assert_equal 1 "$(TEST_ROOT="${TEST_ROOT}/root-link" WG_MANAGER_TEST_MODE=1 CONFIG_PATH="${TEST_ROOT}/root-link/config" transaction_test_hooks_enabled >/dev/null 2>&1; printf '%s' "$?")" 'symlink TEST_ROOT отклонён'
assert_equal 1 "$(TEST_ROOT="$TEST_ROOT" WG_MANAGER_TEST_MODE=1 CONFIG_PATH=/tmp/outside transaction_test_hooks_enabled >/dev/null 2>&1; printf '%s' "$?")" 'CONFIG_PATH вне root отклонён'
assert_equal 1 "$(TEST_ROOT="$TEST_ROOT" WG_MANAGER_TEST_MODE=1 CONFIG_PATH="$CONFIG_PATH" WG_CONF=/tmp/outside transaction_test_hooks_enabled >/dev/null 2>&1; printf '%s' "$?")" 'WG_CONF вне root отклонён'
for hook_path_name in CLIENT_DIR METADATA_DIR BACKUP_DIR LOCK_FILE; do
for hook_path_name in CLIENT_DIR METADATA_DIR BACKUP_DIR LOCK_FILE LOG_DIR ARCHIVE_DIR; do
assert_equal 1 "$(TEST_ROOT="$TEST_ROOT" WG_MANAGER_TEST_MODE=1 CONFIG_PATH="$CONFIG_PATH" WG_CONF="$WG_CONF"; declare "$hook_path_name=/tmp/outside"; if transaction_test_hooks_enabled >/dev/null 2>&1; then printf 0; else printf 1; fi)" "${hook_path_name} вне root отклонён"
done
mkdir -- "${TEST_ROOT}/hook-real"; ln -s -- "${TEST_ROOT}/hook-real" "${TEST_ROOT}/hook-link"
for hook_path_name in WG_CONF CLIENT_DIR METADATA_DIR BACKUP_DIR LOCK_FILE; do
for hook_path_name in WG_CONF CLIENT_DIR METADATA_DIR BACKUP_DIR LOCK_FILE LOG_DIR ARCHIVE_DIR; do
assert_equal 1 "$(TEST_ROOT="$TEST_ROOT" WG_MANAGER_TEST_MODE=1 CONFIG_PATH="$CONFIG_PATH"; declare "$hook_path_name=${TEST_ROOT}/hook-link/value"; if transaction_test_hooks_enabled >/dev/null 2>&1; then printf 0; else printf 1; fi)" "${hook_path_name} с symlink-компонентом отклонён"
done
rm -rf -- "${TEST_ROOT}/hook-real" "${TEST_ROOT}/hook-link"
@@ -2342,6 +2342,452 @@ scan_test_sources_for_host_path
scan_sources_for_absolute_external_commands
fi
run_m6_tests() {
local config_file=$TEST_ROOT/m6.conf fixture=$TEST_ROOT/wg0.conf runtime_dir=$TEST_ROOT/dumps
local output status before archive_dir
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"; chmod 600 -- "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_m2_config "$config_file" "$fixture"
replace_config_line "$config_file" 'CLIENT_DIR=' "CLIENT_DIR=\"$TEST_ROOT/client-files\""
export WG_MANAGER_CONFIG=$config_file WG_MANAGER_BACKUP_DIR=$TEST_ROOT/backups WG_MANAGER_LOG_DIR=$TEST_ROOT/logs WG_MANAGER_METADATA_DIR=$TEST_ROOT/clients WG_MANAGER_ARCHIVE_DIR=$TEST_ROOT/archive WG_MANAGER_LOCK_FILE=$TEST_ROOT/lock WG_MANAGER_NOW=1700000100 WG_MANAGER_TEST_MODE=1
export WG_MOCK_INTERFACE=wg0 TEST_MOCK_LOG=$TEST_ROOT/mocks.log WG_MOCK_DUMP_SEQUENCE_DIR=$runtime_dir
set_runtime_paths
load_config
rm -rf -- "$TEST_ROOT/clients" "$TEST_ROOT/client-files" "$TEST_ROOT/archive" "$TEST_ROOT/logs" "$runtime_dir"
mkdir -p -- "$METADATA_DIR" "$CLIENT_DIR" "$runtime_dir"; chmod 700 -- "$METADATA_DIR" "$CLIENT_DIR" "$runtime_dir"
write_alice_metadata; chmod 600 -- "$METADATA_DIR/alice.meta"
printf '[Interface]\nAddress = 192.0.2.2/32\nPrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\n' >"$CLIENT_DIR/alice.conf"
printf 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\n' >"$CLIENT_DIR/alice.key"
printf 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\n' >"$CLIENT_DIR/alice.pub"
printf 'GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=\n' >"$CLIENT_DIR/alice.psk"; chmod 600 -- "$CLIENT_DIR"/alice.*
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/2"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\n' >"$runtime_dir/3"
if [[ $test_group == remove-exact-block || $test_group == remove-preserves-neighbors ]]; then
cat >"$fixture" <<'EOF'
[Interface]
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
# wg-manager-client: before
[Peer]
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32
Endpoint = before.example.invalid:51820
# wg-manager-client: alice
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
AllowedIPs = 192.0.2.2/32
Endpoint = vpn.example.invalid:51820
# wg-manager-client: after
[Peer]
PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=
AllowedIPs = 192.0.2.4/32
Endpoint = after.example.invalid:51820
EOF
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=\t(none)\tbefore.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\nDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=\t(none)\tafter.example.invalid:51820\t192.0.2.4/32\t0\t0\t0\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=\t(none)\tbefore.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\nDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=\t(none)\tafter.example.invalid:51820\t192.0.2.4/32\t0\t0\t0\t0\n' >"$runtime_dir/2"
begin_test 'remove exact block preserves byte-identical neighboring peers'
local before_neighbor after_neighbor after_before after_after
before_neighbor=$(awk '/# wg-manager-client: before/,/Endpoint = before.example.invalid:51820/' "$fixture")
after_neighbor=$(awk '/# wg-manager-client: after/{on=1} on{print}' "$fixture")
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal 0 "$status" 'удаление точного блока с соседями'
after_before=$(awk '/# wg-manager-client: before/,/Endpoint = before.example.invalid:51820/' "$fixture")
after_after=$(awk '/# wg-manager-client: after/{on=1} on{print}' "$fixture")
assert_equal "$before_neighbor" "$after_before" 'предшествующий peer сохранён побайтно'
assert_equal "$after_neighbor" "$after_after" 'следующий peer сохранён побайтно'
assert_equal '' "$(grep -E 'BBBB|wg-manager-client: alice|192\.0\.2\.2/32' "$fixture" || true)" 'target block removed completely'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' <<<"$output" || true)" 'neighbor removal output is secret-free'
elif [[ $test_group == remove-client ]]; then
begin_test 'remove identity and argument refusals preserve the source'
local remove_before=$(sha256sum -- "$fixture")
set +e; output=$(remove_client alice extra 2>&1); status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'лишний аргумент remove отклонён'
assert_equal "$remove_before" "$(sha256sum -- "$fixture")" 'лишний аргумент не меняет конфиг'
set +e; output=$(/bin/bash "$PROJECT_DIR/wg-manager.sh" remove 2>&1); status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'отсутствующее имя remove отклонено'
set +e; output=$(remove_client absent --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'отсутствующий клиент отклонён'
assert_equal "$remove_before" "$(sha256sum -- "$fixture")" 'отсутствующий клиент не меняет конфиг'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/### Client legacy/' "$fixture"
rm -f -- "$METADATA_DIR/alice.meta"
set +e; output=$(remove_client legacy --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'legacy peer без metadata отклонён'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
replace_config_line "$METADATA_DIR/alice.meta" 'PUBLIC_KEY=' 'PUBLIC_KEY=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF='
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'metadata public key mismatch отклонён'
assert_equal "$remove_before" "$(sha256sum -- "$fixture")" 'public key mismatch не меняет конфиг'
write_alice_metadata
replace_config_line "$METADATA_DIR/alice.meta" 'IPV4=' 'IPV4=192.0.2.99'
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'несовпадение адреса managed identity отклонено'
assert_equal "$remove_before" "$(sha256sum -- "$fixture")" 'несовпадение identity не меняет конфиг'
cat >>"$fixture" <<'EOF'
# wg-manager-client: alice
[Peer]
PublicKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
AllowedIPs = 192.0.2.3/32
EOF
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=\t(none)\tvpn.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\n' >"$runtime_dir/1"
rm -f -- "$runtime_dir/counter"
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'duplicate display name is refused'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
replace_config_line "$METADATA_DIR/alice.meta" 'PUBLIC_KEY=' 'PUBLIC_KEY=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF='
replace_config_line "$METADATA_DIR/alice.meta" 'IPV4=' 'IPV4=192.0.2.3'
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\t(none)\tvpn.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\n' >"$runtime_dir/1"
rm -f -- "$runtime_dir/counter"
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'runtime-only identity is refused'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/### Client alice/' "$fixture"
write_alice_metadata
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'legacy marker is not a managed identity'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
printf '\n# wg-manager-client: alice\n[Peer]\nPublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nAllowedIPs = 192.0.2.2/32\nEndpoint = vpn.example.invalid:51820\n' >>"$fixture"
local duplicate_before=$(sha256sum -- "$fixture")
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'duplicate exact managed block is refused'
assert_equal "$duplicate_before" "$(sha256sum -- "$fixture")" 'duplicate block leaves source unchanged'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
begin_test 'locked identity revalidation rejects metadata changed after confirmation'
load_client_model
local race_index race_key race_path
race_index=$(resolve_named_client alice); race_key=${CLIENT_KEYS[$race_index]}; race_path=${META_CONFIG[$race_key]}
REMOVE_NAME=alice REMOVE_KEY=$race_key REMOVE_CONFIG_PATH=$race_path REMOVE_META_FILE=${META_FILE[$race_key]} REMOVE_ALLOWED=${DISK_ALLOWED[$race_key]}
remove_builder_mutate() { replace_config_line "$REMOVE_META_FILE" 'IPV4=' 'IPV4=192.0.2.99'; parse_metadata; remove_peer_builder "$1"; }
local race_before=$(sha256sum -- "$fixture")
set +e; output=$(run_transaction remove alice remove_builder_mutate "$REMOVE_META_FILE" 2>&1); status=$?; set -e
unset -f remove_builder_mutate
assert_equal "$EXIT_VALIDATION" "$status" 'изменение metadata после lock отклонено'
assert_equal "$race_before" "$(sha256sum -- "$fixture")" 'race не изменяет серверный конфиг'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
elif [[ $test_group == remove-cancel ]]; then
begin_test 'explicit cancellation helper leaves removal untouched'
local cancel_before=$(sha256sum -- "$fixture")
set +e; output=$(printf 'n\n' | confirm_remove alice 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' '192.0.2.2/32' 2>&1); status=$?; set -e
assert_equal 1 "$status" 'отмена подтверждения возвращает отказ'
assert_contains 'Удалить клиента alice' "$output" 'подтверждение показывает имя'
assert_equal "$cancel_before" "$(sha256sum -- "$fixture")" 'отмена не меняет конфиг'
elif [[ -z $test_group || $test_group == remove-client || $test_group == archive-client ]]; then
begin_test 'managed remove archives artifacts and removes exact peer'
before=$(sha256sum -- "$fixture")
local meta_before conf_before key_before pub_before psk_before
meta_before=$(sha256sum -- "$METADATA_DIR/alice.meta")
conf_before=$(sha256sum -- "$CLIENT_DIR/alice.conf")
key_before=$(sha256sum -- "$CLIENT_DIR/alice.key")
pub_before=$(sha256sum -- "$CLIENT_DIR/alice.pub")
psk_before=$(sha256sum -- "$CLIENT_DIR/alice.psk")
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal 0 "$status" 'успешное удаление managed клиента'
assert_equal '' "$(grep -F 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' "$fixture" || true)" 'точный peer удалён'
archive_dir=$(find "$TEST_ROOT/archive" -mindepth 1 -maxdepth 1 -type d -print -quit)
assert_equal 0 "$([[ -f $archive_dir/alice.meta ]] && printf 0 || printf 1)" 'метаданные архивированы'
for archived_name in alice.meta alice.conf alice.key alice.pub alice.psk; do
local original_path
if [[ $archived_name == alice.meta ]]; then original_path="$METADATA_DIR/alice.meta"; else original_path="$CLIENT_DIR/$archived_name"; fi
assert_equal 0 "$([[ -f $archive_dir/$archived_name && $(stat -c '%a' -- "$archive_dir/$archived_name") == 600 ]] && printf 0 || printf 1)" "архивный ${archived_name} защищён"
assert_equal 1 "$([[ -e $original_path || -L $original_path ]] && printf 0 || printf 1)" "исходный ${archived_name} удалён"
done
local archived_hash
archived_hash=$(sha256sum -- "$archive_dir/alice.meta"); assert_equal "${meta_before%% *}" "${archived_hash%% *}" 'архив метаданных побайтно совпадает'
archived_hash=$(sha256sum -- "$archive_dir/alice.conf"); assert_equal "${conf_before%% *}" "${archived_hash%% *}" 'архив конфига побайтно совпадает'
archived_hash=$(sha256sum -- "$archive_dir/alice.key"); assert_equal "${key_before%% *}" "${archived_hash%% *}" 'архив ключа побайтно совпадает'
archived_hash=$(sha256sum -- "$archive_dir/alice.pub"); assert_equal "${pub_before%% *}" "${archived_hash%% *}" 'архив public побайтно совпадает'
archived_hash=$(sha256sum -- "$archive_dir/alice.psk"); assert_equal "${psk_before%% *}" "${archived_hash%% *}" 'архив PSK побайтно совпадает'
assert_equal 700 "$(stat -c '%a' -- "$archive_dir")" 'каталог архива защищён'
assert_equal '' "$(grep -E 'EEEE|GGGG|PrivateKey|PresharedKey' <<<"$output" || true)" 'вывод удаления без секретов'
assert_contains 'operation=remove' "$(<"$TEST_ROOT/logs/wg-manager.log")" 'аудит удаления'
assert_contains 'client=alice' "$(<"$TEST_ROOT/logs/wg-manager.log")" 'аудит содержит client'
assert_contains 'actor_uid=' "$(<"$TEST_ROOT/logs/wg-manager.log")" 'аудит содержит actor uid'
assert_contains 'error=success' "$(<"$TEST_ROOT/logs/wg-manager.log")" 'аудит содержит outcome category'
assert_contains 'timestamp=2023-11-14T22:15:00Z' "$(<"$TEST_ROOT/logs/wg-manager.log")" 'аудит содержит детерминированное UTC-время'
assert_equal '' "$(grep -E 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=|EEEE|GGGG|PrivateKey|PresharedKey' "$TEST_ROOT/logs/wg-manager.log" || true)" 'аудит без секретов'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
printf '[Interface]\nPrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\nAddress = 192.0.2.2/32\n' >"$CLIENT_DIR/alice.conf"
printf 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\n' >"$CLIENT_DIR/alice.key"
printf 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\n' >"$CLIENT_DIR/alice.pub"
printf 'GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=\n' >"$CLIENT_DIR/alice.psk"
chmod 600 -- "$CLIENT_DIR"/* "$METADATA_DIR/alice.meta"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/2"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\n' >"$runtime_dir/3"
rm -f -- "$runtime_dir/counter"
local fail_meta fail_conf fail_key fail_pub fail_psk archive_count
fail_meta=$(sha256sum -- "$METADATA_DIR/alice.meta"); fail_conf=$(sha256sum -- "$CLIENT_DIR/alice.conf")
fail_key=$(sha256sum -- "$CLIENT_DIR/alice.key"); fail_pub=$(sha256sum -- "$CLIENT_DIR/alice.pub"); fail_psk=$(sha256sum -- "$CLIENT_DIR/alice.psk")
archive_count=$(find "$ARCHIVE_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
export M6_ARCHIVE_COPY_FAIL_INDEX=1
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
unset M6_ARCHIVE_COPY_FAIL_INDEX
assert_equal "$EXIT_OPERATION" "$status" 'ошибка архивации возвращает operation'
assert_contains 'Ошибка:' "$output" 'ошибка архивации имеет диагностику'
assert_contains 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' "$(<"$fixture")" 'при ошибке архивации peer восстановлен'
assert_equal 0 "$([[ -f $CLIENT_DIR/alice.conf && -f $METADATA_DIR/alice.meta ]] && printf 0 || printf 1)" 'исходные артефакты доступны для восстановления'
for artifact in "$METADATA_DIR/alice.meta" "$CLIENT_DIR/alice.conf" "$CLIENT_DIR/alice.key" "$CLIENT_DIR/alice.pub" "$CLIENT_DIR/alice.psk"; do
assert_equal 600 "$(stat -c '%a' -- "$artifact")" "copy failure сохраняет режим ${artifact##*/}"
done
assert_equal "${fail_meta%% *}" "$(sha256sum -- "$METADATA_DIR/alice.meta" | cut -d' ' -f1)" 'copy failure сохраняет metadata bytes'
assert_equal "${fail_conf%% *}" "$(sha256sum -- "$CLIENT_DIR/alice.conf" | cut -d' ' -f1)" 'copy failure сохраняет config bytes'
assert_equal "$archive_count" "$(find "$ARCHIVE_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)" 'copy failure cleans incomplete archive'
export M6_ARCHIVE_COPY_FAIL_INDEX=4
rm -f -- "$runtime_dir/counter"
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
unset M6_ARCHIVE_COPY_FAIL_INDEX
assert_equal "$EXIT_OPERATION" "$status" 'ошибка финального копирования возвращает operation'
assert_equal 0 "$([[ -f $CLIENT_DIR/alice.conf && -f $METADATA_DIR/alice.meta ]] && printf 0 || printf 1)" 'финальная ошибка сохраняет исходные артефакты'
assert_equal "${fail_key%% *}" "$(sha256sum -- "$CLIENT_DIR/alice.key" | cut -d' ' -f1)" 'final copy failure сохраняет key bytes'
assert_equal "${fail_pub%% *}" "$(sha256sum -- "$CLIENT_DIR/alice.pub" | cut -d' ' -f1)" 'final copy failure сохраняет pub bytes'
assert_equal "${fail_psk%% *}" "$(sha256sum -- "$CLIENT_DIR/alice.psk" | cut -d' ' -f1)" 'final copy failure сохраняет psk bytes'
rm -f -- "$runtime_dir/counter"
export M6_ARCHIVE_REMOVE_FAIL_INDEX=1
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
unset M6_ARCHIVE_REMOVE_FAIL_INDEX
assert_equal "$EXIT_OPERATION" "$status" 'ошибка удаления после частичной архивации возвращает operation'
assert_equal 0 "$([[ -f $CLIENT_DIR/alice.conf && -f $CLIENT_DIR/alice.key && -f $METADATA_DIR/alice.meta ]] && printf 0 || printf 1)" 'частичная архивация восстановила исходные файлы'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' <<<"$output" || true)" 'ошибка удаления без секретов'
begin_test 'artifact recovery failure remains critical after server rollback'
rm -f -- "$runtime_dir/counter"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/2"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\n' >"$runtime_dir/3"
export M6_ARCHIVE_REMOVE_FAIL_INDEX=1 M6_ARCHIVE_RECOVERY_FAIL_INDEX=1
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
unset M6_ARCHIVE_REMOVE_FAIL_INDEX M6_ARCHIVE_RECOVERY_FAIL_INDEX
assert_equal "$EXIT_ROLLBACK" "$status" 'ошибка recovery сохраняет critical status'
assert_contains 'КРИТИЧНО' "$output" 'ошибка recovery явно critical'
assert_contains 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' "$(<"$fixture")" 'server rollback выполнен при recovery failure'
assert_equal 0 "$( [[ -n $(find "$ARCHIVE_DIR" -mindepth 2 -type f \( -name alice.meta -o -name alice.conf \) -print -quit) ]] && printf 0 || printf 1 )" 'recovery archive remains available'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' <<<"$output" || true)" 'recovery failure без секретов'
write_alice_metadata
printf '[Interface]\nPrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\nAddress = 192.0.2.2/32\n' >"$CLIENT_DIR/alice.conf"
printf 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\n' >"$CLIENT_DIR/alice.key"
printf 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\n' >"$CLIENT_DIR/alice.pub"
printf 'GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=\n' >"$CLIENT_DIR/alice.psk"
chmod 600 -- "$CLIENT_DIR"/* "$METADATA_DIR/alice.meta"
begin_test 'post-mutation audit failure reports partial success safely'
rm -f -- "$runtime_dir/counter"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/2"
export M6_AUDIT_FAIL=1
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
unset M6_AUDIT_FAIL
assert_equal "$EXIT_OPERATION" "$status" 'ошибка аудита после mutation возвращает operation'
assert_contains 'частичный результат' "$output" 'ошибка аудита явно обозначает partial success'
assert_equal '' "$(grep -F 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' "$fixture" || true)" 'при audit failure peer остаётся удалённым'
assert_equal 1 "$( [[ ! -e $CLIENT_DIR/alice.conf && ! -e $METADATA_DIR/alice.meta ]] && printf 1 || printf 0 )" 'при audit failure исходные артефакты архивированы'
assert_equal '' "$(grep -E 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=|EEEE|GGGG|PrivateKey|PresharedKey' <<<"$output" || true)" 'audit failure без секретов'
cp -- "$PROJECT_DIR/tests/fixtures/m3/source.conf" "$fixture"
sed -i 's/wg-manager-client: fixture/wg-manager-client: alice/' "$fixture"
sed -i '/\[Peer\]/{N; s/\[Peer\]\n# wg-manager-client: alice/# wg-manager-client: alice\n[Peer]/;}' "$fixture"
write_alice_metadata
printf '[Interface]\nPrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=\nAddress = 192.0.2.2/32\n' >"$CLIENT_DIR/alice.conf"
chmod 600 -- "$CLIENT_DIR/alice.conf"
begin_test 'optional client artifacts are not fabricated'
rm -f -- "$CLIENT_DIR/alice.key" "$CLIENT_DIR/alice.pub" "$CLIENT_DIR/alice.psk" "$runtime_dir/counter"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\n' >"$runtime_dir/2"
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal 0 "$status" 'удаление без optional artifacts успешно'
local optional_archive
optional_archive=$(find "$ARCHIVE_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -n | tail -n1 | cut -d' ' -f2-)
assert_equal 1 "$( [[ -f $optional_archive/alice.meta && -f $optional_archive/alice.conf && ! -e $optional_archive/alice.key && ! -e $optional_archive/alice.pub && ! -e $optional_archive/alice.psk ]] && printf 1 || printf 0 )" 'optional artifacts не создаются в архиве'
fi
if [[ $test_group == remove-cancel ]]; then
begin_test 'simulated interactive cancellation previews safe identity'
remove_is_interactive() { return 0; }
local interactive_before=$(sha256sum -- "$fixture")
set +e; output=$(printf 'n\n' | remove_client alice 2>&1); status=$?; set -e
unset -f remove_is_interactive
remove_is_interactive() { [[ -t 0 && -t 1 ]]; }
assert_equal 0 "$status" 'интерактивная отмена успешна'
assert_contains 'Удалить клиента alice' "$output" 'preview содержит имя'
assert_contains '192.0.2.2/32' "$output" 'preview содержит адрес'
assert_contains '*BBBBBBB=' "$output" 'preview содержит только суффикс ключа'
assert_equal '' "$(grep -E 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=|PrivateKey|PresharedKey|EEEE|GGGG' <<<"$output" || true)" 'preview без секретов'
assert_equal "$interactive_before" "$(sha256sum -- "$fixture")" 'интерактивная отмена без изменений'
begin_test 'cancelled removal survives audit failure without mutation'
remove_is_interactive() { return 0; }
export M6_AUDIT_FAIL=1
set +e; output=$(printf 'n\n' | remove_client alice 2>&1); status=$?; set -e
unset M6_AUDIT_FAIL
unset -f remove_is_interactive
remove_is_interactive() { [[ -t 0 && -t 1 ]]; }
assert_equal 0 "$status" 'отмена при ошибке аудита остаётся успешной'
assert_contains 'аудит не записан' "$output" 'ошибка аудита отмены предупреждается'
assert_equal "$interactive_before" "$(sha256sum -- "$fixture")" 'ошибка аудита отмены без изменений'
begin_test 'noninteractive removal requires explicit confirmation'
before=$(sha256sum -- "$fixture")
set +e; remove_client alice >/dev/null 2>"$TEST_ROOT/cancel.err"; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'без --yes требуется подтверждение'
assert_equal "$before" "$(sha256sum -- "$fixture")" 'отмена не меняет конфиг'
set +e; remove_client alice --yes --yes >/dev/null 2>"$TEST_ROOT/cancel.err"; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'дублированный --yes отклонён'
set +e; remove_client alice --bad >/dev/null 2>"$TEST_ROOT/cancel.err"; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'неизвестный флаг отклонён'
fi
if [[ $test_group == remove-rollback ]]; then
begin_test 'apply failure rolls back peer and preserves artifacts'
cp -- "$runtime_dir/3" "$runtime_dir/2"
export WG_SYNC_FAIL_ONCE_FILE=$TEST_ROOT/sync-fail
set +e; remove_client alice --yes >/dev/null 2>"$TEST_ROOT/rollback.err"; status=$?; set -e
assert_equal "$EXIT_OPERATION" "$status" 'ошибка apply возвращает operation'
assert_contains 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' "$(<"$fixture")" 'peer восстановлен'
assert_equal 0 "$([[ -f $CLIENT_DIR/alice.conf && -f $METADATA_DIR/alice.meta ]] && printf 0 || printf 1)" 'артефакты сохранены'
unset WG_SYNC_FAIL_ONCE_FILE
begin_test 'post-apply verification failure rolls back disk runtime and artifacts'
local rollback_before=$(sha256sum -- "$fixture")
rm -f -- "$runtime_dir/counter"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51819\t0\n' >"$runtime_dir/1"
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\n' >"$runtime_dir/2"
set +e; output=$(remove_client alice --yes 2>&1); status=$?; set -e
assert_equal "$EXIT_OPERATION" "$status" 'ошибка post-apply verification возвращает operation'
assert_equal "$rollback_before" "$(sha256sum -- "$fixture")" 'verification rollback restores exact config'
assert_equal 0 "$([[ -f $CLIENT_DIR/alice.conf && -f $METADATA_DIR/alice.meta ]] && printf 0 || printf 1)" 'verification rollback сохраняет артефакты'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' <<<"$output" || true)" 'verification failure is secret-free'
fi
if [[ $test_group == archive-path-safety ]]; then
begin_test 'unsafe archive path is rejected'
local archive_link=$TEST_ROOT/archive-link
ln -s -- "$TEST_ROOT" "$archive_link"; export WG_MANAGER_ARCHIVE_DIR=$archive_link
set +e; remove_client alice --yes >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink archive rejected'
assert_equal 0 "$([[ -f $CLIENT_DIR/alice.conf ]] && printf 0 || printf 1)" 'при unsafe archive файлы сохранены'
export WG_MANAGER_ARCHIVE_DIR="$TEST_ROOT/archive"
REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= REMOVE_CONFIG_PATH="$CLIENT_DIR/alice.conf" REMOVE_META_FILE="$METADATA_DIR/alice.meta"
begin_test 'archive validates source identity, modes, links, and collisions'
printf 'sentinel\n' >"$TEST_ROOT/archive-sentinel"
mv -- "$CLIENT_DIR/alice.conf" "$CLIENT_DIR/alice.real.conf"
ln -s -- "$TEST_ROOT/archive-sentinel" "$CLIENT_DIR/alice.conf"
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink source config rejected'
assert_equal sentinel "$(<"$TEST_ROOT/archive-sentinel")" 'symlink target preserved'
rm -- "$CLIENT_DIR/alice.conf"; mv -- "$CLIENT_DIR/alice.real.conf" "$CLIENT_DIR/alice.conf"
chmod 644 "$CLIENT_DIR/alice.conf"
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive source config rejected'
chmod 600 "$CLIENT_DIR/alice.conf"
local archive_real_stat
archive_real_stat=$(command -v stat)
stat() {
if [[ ${2:-} == '%u:%g' && ${4:-} == "$CLIENT_DIR/alice.conf" ]]; then printf '999:999\n'; return 0; fi
"$archive_real_stat" "$@"
}
set +e; output=$(archive_remove_artifacts 2>&1); status=$?; set -e
unset -f stat
assert_equal "$EXIT_VALIDATION" "$status" 'owner mismatch source rejected'
assert_contains 'небезопасные атрибуты' "$output" 'owner mismatch archive diagnostic'
ln -- "$CLIENT_DIR/alice.conf" "$TEST_ROOT/alice.conf.link"
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'hardlinked source config rejected'
rm -- "$TEST_ROOT/alice.conf.link"
mv -- "$METADATA_DIR/alice.meta" "$METADATA_DIR/alice.real.meta"
ln -s -- "$TEST_ROOT/archive-sentinel" "$METADATA_DIR/alice.meta"
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink source metadata rejected'
rm -- "$METADATA_DIR/alice.meta"; mv -- "$METADATA_DIR/alice.real.meta" "$METADATA_DIR/alice.meta"
chmod 644 "$METADATA_DIR/alice.meta"
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive source metadata rejected'
chmod 600 "$METADATA_DIR/alice.meta"
ln -- "$METADATA_DIR/alice.meta" "$TEST_ROOT/alice.meta.link"
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'hardlinked source metadata rejected'
rm -- "$TEST_ROOT/alice.meta.link"
printf 'old\n' >"$TEST_ROOT/archive-existing"
rm -f -- "$TEST_ROOT/archive-existing"
local archive_stamp
archive_stamp=$(date -u -d @1700000100 +%Y%m%dT%H%M%SZ)
rm -rf -- "$ARCHIVE_DIR/${archive_stamp}-wg0-alice"
mkdir -- "$ARCHIVE_DIR/${archive_stamp}-wg0-alice"; chmod 700 -- "$ARCHIVE_DIR/${archive_stamp}-wg0-alice"
write_alice_metadata
set +e; archive_remove_artifacts >/dev/null 2>"$TEST_ROOT/archive.err"; status=$?; set -e
assert_equal 0 "$status" 'archive collision receives a unique suffix'
assert_equal 0 "$([[ -d $ARCHIVE_DIR/${archive_stamp}-wg0-alice ]] && printf 0 || printf 1)" 'existing archive preserved'
assert_equal "${ARCHIVE_DIR}/${archive_stamp}-wg0-alice-1" "$REMOVE_ARCHIVE_PATH" 'archive uses exact collision suffix'
for archived_name in alice.meta alice.conf alice.key alice.pub alice.psk; do
assert_equal 0 "$([[ -f ${REMOVE_ARCHIVE_PATH}/${archived_name} && $(stat -c '%a' -- "${REMOVE_ARCHIVE_PATH}/${archived_name}") == 600 ]] && printf 0 || printf 1)" "архив содержит ${archived_name} с режимом 600"
done
assert_equal 700 "$(stat -c '%a' -- "$ARCHIVE_DIR")" 'корень архива имеет режим 700'
assert_equal 700 "$(stat -c '%a' -- "$REMOVE_ARCHIVE_PATH")" 'каталог архива имеет режим 700'
begin_test 'audit rejects symlinked log objects without mutation'
local audit_target=$TEST_ROOT/audit-sentinel
printf 'sentinel\n' >"$audit_target"
rm -rf -- "$LOG_DIR"
ln -s -- "$audit_target" "$LOG_DIR"
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
assert_failure "$status" 'symlinked log directory rejected'
assert_equal sentinel "$(<"$audit_target")" 'audit symlink target preserved'
rm -f -- "$LOG_DIR"; mkdir -- "$LOG_DIR"; chmod 700 -- "$LOG_DIR"
ln -s -- "$audit_target" "$LOG_DIR/wg-manager.log"
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
assert_failure "$status" 'symlinked audit file rejected'
assert_equal sentinel "$(<"$audit_target")" 'audit file target preserved'
rm -f -- "$LOG_DIR/wg-manager.log"
begin_test 'audit log directory mode and owner are validated'
chmod 755 -- "$LOG_DIR"
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive log directory rejected'
chmod 700 -- "$LOG_DIR"
local log_real_stat
log_real_stat=$(command -v stat)
stat() {
if [[ ${2:-} == '%u:%g' && ${4:-} == "$LOG_DIR" ]]; then printf '999:999\n'; return 0; fi
"$log_real_stat" "$@"
}
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
unset -f stat
assert_equal "$EXIT_VALIDATION" "$status" 'owner mismatch log directory rejected'
begin_test 'audit validates opened FD identity and restrictive attributes'
printf 'existing\n' >"$LOG_DIR/wg-manager.log"; chmod 600 -- "$LOG_DIR/wg-manager.log"
local audit_real_stat audit_before
audit_real_stat=$(command -v stat); audit_before=$(<"$LOG_DIR/wg-manager.log")
stat() {
if [[ ${1:-} == -Lc && ${4:-} == /proc/self/fd/* ]]; then printf '999:999:600:999:999:1\n'; return 0; fi
"$audit_real_stat" "$@"
}
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
unset -f stat
assert_equal "$EXIT_VALIDATION" "$status" 'audit FD identity mismatch rejected'
assert_equal "$audit_before" "$(<"$LOG_DIR/wg-manager.log")" 'FD mismatch writes no bytes'
chmod 644 -- "$LOG_DIR/wg-manager.log"
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive audit file rejected'
chmod 600 -- "$LOG_DIR/wg-manager.log"
ln -- "$LOG_DIR/wg-manager.log" "$TEST_ROOT/audit-hardlink"
set +e; output=$(REMOVE_NAME=alice REMOVE_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= remove_audit failed 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'hardlinked audit file rejected'
rm -f -- "$TEST_ROOT/audit-hardlink" "$LOG_DIR/wg-manager.log"
fi
}
if [[ $test_group == config ]]; then
run_config_tests
elif [[ $test_group == show-client || $test_group == config-export || $test_group == qr || $test_group == path-safety || $test_group == no-secret-default-output ]]; then
@@ -2353,6 +2799,8 @@ elif [[ $test_group == transaction || $test_group == rollback || $test_group ==
[[ $test_group == secret-modes ]] && run_m4_tests
elif [[ $test_group == add-client || $test_group == address-allocation || $test_group == add-collisions || $test_group == add-rollback || $test_group == no-secret-logs || $test_group == add-golden-config || $test_group == add-invalid-names ]]; then
run_m4_tests
elif [[ $test_group == remove-client || $test_group == remove-cancel || $test_group == remove-exact-block || $test_group == remove-rollback || $test_group == archive-client || $test_group == archive-path-safety || $test_group == remove-preserves-neighbors ]]; then
run_m6_tests
else
run_config_tests
run_gum_tests
@@ -2364,6 +2812,8 @@ else
test_group=show-client
run_m5_tests
test_group=''
run_m6_tests
test_group=''
fi
finish_tests