feat(clients): add WireGuard state parsing and client listing
This commit is contained in:
+472
-2
@@ -11,7 +11,7 @@ source "${PROJECT_DIR}/wg-manager.sh"
|
||||
|
||||
case $# in
|
||||
0) ;;
|
||||
1) [[ $1 == config ]] || { printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64; } ;;
|
||||
1) case $1 in config|parser|status|list|list-redirected|list-deterministic) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
|
||||
*) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;;
|
||||
esac
|
||||
test_group=${1-}
|
||||
@@ -156,7 +156,7 @@ run_config_tests() {
|
||||
|
||||
begin_test 'known command validates config then reports not implemented'
|
||||
set +e
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" status 2>&1)
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" add 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_OPERATION" "$status" 'статус команды-заглушки'
|
||||
@@ -412,6 +412,450 @@ run_debug_tests() {
|
||||
assert_contains 'Отладка: visible' "$(<"${TEST_ROOT}/debug.stderr")" 'debug stderr'
|
||||
}
|
||||
|
||||
write_m2_config() {
|
||||
local output_file=$1 wireguard_file=$2
|
||||
write_valid_config "$output_file"
|
||||
replace_config_line "$output_file" 'WG_CONF=' "WG_CONF=\"${wireguard_file}\""
|
||||
}
|
||||
|
||||
write_alice_metadata() {
|
||||
mkdir -p "$METADATA_DIR"
|
||||
cat >"${METADATA_DIR}/alice.meta" <<EOF
|
||||
NAME=alice
|
||||
PUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
|
||||
IPV4=192.0.2.2
|
||||
CREATED_AT=1700000000
|
||||
CONFIG_PATH=${CLIENT_DIR}/alice.conf
|
||||
CLIENT_MODE=full-tunnel
|
||||
MANAGED=1
|
||||
EOF
|
||||
}
|
||||
|
||||
write_bob_metadata() {
|
||||
cat >"${METADATA_DIR}/bob.meta" <<EOF
|
||||
NAME=bob
|
||||
PUBLIC_KEY=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=
|
||||
IPV4=192.0.2.3
|
||||
CREATED_AT=1700000001
|
||||
CONFIG_PATH=${CLIENT_DIR}/bob.conf
|
||||
CLIENT_MODE=split-tunnel
|
||||
MANAGED=1
|
||||
EOF
|
||||
}
|
||||
|
||||
snapshot_model_inputs() {
|
||||
local root path mode
|
||||
for root in "$@"; do
|
||||
[[ -e "$root" ]] || continue
|
||||
find "$root" -print0
|
||||
done | LC_ALL=C sort -z | while IFS= read -r -d '' path; do
|
||||
mode=$(stat -c '%a' -- "$path")
|
||||
if [[ -d "$path" ]]; then
|
||||
printf 'D:%s:%s\n' "$path" "$mode"
|
||||
else
|
||||
printf 'F:%s:%s:' "$path" "$mode"
|
||||
sha256sum -- "$path"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
run_m2_tests() {
|
||||
local config_file="${TEST_ROOT}/m2.conf" fixture="${TEST_ROOT}/wg0.conf" output status
|
||||
local runtime_file="${TEST_ROOT}/runtime.dump"
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
write_m2_config "$config_file" "$fixture"
|
||||
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
|
||||
set_runtime_paths
|
||||
WG_CONF=$fixture
|
||||
mkdir -p "$METADATA_DIR"
|
||||
: >"$TEST_MOCK_LOG"
|
||||
unset WG_MOCK_DUMP_FILE WG_MOCK_INTERFACE
|
||||
|
||||
begin_test 'persistent WireGuard parser and trusted metadata identity'
|
||||
write_alice_metadata
|
||||
parse_wireguard_config "$fixture"
|
||||
assert_equal 1 "${#DISK_ALLOWED[@]}" 'один peer на диске'
|
||||
assert_equal 192.0.2.2/32 "${DISK_IPV4[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'адрес peer'
|
||||
parse_metadata
|
||||
assert_equal alice "${META_NAME[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'имя из метаданных'
|
||||
assert_equal 1 "${META_MANAGED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'доверенная метка managed'
|
||||
assert_equal "${CLIENT_DIR}/alice.conf" "${META_CONFIG[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'путь клиентского конфига'
|
||||
assert_equal 1700000000 "${META_CREATED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'время создания метаданных'
|
||||
|
||||
begin_test 'two managed peers match exact keys and addresses'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple-managed.conf" "$fixture"
|
||||
write_bob_metadata
|
||||
printf 'IPV6=2001:db8::2\n' >>"${METADATA_DIR}/alice.meta"
|
||||
load_client_model
|
||||
local managed_count=0
|
||||
for record_index in "${!CLIENT_KEYS[@]}"; do [[ ${CLIENT_ORIGINS[$record_index]} == managed ]] && ((managed_count += 1)); done
|
||||
assert_equal 2 "$managed_count" 'две управляемые записи'
|
||||
rm -- "${METADATA_DIR}/bob.meta"
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
|
||||
begin_test 'pre-peer marker and spaced mixed AllowedIPs are parsed'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/pre-peer-marker.conf" "$fixture"
|
||||
parse_wireguard_config "$fixture"
|
||||
assert_equal alice "${DISK_MARKER[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'маркер перед peer'
|
||||
assert_equal '192.0.2.2/32,2001:db8::2/128' "${DISK_ALLOWED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'нормализация AllowedIPs'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
|
||||
begin_test 'repeatable interface fields and duplicate peer singleton checks'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/repeatable-interface.conf" "$fixture"
|
||||
parse_wireguard_config "$fixture"
|
||||
replace_config_line "$fixture" 'FwMark =' 'FwMark = 0xca6c'
|
||||
parse_wireguard_config "$fixture"
|
||||
for bad_fwmark in 0x100000000 0xnope 4294967296; do
|
||||
replace_config_line "$fixture" 'FwMark =' "FwMark = ${bad_fwmark}"
|
||||
set +e
|
||||
parse_wireguard_config "$fixture" >/dev/null 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" "некорректный FwMark ${bad_fwmark}"
|
||||
done
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
printf 'Endpoint = vpn.example.invalid:51820\n' >>"$fixture"
|
||||
set +e
|
||||
output=$(parse_wireguard_config "$fixture" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'дублирующийся Endpoint'
|
||||
assert_contains 'Дублирующийся Endpoint' "$output" 'диагностика duplicate Endpoint'
|
||||
for duplicate_field in PresharedKey AllowedIPs PersistentKeepalive; do
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
case $duplicate_field in
|
||||
PresharedKey) printf 'PresharedKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\nPresharedKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n' >>"$fixture" ;;
|
||||
AllowedIPs) printf 'AllowedIPs = 192.0.2.2/32\nAllowedIPs = 192.0.2.2/32\n' >>"$fixture" ;;
|
||||
PersistentKeepalive) printf 'PersistentKeepalive = 25\nPersistentKeepalive = 25\n' >>"$fixture" ;;
|
||||
esac
|
||||
set +e
|
||||
output=$(parse_wireguard_config "$fixture" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" "дубликат ${duplicate_field}"
|
||||
done
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
printf 'Endpoint = vpn.example.invalid\n' >>"$fixture"
|
||||
set +e
|
||||
output=$(parse_wireguard_config "$fixture" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'Endpoint без порта'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
printf 'PresharedKey = (none)\n' >>"$fixture"
|
||||
set +e
|
||||
output=$(parse_wireguard_config "$fixture" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'persistent PSK token отвергнут'
|
||||
|
||||
begin_test 'section ordering and marker leakage are rejected safely'
|
||||
printf '# wg-manager-client: leaked\n[Interface]\nPrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\nListenPort = 51820\n[Peer]\nPublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nAllowedIPs = 192.0.2.2/32\n' >"${TEST_ROOT}/marker-leak.conf"
|
||||
parse_wireguard_config "${TEST_ROOT}/marker-leak.conf"
|
||||
assert_equal '' "${DISK_MARKER[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]:-}" 'маркер не протёк через Interface'
|
||||
for invalid_section in peer-before-interface empty-config repeated-interface; do
|
||||
case $invalid_section in
|
||||
peer-before-interface) printf '[Peer]\nPublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nAllowedIPs = 192.0.2.2/32\n' >"${TEST_ROOT}/${invalid_section}.conf" ;;
|
||||
empty-config) : >"${TEST_ROOT}/${invalid_section}.conf" ;;
|
||||
repeated-interface) printf '[Interface]\nPrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n[Interface]\nListenPort = 51820\n' >"${TEST_ROOT}/${invalid_section}.conf" ;;
|
||||
esac
|
||||
set +e
|
||||
parse_wireguard_config "${TEST_ROOT}/${invalid_section}.conf" >/dev/null 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" "секция ${invalid_section}"
|
||||
done
|
||||
|
||||
begin_test 'adjacent conventional markers attach to the next peer'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/markers-adjacent.conf" "$fixture"
|
||||
parse_wireguard_config "$fixture"
|
||||
assert_equal first "${DISK_LEGACY[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'первый marker'
|
||||
assert_equal second "${DISK_LEGACY[CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=]}" 'соседний marker'
|
||||
assert_equal third "${DISK_MARKER[DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=]}" 'соседний explicit marker'
|
||||
|
||||
begin_test 'metadata names remain restricted and cannot create paths'
|
||||
rm -- "${METADATA_DIR}/alice.meta"
|
||||
local unsafe_name metadata_output
|
||||
for unsafe_name in 'bad name' '*glob?' '-name' $'bad\nname'; do
|
||||
printf 'NAME=%s\nPUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nIPV4=192.0.2.2\nCREATED_AT=1700000000\nCONFIG_PATH=%s/bad.conf\nCLIENT_MODE=full-tunnel\nMANAGED=1\n' "$unsafe_name" "$CLIENT_DIR" >"${METADATA_DIR}/bad.meta"
|
||||
set +e
|
||||
metadata_output=$(parse_metadata 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" "unsafe metadata name ${unsafe_name@Q}"
|
||||
assert_equal '' "$(printf '%s' "$metadata_output" | grep -E 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=|PrivateKey|PresharedKey' || true)" 'metadata error without key material'
|
||||
assert_equal 0 "$( [[ ! -e "${CLIENT_DIR}/${unsafe_name}.conf" ]] && printf 0 || printf 1 )" 'unsafe metadata does not create path'
|
||||
rm -- "${METADATA_DIR}/bad.meta"
|
||||
done
|
||||
write_alice_metadata
|
||||
|
||||
begin_test 'duplicate metadata identity is rejected'
|
||||
cp -- "${METADATA_DIR}/alice.meta" "${METADATA_DIR}/duplicate.meta"
|
||||
set +e
|
||||
output=$(parse_metadata 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'дубликат метаданных'
|
||||
assert_contains 'Дублирующийся' "$output" 'диагностика дубликата метаданных'
|
||||
rm -- "${METADATA_DIR}/duplicate.meta"
|
||||
|
||||
begin_test 'empty peer list remains a valid disk model'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/no-peers.conf" "$fixture"
|
||||
rm -- "${METADATA_DIR}/alice.meta"
|
||||
unset WG_MOCK_DUMP_FILE
|
||||
load_client_model
|
||||
assert_equal 0 "${#CLIENT_KEYS[@]}" 'нет peers'
|
||||
write_alice_metadata
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
|
||||
begin_test 'metadata address mismatch never establishes managed identity'
|
||||
replace_config_line "${METADATA_DIR}/alice.meta" 'IPV4=' 'IPV4=192.0.2.99'
|
||||
load_client_model
|
||||
assert_equal legacy "${CLIENT_ORIGINS[0]}" 'mismatch origin'
|
||||
write_alice_metadata
|
||||
|
||||
begin_test 'legacy and unmanaged origins remain distinct from managed identity'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple.conf" "$fixture"
|
||||
rm -- "${METADATA_DIR}/alice.meta"
|
||||
unset WG_MOCK_DUMP_FILE
|
||||
load_client_model
|
||||
local record_index
|
||||
for record_index in "${!CLIENT_KEYS[@]}"; do
|
||||
case ${CLIENT_KEYS[$record_index]} in
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=) assert_equal legacy "${CLIENT_ORIGINS[$record_index]}" 'peer с явным marker без метаданных'; assert_equal unmanaged "${CLIENT_STATES[$record_index]}" 'marker без доверенной метадаты' ;;
|
||||
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=) assert_equal legacy "${CLIENT_ORIGINS[$record_index]}" 'legacy peer'; assert_equal unmanaged "${CLIENT_STATES[$record_index]}" 'legacy state' ;;
|
||||
DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=) assert_equal unmanaged "${CLIENT_ORIGINS[$record_index]}" 'unmanaged peer' ;;
|
||||
esac
|
||||
done
|
||||
|
||||
begin_test 'runtime parser and exact wg dump command'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
write_alice_metadata
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0
|
||||
: >"$TEST_MOCK_LOG"
|
||||
load_client_model
|
||||
assert_equal 1 "$RUNTIME_AVAILABLE" 'runtime доступен'
|
||||
assert_equal 51820 "$RUNTIME_LISTEN_PORT" 'порт runtime'
|
||||
assert_equal active "${CLIENT_STATES[0]}" 'активный peer на границе threshold'
|
||||
local runtime_log
|
||||
runtime_log=$(<"$TEST_MOCK_LOG")
|
||||
runtime_log+=$'\n'
|
||||
assert_equal $'wg show wg0 dump\n' "$runtime_log" 'точный argv wg'
|
||||
assert_equal '' "$(printf '%s' "$runtime_log" | grep -E 'systemctl|ip|gum' || true)" 'runtime без лишних команд'
|
||||
begin_test 'runtime unavailable tokens normalize safely'
|
||||
parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\t(none)\t(none)\t0\t0\t0\t0'
|
||||
assert_equal - "${RUNTIME_ENDPOINT[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'endpoint none'
|
||||
assert_equal - "${RUNTIME_ALLOWED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'allowed none'
|
||||
parse_runtime_dump $'(none)\t(none)\t51820\t0'
|
||||
assert_equal 51820 "$RUNTIME_LISTEN_PORT" 'interface none tokens'
|
||||
set +e
|
||||
output=$(parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid\t192.0.2.2/32\t0\t0\t0\t0' 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'runtime endpoint without port'
|
||||
parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\t[2001:db8::1]:51820\t192.0.2.2/32\t0\t0\t0\t0'
|
||||
assert_equal '[2001:db8::1]:51820' "${RUNTIME_ENDPOINT[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'runtime IPv6 endpoint'
|
||||
|
||||
begin_test 'unavailable runtime command degrades safely'
|
||||
local saved_path=$PATH
|
||||
local safe_runtime_path="${TEST_ROOT}/no-wg"
|
||||
mkdir -p "$safe_runtime_path"
|
||||
ln -s -- "$(type -P sort)" "${safe_runtime_path}/sort"
|
||||
ln -s -- "$(type -P paste)" "${safe_runtime_path}/paste"
|
||||
PATH=$safe_runtime_path
|
||||
if command -v wg >/dev/null 2>&1; then
|
||||
record_failure 'контролируемый PATH неожиданно содержит wg'
|
||||
fi
|
||||
unset WG_MOCK_DUMP_FILE
|
||||
output=$(/bin/bash "${PROJECT_DIR}/wg-manager.sh" status)
|
||||
PATH=$saved_path
|
||||
assert_contains 'недоступен' "$output" 'wg отсутствует'
|
||||
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0
|
||||
|
||||
begin_test 'active threshold, inactive, and never states are deterministic'
|
||||
replace_config_line "$runtime_file" 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' $'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tvpn.example.invalid:51820\t192.0.2.2/32\t1699999919\t100\t200\t25'
|
||||
load_client_model
|
||||
assert_equal inactive "${CLIENT_STATES[0]}" 'peer just beyond threshold'
|
||||
replace_config_line "$runtime_file" 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' $'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t100\t200\t25'
|
||||
load_client_model
|
||||
assert_equal never "${CLIENT_STATES[0]}" 'peer without handshake'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
|
||||
begin_test 'runtime-only peer is retained as unmanaged'
|
||||
printf '%s\t%s\t-\t192.0.2.9/32\t1700000100\t1\t2\t0\n' 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=' '(none)' >>"$runtime_file"
|
||||
load_client_model
|
||||
local runtime_index runtime_origin=''
|
||||
assert_contains EEEEEEEE "$(printf '%s\n' "${CLIENT_KEYS[@]}")" 'runtime-only key parsed'
|
||||
for runtime_index in "${!CLIENT_KEYS[@]}"; do
|
||||
[[ ${CLIENT_KEYS[$runtime_index]} == EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE= ]] && runtime_origin=${CLIENT_ORIGINS[$runtime_index]}
|
||||
done
|
||||
assert_equal unmanaged "$runtime_origin" 'runtime-only origin'
|
||||
|
||||
begin_test 'malformed runtime dump fails without exposing fields'
|
||||
printf 'malformed runtime line\n' >"$runtime_file"
|
||||
set +e
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'malformed runtime status'
|
||||
assert_contains 'wg dump' "$output" 'malformed runtime diagnostic'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
|
||||
begin_test 'status is useful without gum or systemd'
|
||||
local model_before model_after
|
||||
model_before=$(snapshot_model_inputs "$config_file" "$fixture" "$METADATA_DIR" "$runtime_file" "$BACKUP_DIR" "$LOG_DIR" "$ARCHIVE_DIR" "$LOCK_FILE")
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" status 2>&1)
|
||||
assert_contains 'Интерфейс: wg0' "$output" 'status interface'
|
||||
assert_contains 'Управляемые: 1' "$output" 'status managed count'
|
||||
assert_contains 'Активные: 1' "$output" 'status active count'
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list)
|
||||
model_after=$(snapshot_model_inputs "$config_file" "$fixture" "$METADATA_DIR" "$runtime_file" "$BACKUP_DIR" "$LOG_DIR" "$ARCHIVE_DIR" "$LOCK_FILE")
|
||||
assert_equal "$model_before" "$model_after" 'status/list не изменяют состояние'
|
||||
|
||||
begin_test 'invalid deterministic clock fails without secrets'
|
||||
WG_MANAGER_NOW=invalid
|
||||
set +e
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" status 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'некорректные часы'
|
||||
assert_equal '' "$(printf '%s' "$output" | grep -E 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' || true)" 'ошибка часов без ключей'
|
||||
WG_MANAGER_NOW=1700000100
|
||||
|
||||
begin_test 'list is deterministic and secret-free'
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list)
|
||||
assert_contains $'NAME\tADDRESSES\tPUBLIC_KEY' "$output" 'list header'
|
||||
assert_contains $'alice\t192.0.2.2/32\tBBBBBBBB' "$output" 'list managed row'
|
||||
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' <<<"$output" || true)" 'list не показывает секретные поля'
|
||||
|
||||
begin_test 'redirected list remains plain text'
|
||||
"${PROJECT_DIR}/wg-manager.sh" list >"${TEST_ROOT}/list.out"
|
||||
assert_contains $'alice\t192.0.2.2/32' "$(<"${TEST_ROOT}/list.out")" 'redirected list row'
|
||||
assert_equal '' "$(LC_ALL=C grep -nP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]' "${TEST_ROOT}/list.out" || true)" 'redirected list without controls'
|
||||
|
||||
begin_test 'list ordering is independent of peer input order'
|
||||
rm -- "${METADATA_DIR}/alice.meta"
|
||||
unset WG_MOCK_DUMP_FILE
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple.conf" "$fixture"
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list)
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple-reversed.conf" "$fixture"
|
||||
assert_equal "$output" "$("${PROJECT_DIR}/wg-manager.sh" list)" 'стабильная сортировка list'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
write_alice_metadata
|
||||
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0
|
||||
|
||||
begin_test 'stopped runtime degrades to disk-only output'
|
||||
unset WG_MOCK_DUMP_FILE
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" status)
|
||||
assert_contains 'недоступен' "$output" 'runtime unavailable'
|
||||
assert_contains 'Peers: 1' "$output" 'disk peer count'
|
||||
|
||||
begin_test 'malformed persistent config fails safely'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/malformed.conf" "$fixture"
|
||||
set +e
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'malformed config status'
|
||||
assert_contains 'PublicKey' "$output" 'malformed config diagnostic'
|
||||
|
||||
begin_test 'duplicate AllowedIPs are rejected'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/duplicate-allowed.conf" "$fixture"
|
||||
set +e
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'duplicate AllowedIPs status'
|
||||
assert_contains 'AllowedIPs' "$output" 'duplicate AllowedIPs diagnostic'
|
||||
|
||||
begin_test 'duplicate public keys and overlapping networks are rejected'
|
||||
for invalid_fixture in duplicate-public overlap overlap-v6; do
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/${invalid_fixture}.conf" "$fixture"
|
||||
set +e
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" "${invalid_fixture} status"
|
||||
if [[ $invalid_fixture == duplicate-public ]]; then assert_contains 'PublicKey' "$output" "${invalid_fixture} diagnostic"; else assert_contains 'AllowedIPs' "$output" "${invalid_fixture} diagnostic"; fi
|
||||
done
|
||||
}
|
||||
|
||||
run_m2_focused() {
|
||||
local group=$1 config_file="${TEST_ROOT}/focused-m2.conf" fixture="${TEST_ROOT}/wg0.conf" runtime_file="${TEST_ROOT}/runtime.dump" output reversed_output status
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
|
||||
write_m2_config "$config_file" "$fixture"
|
||||
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
|
||||
set_runtime_paths
|
||||
WG_CONF=$fixture
|
||||
mkdir -p "$METADATA_DIR"
|
||||
load_config
|
||||
write_alice_metadata
|
||||
case $group in
|
||||
parser)
|
||||
begin_test 'focused persistent and runtime parser'
|
||||
parse_wireguard_config "$fixture"
|
||||
parse_metadata
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/repeatable-interface.conf" "$fixture"
|
||||
replace_config_line "$fixture" 'FwMark =' 'FwMark = 0xca6c'
|
||||
parse_wireguard_config "$fixture"
|
||||
assert_equal 1 "${#DISK_ALLOWED[@]}" 'hex FwMark не ломает parser'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/markers-adjacent.conf" "$fixture"
|
||||
parse_wireguard_config "$fixture"
|
||||
assert_equal second "${DISK_LEGACY[CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=]}" 'focused marker scoping'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
parse_runtime_dump "$(<"$runtime_file")"
|
||||
assert_equal 1 "$RUNTIME_AVAILABLE" 'runtime parser доступен'
|
||||
assert_equal 51820 "$RUNTIME_LISTEN_PORT" 'порт интерфейса runtime'
|
||||
assert_equal 1700000100 "${RUNTIME_HANDSHAKE[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'handshake peer'
|
||||
assert_equal 100 "${RUNTIME_RX[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'rx peer'
|
||||
assert_equal 200 "${RUNTIME_TX[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'tx peer'
|
||||
assert_equal '192.0.2.2/32' "${RUNTIME_ALLOWED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'allowed ips peer'
|
||||
parse_runtime_dump $'(none)\t(none)\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\t[2001:db8::1]:51820\t192.0.2.2/32\t0\t0\t0\t0'
|
||||
assert_equal '[2001:db8::1]:51820' "${RUNTIME_ENDPOINT[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'focused runtime endpoint'
|
||||
set +e
|
||||
output=$(parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\tmalformed\t(none)\t(none)\t0\t0\t0\t0' 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_equal "$EXIT_VALIDATION" "$status" 'malformed PSK отвергнут'
|
||||
assert_contains 'ключ' "$output" 'диагностика malformed PSK'
|
||||
;;
|
||||
status)
|
||||
begin_test 'focused status output'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" status)
|
||||
assert_contains 'Интерфейс: wg0' "$output" 'status interface'
|
||||
assert_contains 'Управляемые: 1' "$output" 'status managed'
|
||||
;;
|
||||
list)
|
||||
begin_test 'focused list output'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list)
|
||||
assert_contains $'alice\t192.0.2.2/32' "$output" 'list row'
|
||||
assert_equal '' "$(printf '%s' "$output" | grep -E 'PrivateKey|PresharedKey' || true)" 'list secret-free'
|
||||
;;
|
||||
list-redirected)
|
||||
begin_test 'focused redirected list'
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file"
|
||||
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0
|
||||
"${PROJECT_DIR}/wg-manager.sh" list >"${TEST_ROOT}/focused-list.out" 2>"${TEST_ROOT}/focused-list.err"
|
||||
assert_contains $'alice\t192.0.2.2/32' "$(<"${TEST_ROOT}/focused-list.out")" 'redirected row'
|
||||
assert_equal '' "$(<"${TEST_ROOT}/focused-list.err")" 'redirected stderr empty'
|
||||
;;
|
||||
list-deterministic)
|
||||
begin_test 'focused deterministic list ordering'
|
||||
unset WG_MOCK_DUMP_FILE
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple.conf" "$fixture"
|
||||
output=$("${PROJECT_DIR}/wg-manager.sh" list)
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple-reversed.conf" "$fixture"
|
||||
reversed_output=$("${PROJECT_DIR}/wg-manager.sh" list)
|
||||
assert_equal "$output" "$reversed_output" 'порядок list стабилен'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -z $test_group ]]; then
|
||||
begin_test 'sourcing entry point does not execute main'
|
||||
source_output=''
|
||||
@@ -504,16 +948,42 @@ scan_test_sources_for_host_path() {
|
||||
scan_path "$TESTS_DIR"
|
||||
}
|
||||
|
||||
scan_sources_for_absolute_external_commands() {
|
||||
local command_name candidate path line
|
||||
for command_name in wg wg-quick ip systemctl nft iptables journalctl sysctl; do
|
||||
candidate='/bin'
|
||||
candidate+="/${command_name}"
|
||||
for path in "$PROJECT_DIR/wg-manager.sh" "$TESTS_DIR"/*.sh "$TESTS_DIR"/mocks/bin/*; do
|
||||
[[ -f "$path" ]] || continue
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" == "$candidate" || "$line" == *" $candidate"* || "$line" == *"\"$candidate"* || "$line" == *"'$candidate"* || "$line" == *"=$candidate"* ]]; then record_failure "абсолютный внешний вызов в ${path}: ${candidate}"; break; fi
|
||||
done <"$path"
|
||||
done
|
||||
candidate='/usr'
|
||||
candidate+="/bin/${command_name}"
|
||||
for path in "$PROJECT_DIR/wg-manager.sh" "$TESTS_DIR"/*.sh "$TESTS_DIR"/mocks/bin/*; do
|
||||
[[ -f "$path" ]] || continue
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" == "$candidate" || "$line" == *" $candidate"* || "$line" == *"\"$candidate"* || "$line" == *"'$candidate"* || "$line" == *"=$candidate"* ]]; then record_failure "абсолютный внешний вызов в ${path}: ${candidate}"; break; fi
|
||||
done <"$path"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
scan_test_sources_for_host_path
|
||||
scan_sources_for_absolute_external_commands
|
||||
fi
|
||||
|
||||
if [[ $test_group == config ]]; then
|
||||
run_config_tests
|
||||
elif [[ $test_group == parser || $test_group == status || $test_group == list || $test_group == list-redirected || $test_group == list-deterministic ]]; then
|
||||
run_m2_focused "$test_group"
|
||||
else
|
||||
run_config_tests
|
||||
run_gum_tests
|
||||
run_dependency_tests
|
||||
run_debug_tests
|
||||
run_m2_tests
|
||||
fi
|
||||
|
||||
finish_tests
|
||||
|
||||
Reference in New Issue
Block a user