#!/usr/bin/env bash set -euo pipefail ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) # shellcheck source=tests/lib/assert.sh source "${ROOT_DIR}/tests/lib/assert.sh" status_of() { set +e "$@" >/dev/null 2>&1 local rc=$? set -e printf '%s\n' "${rc}" } wait_status() { local pid=$1 set +e wait "${pid}" WAIT_STATUS_RESULT=$? set -e } TMP_DIR=$(mktemp -d) trap 'rm -rf "${TMP_DIR}"' EXIT export IPF_STORAGE_DIR="${TMP_DIR}/storage" export IPF_STORAGE_DB="${IPF_STORAGE_DIR}/rules.db" export IPF_LOCK_FILE="${IPF_STORAGE_DIR}/.lock" export IPTABLES_MOCK_DIR="${TMP_DIR}/iptables-mock" export IPTABLES_MOCK_LOG="${TMP_DIR}/iptables.log" export IPTABLES_BIN="${ROOT_DIR}/tests/mocks/iptables" export IP6TABLES_BIN="${ROOT_DIR}/tests/mocks/ip6tables" export IPTABLES_SAVE_BIN="${ROOT_DIR}/tests/mocks/iptables-save" export IP6TABLES_SAVE_BIN="${ROOT_DIR}/tests/mocks/ip6tables-save" export NETFILTER_PERSISTENT_BIN="${ROOT_DIR}/tests/mocks/persist-mock.sh" export PERSIST_MOCK_LOG="${TMP_DIR}/persist.log" # shellcheck source=lib/common.sh source "${ROOT_DIR}/lib/common.sh" # shellcheck source=lib/storage.sh source "${ROOT_DIR}/lib/storage.sh" # shellcheck source=lib/persist.sh source "${ROOT_DIR}/lib/persist.sh" # shellcheck source=lib/iptables_ops.sh source "${ROOT_DIR}/lib/iptables_ops.sh" # shellcheck source=lib/rules_mgr.sh source "${ROOT_DIR}/lib/rules_mgr.sh" reset_mock_state() { rm -rf "${IPTABLES_MOCK_DIR}" "${IPF_STORAGE_DIR}" mkdir -p "${IPTABLES_MOCK_DIR}" "${IPF_STORAGE_DIR}" : >"${IPTABLES_MOCK_LOG}" : >"${PERSIST_MOCK_LOG}" unset IPTABLES_MOCK_FAIL_ON_N || true unset PERSIST_MOCK_FAIL || true unset PERSIST_MOCK_DELAY_SECS PERSIST_MOCK_ACTIVE_DIR PERSIST_MOCK_FAIL_ON_CONCURRENT || true storage_init } reset_mock_state uuid_v4=$(cmd_add_batch tcp 8080 127.0.0.1 80 4 'web service') line_v4=$(storage_get "${uuid_v4}") assert_eq '1' "$(storage_count)" 'cmd_add_batch should persist one rule' assert_eq '4' "$(grep -Ec '^iptables ' "${IPTABLES_MOCK_LOG}")" 'tcp/ipv4 add should emit four iptables commands' assert_contains "${line_v4}" "uuid=${uuid_v4}" 'stored line should contain generated uuid' assert_eq '1' "$(grep -Ec 'persist-mock\.sh save' "${PERSIST_MOCK_LOG}")" 'successful add should trigger persist_save' assert_contains "$(ipt_find_by_uuid "${uuid_v4}")" "MGMT:${uuid_v4}" 'ipt_find_by_uuid should locate saved mock rules' assert_contains "$(cat "${IPTABLES_MOCK_LOG}")" '--sport 80' 'add should include reply-direction FORWARD rule' assert_eq '✓' "$(rule_health_mark "${line_v4}")" 'healthy runtime rule should show ok marker' "${IPTABLES_BIN}" -D FORWARD \ -p tcp -s 127.0.0.1 --sport 80 \ -m conntrack --ctstate ESTABLISHED,RELATED \ -j ACCEPT \ -m comment --comment "MGMT:${uuid_v4}" assert_contains "$(ipt_find_by_uuid "${uuid_v4}")" "MGMT:${uuid_v4}" 'partial runtime loss should still leave uuid-tagged rules' assert_eq '!' "$(rule_health_mark "${line_v4}")" 'partial runtime loss should mark rule unhealthy' list_output=$(cmd_list 0) [[ ${list_output} =~ [[:space:]]![[:space:]] ]] || fail 'cmd_list should expose degraded health marker' reset_mock_state uuid_both=$(cmd_add_batch both 5353 '127.0.0.1,::1' 53 both 'dual stack dns') add_count=$(grep -Ec '^(iptables|ip6tables) ' "${IPTABLES_MOCK_LOG}") assert_eq '16' "${add_count}" 'both/both add should emit sixteen commands' cmd_delete_uuid "${uuid_both}" del_count=$(grep -Ec ' -D ' "${IPTABLES_MOCK_LOG}") assert_eq '16' "${del_count}" 'deleting both/both rule should emit sixteen delete commands' assert_eq '0' "$(storage_count)" 'cmd_delete_uuid should remove rule from storage' reset_mock_state cmd_add_batch tcp 9080 127.0.0.1 98 4 'desc-mark' >/dev/null mapfile -t list_lines < <(cmd_list 0) assert_eq "$(display_width "${list_lines[0]%%描述*}")" "$(display_width "${list_lines[1]%%desc-mark*}")" 'cmd_list should align description column by display width' reset_mock_state export IPTABLES_MOCK_FAIL_ON_N=2 assert_eq '1' "$(status_of cmd_add_batch tcp 9000 127.0.0.1 90 4 'rollback')" 'cmd_add_batch should fail when iptables mock injects an error' assert_eq '0' "$(storage_count)" 'failed add should not persist storage' assert_contains "$(cat "${IPTABLES_MOCK_LOG}")" ' -D ' 'failed add should trigger rollback deletes' assert_eq '0' "$(wc -l < "${PERSIST_MOCK_LOG}")" 'failed add before persistence should not call persist_save' reset_mock_state export PERSIST_MOCK_FAIL=1 assert_eq '1' "$(status_of cmd_add_batch tcp 9001 127.0.0.1 91 4 'persist rollback add')" 'cmd_add_batch should fail when persist_save fails' assert_eq '0' "$(storage_count)" 'persist_save failure on add should roll back storage' assert_eq '0' "$(wc -l < "${IPTABLES_MOCK_DIR}/state.v4")" 'persist_save failure on add should remove runtime rules' assert_contains "$(cat "${IPTABLES_MOCK_LOG}")" ' -D ' 'persist_save failure on add should trigger runtime rollback' assert_eq '1' "$(grep -Ec 'persist-mock\.sh save' "${PERSIST_MOCK_LOG}")" 'persist_save failure on add should still attempt one save' reset_mock_state uuid_delete_rollback=$(cmd_add_batch tcp 9100 127.0.0.1 92 4 'persist rollback delete') export PERSIST_MOCK_FAIL=1 assert_eq '1' "$(status_of cmd_delete_uuid "${uuid_delete_rollback}")" 'cmd_delete_uuid should fail when persist_save fails' assert_eq '1' "$(storage_count)" 'persist_save failure on delete should restore storage' assert_contains "$(storage_get "${uuid_delete_rollback}")" "uuid=${uuid_delete_rollback}" 'persist_save failure on delete should restore storage line' assert_contains "$(ipt_find_by_uuid "${uuid_delete_rollback}")" "MGMT:${uuid_delete_rollback}" 'persist_save failure on delete should restore runtime rules' assert_eq '2' "$(grep -Ec 'persist-mock\.sh save' "${PERSIST_MOCK_LOG}")" 'persist_save failure on delete should include add and delete save attempts' reset_mock_state uuid_storage_delete_rollback=$(cmd_add_batch tcp 9200 127.0.0.1 93 4 'storage rollback delete') storage_delete_impl=$(declare -f storage_delete_unlocked) storage_delete_unlocked() { return 1 } assert_eq '1' "$(status_of cmd_delete_uuid "${uuid_storage_delete_rollback}")" 'cmd_delete_uuid should fail when storage delete fails' assert_eq '1' "$(storage_count)" 'storage delete failure should keep storage record' assert_contains "$(storage_get "${uuid_storage_delete_rollback}")" "uuid=${uuid_storage_delete_rollback}" 'storage delete failure should keep original storage line' assert_contains "$(ipt_find_by_uuid "${uuid_storage_delete_rollback}")" "MGMT:${uuid_storage_delete_rollback}" 'storage delete failure should restore runtime rules' eval "${storage_delete_impl}" reset_mock_state export PERSIST_MOCK_DELAY_SECS=0.2 export PERSIST_MOCK_ACTIVE_DIR="${TMP_DIR}/persist-active" export PERSIST_MOCK_FAIL_ON_CONCURRENT=1 cmd_add_batch tcp 9300 127.0.0.1 94 4 'parallel one' >"${TMP_DIR}/parallel-1.out" 2>"${TMP_DIR}/parallel-1.err" & pid1=$! sleep 0.05 cmd_add_batch tcp 9301 127.0.0.1 95 4 'parallel two' >"${TMP_DIR}/parallel-2.out" 2>"${TMP_DIR}/parallel-2.err" & pid2=$! wait_status "${pid1}" assert_eq '0' "${WAIT_STATUS_RESULT}" 'first concurrent add should succeed' wait_status "${pid2}" assert_eq '0' "${WAIT_STATUS_RESULT}" 'second concurrent add should wait and succeed' assert_eq '2' "$(storage_count)" 'concurrent adds should persist both rules' assert_eq '2' "$(grep -Ec 'persist-mock\.sh save' "${PERSIST_MOCK_LOG}")" 'concurrent adds should serialize persist_save calls' unset PERSIST_MOCK_DELAY_SECS PERSIST_MOCK_ACTIVE_DIR PERSIST_MOCK_FAIL_ON_CONCURRENT pass 'test_rules_unit.sh'