diff --git a/README.md b/README.md index 087153b..588741b 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ sudo ./iptables-forward.sh - 立即保存到磁盘 - 退出 +规则列表中的状态列含义: + +- `✓`:该条规则对应的 PREROUTING / POSTROUTING / FORWARD 运行态规则都存在 +- `!`:至少缺少一条运行态规则,或仅剩数据库记录 + ### 批处理模式 用于自动化测试或脚本调用: diff --git a/lib/iptables_ops.sh b/lib/iptables_ops.sh index 732d43a..3576b79 100644 --- a/lib/iptables_ops.sh +++ b/lib/iptables_ops.sh @@ -150,6 +150,36 @@ ipt_apply_rule() { fi } +ipt_rule_healthy() { + local uuid=$1 proto=$2 lport=$3 tip_raw=$4 tport=$5 ipver=$6 + local family protocol bin tip comment destination + comment=$(ipt_comment_tag "${uuid}") + + while IFS= read -r family; do + [[ -n ${family} ]] || continue + bin=$(ipt_bin_for_family "${family}") + tip=$(ipt_target_for_family "${tip_raw}" "${family}") + destination=$(ipt_to_destination "${tip_raw}" "${tport}" "${family}") + + while IFS= read -r protocol; do + [[ -n ${protocol} ]] || continue + _ipt_check_rule "${bin}" nat PREROUTING \ + -p "${protocol}" --dport "${lport}" \ + -j DNAT --to-destination "${destination}" \ + -m comment --comment "${comment}" || return 1 + _ipt_check_rule "${bin}" nat POSTROUTING \ + -p "${protocol}" -d "${tip}" --dport "${tport}" \ + -j MASQUERADE \ + -m comment --comment "${comment}" || return 1 + _ipt_check_rule "${bin}" '' FORWARD \ + -p "${protocol}" -d "${tip}" --dport "${tport}" \ + -m conntrack --ctstate NEW,ESTABLISHED,RELATED \ + -j ACCEPT \ + -m comment --comment "${comment}" || return 1 + done < <(ipt_protocols_for "${proto}") + done < <(ipt_families_for "${ipver}") +} + ipt_remove_rule() { local uuid=$1 proto=$2 lport=$3 tip_raw=$4 tport=$5 ipver=$6 local -a families protocols diff --git a/lib/rules_mgr.sh b/lib/rules_mgr.sh index 3220a4d..1484549 100644 --- a/lib/rules_mgr.sh +++ b/lib/rules_mgr.sh @@ -130,8 +130,16 @@ rules_load_lines() { } rule_health_mark() { - local uuid=${1-} - if ipt_find_by_uuid "${uuid}" >/dev/null 2>&1; then + local line=${1-} + local uuid proto lport tip tport ipver + uuid=$(rule_field "${line}" uuid) + proto=$(rule_field "${line}" proto) + lport=$(rule_field "${line}" lport) + tip=$(rule_field "${line}" tip) + tport=$(rule_field "${line}" tport) + ipver=$(rule_field "${line}" ipver) + + if ipt_rule_healthy "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}"; then printf '✓\n' else printf '!\n' @@ -156,7 +164,7 @@ render_rules_plain() { tport=$(rule_field "${line}" tport) ipver=$(rule_ipver_label "$(rule_field "${line}" ipver)") desc=$(rule_field "${line}" desc || true) - health=$(rule_health_mark "${uuid}") + health=$(rule_health_mark "${line}") printf '%-3s %-10s %-10s %-32s %-10s %-12s %-4s %s\n' \ "$((idx + 1))" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" "${health}" "${desc}" done @@ -166,7 +174,7 @@ cmd_list() { local pause=${1:-1} rules_load_lines render_rules_plain "${RULES_CACHE[@]}" - printf '[✓] 已在 iptables 中找到规则;[!] 仅存在于数据库中。\n' + printf '[✓] 运行态规则完整;[!] 至少缺少一条运行态规则或仅存在于数据库中。\n' if [[ ${pause} == 1 ]]; then pause_return fi diff --git a/tests/test_integration.sh b/tests/test_integration.sh index 8689b27..b06f3f9 100644 --- a/tests/test_integration.sh +++ b/tests/test_integration.sh @@ -85,10 +85,14 @@ export PERSIST_FIXTURE_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" 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' @@ -96,9 +100,19 @@ assert_contains "$("${ROOT_DIR}/iptables-forward.sh" --batch list)" "uuid=${uuid 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' +iptables -t nat -D PREROUTING \ + -p tcp --dport 65432 \ + -j DNAT --to-destination 127.0.0.1:22 \ + -m comment --comment "MGMT:${uuid_v4}" +list_output=$(cmd_list 0) +[[ ${list_output} =~ [[:space:]]![[:space:]] ]] || fail 'partial runtime loss should show degraded health in cmd_list' + +persist_reload +list_output=$(cmd_list 0) +[[ ${list_output} =~ [[:space:]]✓[[:space:]] ]] || fail 'persist_reload should restore healthy status in cmd_list' + 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' diff --git a/tests/test_rules_unit.sh b/tests/test_rules_unit.sh index e3755ef..16bf083 100644 --- a/tests/test_rules_unit.sh +++ b/tests/test_rules_unit.sh @@ -60,11 +60,22 @@ reset_mock_state() { 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 '3' "$(grep -Ec '^iptables ' "${IPTABLES_MOCK_LOG}")" 'tcp/ipv4 add should emit three iptables commands' -assert_contains "$(storage_get "${uuid_v4}")" "uuid=${uuid_v4}" 'stored line should contain generated uuid' +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_eq '✓' "$(rule_health_mark "${line_v4}")" 'healthy runtime rule should show ok marker' + +"${IPTABLES_BIN}" -t nat -D PREROUTING \ + -p tcp --dport 8080 \ + -j DNAT --to-destination 127.0.0.1:80 \ + -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')