diff --git a/README.md b/README.md index 7bf0a3d..3bb8a33 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,14 @@ tests/run_all.sh --skip-integration - `tests/test_storage.sh`:规则存储 - `tests/test_env_check.sh`:环境检查与修复 - `tests/test_rules_unit.sh`:mock iptables 下的增删回滚 -- `tests/test_integration.sh`:真实 iptables 生命周期测试 +- `tests/test_integration.sh`:真实 iptables 生命周期 + save/reload 持久化回放测试 `tests/test_integration.sh` 的执行策略: - root 环境下直接执行 - 非 root 但支持 `unshare -Urn` 时,会进入隔离网络命名空间执行 - 两者都不满足时会输出 `SKIP` +- 持久化验证不会直接改写宿主 `/etc/iptables`,而是通过临时 `netfilter-persistent` 包装器把 `save/reload` 落到测试目录中的 `rules.v4` / `rules.v6` ## 手工验收建议 diff --git a/tests/mocks/persist-fixture.sh b/tests/mocks/persist-fixture.sh new file mode 100755 index 0000000..e4620b2 --- /dev/null +++ b/tests/mocks/persist-fixture.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${PERSIST_RULES_V4:?PERSIST_RULES_V4 is required}" +: "${PERSIST_RULES_V6:=}" +: "${IPTABLES_SAVE_BIN:=iptables-save}" +: "${IP6TABLES_SAVE_BIN:=ip6tables-save}" +: "${IPTABLES_RESTORE_BIN:=iptables-restore}" +: "${IP6TABLES_RESTORE_BIN:=ip6tables-restore}" +: "${PERSIST_FIXTURE_LOG:=}" + +log_action() { + [[ -n ${PERSIST_FIXTURE_LOG} ]] || return 0 + printf '%s %s\n' "$(basename -- "$0")" "$*" >>"${PERSIST_FIXTURE_LOG}" +} + +save_family_rules() { + local save_bin=$1 + local output_file=$2 + [[ -n ${output_file} ]] || return 0 + mkdir -p "$(dirname -- "${output_file}")" + if command -v "${save_bin}" >/dev/null 2>&1; then + "${save_bin}" >"${output_file}" + fi +} + +reload_family_rules() { + local restore_bin=$1 + local input_file=$2 + [[ -n ${input_file} && -f ${input_file} ]] || return 0 + if command -v "${restore_bin}" >/dev/null 2>&1; then + "${restore_bin}" <"${input_file}" + fi +} + +case ${1-} in + save) + log_action "$@" + save_family_rules "${IPTABLES_SAVE_BIN}" "${PERSIST_RULES_V4}" + save_family_rules "${IP6TABLES_SAVE_BIN}" "${PERSIST_RULES_V6}" + ;; + reload) + log_action "$@" + reload_family_rules "${IPTABLES_RESTORE_BIN}" "${PERSIST_RULES_V4}" + reload_family_rules "${IP6TABLES_RESTORE_BIN}" "${PERSIST_RULES_V6}" + ;; + *) + printf 'usage: %s \n' "$(basename -- "$0")" >&2 + exit 1 + ;; +esac diff --git a/tests/test_integration.sh b/tests/test_integration.sh index 7d50656..8689b27 100644 --- a/tests/test_integration.sh +++ b/tests/test_integration.sh @@ -41,17 +41,29 @@ if ! command -v iptables >/dev/null 2>&1; then exit 0 fi +if ! command -v iptables-save >/dev/null 2>&1 || ! command -v iptables-restore >/dev/null 2>&1; then + printf 'SKIP: iptables-save/iptables-restore 不可用,跳过集成测试。\n' + exit 0 +fi + TMP_DIR=$(mktemp -d) BACKUP_V4="${TMP_DIR}/iptables.v4.bak" BACKUP_V6="${TMP_DIR}/iptables.v6.bak" +PERSIST_RULES_V4="${TMP_DIR}/rules.v4" +PERSIST_RULES_V6="${TMP_DIR}/rules.v6" +PERSIST_FIXTURE_LOG="${TMP_DIR}/persist.log" -cleanup() { +restore_runtime_from_backup() { if [[ -f ${BACKUP_V4} ]]; then iptables-restore <"${BACKUP_V4}" >/dev/null 2>&1 || true fi - if [[ -f ${BACKUP_V6} ]]; then + if [[ -f ${BACKUP_V6} ]] && command -v ip6tables-restore >/dev/null 2>&1; then ip6tables-restore <"${BACKUP_V6}" >/dev/null 2>&1 || true fi +} + +cleanup() { + restore_runtime_from_backup rm -rf "${TMP_DIR}" } trap cleanup EXIT @@ -65,24 +77,50 @@ 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 IPF_SKIP_ENV_CHECK=1 -export IPF_SKIP_PERSIST=1 export IPF_FORCE_PLAIN_UI=1 +export NETFILTER_PERSISTENT_BIN="${ROOT_DIR}/tests/mocks/persist-fixture.sh" +export PERSIST_RULES_V4 +export PERSIST_RULES_V6 +export PERSIST_FIXTURE_LOG + +# shellcheck source=lib/common.sh +source "${ROOT_DIR}/lib/common.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" uuid_v4=$("${ROOT_DIR}/iptables-forward.sh" --batch add tcp 65432 127.0.0.1 22 4 'integration-v4') assert_contains "$(iptables-save)" "MGMT:${uuid_v4}" 'IPv4 rule should appear in iptables-save output' assert_contains "$("${ROOT_DIR}/iptables-forward.sh" --batch list)" "uuid=${uuid_v4}" 'batch list should include managed rule' +assert_file_contains "${PERSIST_RULES_V4}" "MGMT:${uuid_v4}" 'persist save should write IPv4 rules snapshot' +assert_file_contains "${PERSIST_FIXTURE_LOG}" 'persist-fixture.sh save' 'adding a rule should call persist save' + +ipt_remove_rule "${uuid_v4}" tcp 65432 127.0.0.1 22 4 +assert_status 1 "$(status_of grep -F "MGMT:${uuid_v4}" <(iptables-save))" 'manual runtime removal should clear managed IPv4 rule' + +persist_reload +assert_file_contains "${PERSIST_FIXTURE_LOG}" 'persist-fixture.sh reload' 'persist_reload should call persistence wrapper' +assert_contains "$(iptables-save)" "MGMT:${uuid_v4}" 'persist_reload should restore IPv4 rule from snapshot' iptables -A FORWARD -p tcp --dport 65000 -j ACCEPT "${ROOT_DIR}/iptables-forward.sh" --batch delete "${uuid_v4}" assert_status 1 "$(status_of grep -F "MGMT:${uuid_v4}" <(iptables-save))" 'deleted IPv4 rule should disappear from iptables-save' assert_status 0 "$(status_of grep -F -- '--dport 65000 -j ACCEPT' <(iptables-save))" 'unmanaged rule should remain after deleting managed rule' +assert_not_contains "$(cat "${PERSIST_RULES_V4}")" "MGMT:${uuid_v4}" 'deleting IPv4 rule should refresh persisted snapshot' -if command -v ip6tables >/dev/null 2>&1; then +if command -v ip6tables >/dev/null 2>&1 && command -v ip6tables-save >/dev/null 2>&1 && command -v ip6tables-restore >/dev/null 2>&1; then uuid_v6=$("${ROOT_DIR}/iptables-forward.sh" --batch add tcp 65433 ::1 22 6 'integration-v6') assert_contains "$(ip6tables-save)" "MGMT:${uuid_v6}" 'IPv6 rule should appear in ip6tables-save output' + assert_file_contains "${PERSIST_RULES_V6}" "MGMT:${uuid_v6}" 'persist save should write IPv6 rules snapshot' + ipt_remove_rule "${uuid_v6}" tcp 65433 ::1 22 6 + assert_status 1 "$(status_of grep -F "MGMT:${uuid_v6}" <(ip6tables-save))" 'manual runtime removal should clear managed IPv6 rule' + persist_reload + assert_contains "$(ip6tables-save)" "MGMT:${uuid_v6}" 'persist_reload should restore IPv6 rule from snapshot' "${ROOT_DIR}/iptables-forward.sh" --batch delete "${uuid_v6}" assert_status 1 "$(status_of grep -F "MGMT:${uuid_v6}" <(ip6tables-save))" 'deleted IPv6 rule should disappear from ip6tables-save' + assert_not_contains "$(cat "${PERSIST_RULES_V6}")" "MGMT:${uuid_v6}" 'deleting IPv6 rule should refresh persisted snapshot' fi pass 'test_integration.sh'