From 87ee5283c868b71e3e114927a2510005e3e3975e65782bc8c11d4ea44093df76 Mon Sep 17 00:00:00 2001 From: ahdoawhfo Date: Fri, 17 Apr 2026 12:10:11 +0800 Subject: [PATCH] Fix FORWARD-drop reply handling --- README.md | 7 ++--- lib/iptables_ops.sh | 55 ++++++++++++++++++--------------------- plan_iptables_forward.md | 21 +++++++++++---- tests/test_integration.sh | 6 +++++ tests/test_interactive.sh | 4 +-- tests/test_rules_unit.sh | 14 +++++----- 6 files changed, 61 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3a914a2..831e5f6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ - 启动时自动做环境自检。 - 仅管理本工具创建的 `MGMT:` 规则。 - 支持 TCP / UDP / both,支持 IPv4 / IPv6 / both。 +- 在 `FORWARD` 默认策略为 `DROP` 时也会补齐回程放行规则。 - 支持 `iptables-persistent` / `netfilter-persistent save` 持久化。 - 所有写操作通过 `flock` 串行化,降低多实例并发竞争风险。 - 附带单元测试与真实 iptables 集成测试。 @@ -73,7 +74,7 @@ sudo ./iptables-forward.sh 规则列表中的状态列含义: -- `✓`:该条规则对应的 PREROUTING / POSTROUTING / FORWARD 运行态规则都存在 +- `✓`:该条规则对应的 PREROUTING / POSTROUTING / 双向 FORWARD 运行态规则都存在 - `!`:至少缺少一条运行态规则,或仅剩数据库记录 ### 批处理模式 @@ -155,8 +156,8 @@ tests/run_all.sh --skip-integration - `tests/test_common.sh`:输入校验 - `tests/test_storage.sh`:规则存储 - `tests/test_env_check.sh`:环境检查与修复 -- `tests/test_rules_unit.sh`:mock iptables 下的增删回滚与并发写保护 -- `tests/test_integration.sh`:真实 iptables 生命周期 + 命名空间端到端 IPv4/IPv6 TCP 转发 + save/reload 持久化回放测试 +- `tests/test_rules_unit.sh`:mock iptables 下的增删回滚、双向 FORWARD 规则与并发写保护 +- `tests/test_integration.sh`:真实 iptables 生命周期 + `FORWARD=DROP` 下的命名空间端到端 IPv4/IPv6 TCP 转发 + save/reload 持久化回放测试 `tests/test_integration.sh` 的执行策略: diff --git a/lib/iptables_ops.sh b/lib/iptables_ops.sh index 3576b79..1208a15 100644 --- a/lib/iptables_ops.sh +++ b/lib/iptables_ops.sh @@ -1,19 +1,15 @@ #!/usr/bin/env bash - if [[ -n ${IPF_IPTABLES_OPS_SH_LOADED:-} ]]; then return 0 fi IPF_IPTABLES_OPS_SH_LOADED=1 - : "${IPTABLES_BIN:=iptables}" : "${IP6TABLES_BIN:=ip6tables}" : "${IPTABLES_SAVE_BIN:=iptables-save}" : "${IP6TABLES_SAVE_BIN:=ip6tables-save}" - ipt_comment_tag() { printf 'MGMT:%s\n' "$1" } - ipt_protocols_for() { case ${1-} in tcp|udp) printf '%s\n' "$1" ;; @@ -21,7 +17,6 @@ ipt_protocols_for() { *) return 1 ;; esac } - ipt_families_for() { case ${1-} in 4|6) printf '%s\n' "$1" ;; @@ -29,7 +24,6 @@ ipt_families_for() { *) return 1 ;; esac } - ipt_bin_for_family() { case ${1-} in 4) printf '%s\n' "${IPTABLES_BIN}" ;; @@ -37,7 +31,6 @@ ipt_bin_for_family() { *) return 1 ;; esac } - ipt_save_bin_for_family() { case ${1-} in 4) printf '%s\n' "${IPTABLES_SAVE_BIN}" ;; @@ -45,7 +38,6 @@ ipt_save_bin_for_family() { *) return 1 ;; esac } - ipt_target_for_family() { local raw=${1-} local family=${2-} @@ -59,7 +51,6 @@ ipt_target_for_family() { fi printf '%s\n' "${raw}" } - ipt_to_destination() { local tip_raw=${1-} local tport=${2-} @@ -72,7 +63,6 @@ ipt_to_destination() { printf '%s:%s\n' "${tip}" "${tport}" fi } - _ipt_exec_rule() { local bin=$1 local table=$2 @@ -85,7 +75,6 @@ _ipt_exec_rule() { "${bin}" "-${action}" "${chain}" "$@" fi } - _ipt_check_rule() { local bin=$1 local table=$2 @@ -97,7 +86,6 @@ _ipt_check_rule() { "${bin}" -C "${chain}" "$@" >/dev/null 2>&1 fi } - _ipt_apply_expected_rules() { local action=$1 local uuid=$2 @@ -108,13 +96,11 @@ _ipt_apply_expected_rules() { local ipver=$7 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 if ! _ipt_exec_rule "${bin}" nat "${action}" PREROUTING \ @@ -123,14 +109,12 @@ _ipt_apply_expected_rules() { -m comment --comment "${comment}"; then return 1 fi - if ! _ipt_exec_rule "${bin}" nat "${action}" POSTROUTING \ -p "${protocol}" -d "${tip}" --dport "${tport}" \ -j MASQUERADE \ -m comment --comment "${comment}"; then return 1 fi - if ! _ipt_exec_rule "${bin}" '' "${action}" FORWARD \ -p "${protocol}" -d "${tip}" --dport "${tport}" \ -m conntrack --ctstate NEW,ESTABLISHED,RELATED \ @@ -138,10 +122,16 @@ _ipt_apply_expected_rules() { -m comment --comment "${comment}"; then return 1 fi + if ! _ipt_exec_rule "${bin}" '' "${action}" FORWARD \ + -p "${protocol}" -s "${tip}" --sport "${tport}" \ + -m conntrack --ctstate ESTABLISHED,RELATED \ + -j ACCEPT \ + -m comment --comment "${comment}"; then + return 1 + fi done < <(ipt_protocols_for "${proto}") done < <(ipt_families_for "${ipver}") } - ipt_apply_rule() { local uuid=$1 proto=$2 lport=$3 tip_raw=$4 tport=$5 ipver=$6 if ! _ipt_apply_expected_rules A "${uuid}" "${proto}" "${lport}" "${tip_raw}" "${tport}" "${ipver}"; then @@ -149,18 +139,15 @@ ipt_apply_rule() { return 1 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 \ @@ -176,10 +163,14 @@ ipt_rule_healthy() { -m conntrack --ctstate NEW,ESTABLISHED,RELATED \ -j ACCEPT \ -m comment --comment "${comment}" || return 1 + _ipt_check_rule "${bin}" '' FORWARD \ + -p "${protocol}" -s "${tip}" --sport "${tport}" \ + -m conntrack --ctstate 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 @@ -188,18 +179,28 @@ ipt_remove_rule() { comment=$(ipt_comment_tag "${uuid}") mapfile -t families < <(ipt_families_for "${ipver}") mapfile -t protocols < <(ipt_protocols_for "${proto}") - for ((family_index=${#families[@]} - 1; family_index >= 0; family_index--)); do family=${families[family_index]} [[ -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}") - for ((proto_index=${#protocols[@]} - 1; proto_index >= 0; proto_index--)); do protocol=${protocols[proto_index]} [[ -n ${protocol} ]] || continue - + if _ipt_check_rule "${bin}" '' FORWARD \ + -p "${protocol}" -s "${tip}" --sport "${tport}" \ + -m conntrack --ctstate ESTABLISHED,RELATED \ + -j ACCEPT \ + -m comment --comment "${comment}"; then + _ipt_exec_rule "${bin}" '' D FORWARD \ + -p "${protocol}" -s "${tip}" --sport "${tport}" \ + -m conntrack --ctstate ESTABLISHED,RELATED \ + -j ACCEPT \ + -m comment --comment "${comment}" || return 1 + else + missing=1 + fi if _ipt_check_rule "${bin}" '' FORWARD \ -p "${protocol}" -d "${tip}" --dport "${tport}" \ -m conntrack --ctstate NEW,ESTABLISHED,RELATED \ @@ -213,7 +214,6 @@ ipt_remove_rule() { else missing=1 fi - if _ipt_check_rule "${bin}" nat POSTROUTING \ -p "${protocol}" -d "${tip}" --dport "${tport}" \ -j MASQUERADE \ @@ -225,7 +225,6 @@ ipt_remove_rule() { else missing=1 fi - if _ipt_check_rule "${bin}" nat PREROUTING \ -p "${protocol}" --dport "${lport}" \ -j DNAT --to-destination "${destination}" \ @@ -239,17 +238,14 @@ ipt_remove_rule() { fi done done - if (( missing == 1 )); then return 0 fi } - ipt_find_by_uuid() { local uuid=${1-} local family save_bin found=1 line [[ -n ${uuid} ]] || return 1 - for family in 4 6; do save_bin=$(ipt_save_bin_for_family "${family}") if ! command_is_available "${save_bin}"; then @@ -261,6 +257,5 @@ ipt_find_by_uuid() { found=0 done < <("${save_bin}") done - return "${found}" } diff --git a/plan_iptables_forward.md b/plan_iptables_forward.md index 981945d..df1318f 100644 --- a/plan_iptables_forward.md +++ b/plan_iptables_forward.md @@ -60,7 +60,7 @@ ### 2.1 iptables 规则模板 (每条逻辑规则生成) -对协议 P (tcp/udp) × IP 族 F (iptables/ip6tables),生成 **3 条** iptables 规则: +对协议 P (tcp/udp) × IP 族 F (iptables/ip6tables),生成 **4 条** iptables 规则: ```text # 1) PREROUTING DNAT — 外部进入的包改目标 @@ -78,13 +78,19 @@ $CMD -A FORWARD -p

-d --dport \ -m conntrack --ctstate NEW,ESTABLISHED,RELATED \ -j ACCEPT \ -m comment --comment "MGMT:" + +# 4) FORWARD 回程 ACCEPT — 允许 DNAT/MASQUERADE 后的回包继续转发 +$CMD -A FORWARD -p

-s --sport \ + -m conntrack --ctstate ESTABLISHED,RELATED \ + -j ACCEPT \ + -m comment --comment "MGMT:" ``` 其中 `$CMD` 为 `iptables`(v4) 或 `ip6tables`(v6)。`` v6 时需加 `[]` (仅在 `--to-destination` 部分)。 一条"逻辑规则"的展开上限: -- `proto=both` × `ipv=both` = 2 × 2 × 3 条 = **12** 条 iptables 规则 -- 最常见 `proto=tcp` × `ipv=4` = 1 × 1 × 3 条 = **3** 条 +- `proto=both` × `ipv=both` = 2 × 2 × 4 条 = **16** 条 iptables 规则 +- 最常见 `proto=tcp` × `ipv=4` = 1 × 1 × 4 条 = **4** 条 所有规则共享同一个 UUID,通过注释绑定,删除时一并清除。 @@ -221,7 +227,7 @@ ipt_find_by_uuid() # args: uuid → 列出匹配的 (cmd, table, chain, rule) ### 4.5 `lib/rules_mgr.sh` — 业务编排层 职责: -- `cmd_list` — 从 storage 读出全部规则,格式化为表格,对每条规则用 `ipt_find_by_uuid` 校验是否仍在 iptables 中 (健康指示 [✓]/[!])。 +- `cmd_list` — 从 storage 读出全部规则,格式化为表格,对每条规则校验其 PREROUTING / POSTROUTING / 双向 FORWARD 规则是否仍在 iptables 中 (健康指示 [✓]/[!])。 - `cmd_add` — 交互表单 → 校验 → 生成 UUID → `ipt_apply_rule` → `storage_add` → `persist_save`。失败自动回滚已加 iptables 规则。 - `cmd_delete` — 调用 `cmd_list` 的交互版 → 用户选编号 → 二次确认 → `ipt_remove_rule` → `storage_delete` → `persist_save`。 @@ -356,7 +362,7 @@ persist_reload() { netfilter-persistent reload >/dev/null; } `tests/test_rules_unit.sh`: - 用 `iptables-mock.sh` 作为 `IPTABLES_BIN`,记录所有调用到 `/tmp/ipt_calls.log` - `cmd_add` (tcp/ipv4) 后: - - mock 日志包含 3 条命令 (PREROUTING/POSTROUTING/FORWARD) 且参数正确 + - mock 日志包含 4 条命令 (PREROUTING/POSTROUTING/FORWARD 请求/回程) 且参数正确 - storage 中新增 1 条,UUID 与 mock 注释一致 - `cmd_add` (both/both) 后:mock 日志包含 12 条命令 - `cmd_delete` 后:mock 日志包含对应 `-D` 命令,storage 中记录被移除 @@ -534,6 +540,11 @@ iptables -A FORWARD -p tcp -d 192.168.1.100 --dport 80 \ -m conntrack --ctstate NEW,ESTABLISHED,RELATED \ -j ACCEPT \ -m comment --comment "MGMT:a1b2c3d4" + +iptables -A FORWARD -p tcp -s 192.168.1.100 --sport 80 \ + -m conntrack --ctstate ESTABLISHED,RELATED \ + -j ACCEPT \ + -m comment --comment "MGMT:a1b2c3d4" ``` ### 10.2 卸载指引 diff --git a/tests/test_integration.sh b/tests/test_integration.sh index 472a6de..7cfb7d0 100644 --- a/tests/test_integration.sh +++ b/tests/test_integration.sh @@ -235,6 +235,10 @@ if [[ ${TRAFFIC_IPV6_READY} == 1 ]]; then fi traffic_setup_fixture +iptables -P FORWARD DROP +if [[ ${TRAFFIC_IPV6_READY} == 1 && -n ${BACKUP_IPV6_FORWARD} ]]; then + ip6tables -P FORWARD DROP +fi export IPF_STORAGE_DIR="${TMP_DIR}/storage" export IPF_STORAGE_DB="${IPF_STORAGE_DIR}/rules.db" @@ -262,6 +266,7 @@ assert_contains "$(iptables-save)" "MGMT:${uuid_v4}" 'IPv4 rule should appear in 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' +assert_contains "$(iptables -S FORWARD)" "--sport ${TRAFFIC_TARGET_PORT_V4}" 'IPv4 rule should include reply-direction FORWARD allow' assert_contains "$(traffic_request_ok 4 "${TRAFFIC_ROUTER_EDGE_IP4}" "${TRAFFIC_LISTEN_PORT_V4}" "${TRAFFIC_TARGET_IP4}" "${TRAFFIC_TARGET_PORT_V4}")" 'OK' 'managed IPv4 rule should forward actual TCP traffic' iptables -t nat -D PREROUTING \ @@ -297,6 +302,7 @@ if [[ ${TRAFFIC_IPV6_READY} == 1 && -n ${BACKUP_IPV6_FORWARD} ]]; then uuid_v6=$("${ROOT_DIR}/iptables-forward.sh" --batch add tcp "${TRAFFIC_LISTEN_PORT_V6}" "${TRAFFIC_TARGET_IP6}" "${TRAFFIC_TARGET_PORT_V6}" 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' + assert_contains "$(ip6tables -S FORWARD)" "--sport ${TRAFFIC_TARGET_PORT_V6}" 'IPv6 rule should include reply-direction FORWARD allow' assert_contains "$(traffic_request_ok 6 "${TRAFFIC_ROUTER_EDGE_IP6}" "${TRAFFIC_LISTEN_PORT_V6}" "${TRAFFIC_TARGET_IP6}" "${TRAFFIC_TARGET_PORT_V6}")" 'OK' 'managed IPv6 rule should forward actual TCP traffic' ipt_remove_rule "${uuid_v6}" tcp "${TRAFFIC_LISTEN_PORT_V6}" "${TRAFFIC_TARGET_IP6}" "${TRAFFIC_TARGET_PORT_V6}" 6 assert_status 1 "$(status_of grep -F "MGMT:${uuid_v6}" <(ip6tables-save))" 'manual runtime removal should clear managed IPv6 rule' diff --git a/tests/test_interactive.sh b/tests/test_interactive.sh index 8f74929..81b82f4 100644 --- a/tests/test_interactive.sh +++ b/tests/test_interactive.sh @@ -55,8 +55,8 @@ assert_contains "${output}" '已退出。' 'interactive flow should return clean assert_eq '0' "$(wc -l < "${IPF_STORAGE_DB}")" 'interactive add/delete flow should leave storage empty' assert_eq '0' "$(wc -l < "${IPTABLES_MOCK_DIR}/state.v4")" 'interactive add/delete flow should leave runtime mock state empty' -assert_eq '3' "$(grep -Ec ' -A ' "${IPTABLES_MOCK_LOG}")" 'interactive add flow should emit three IPv4 add commands' -assert_eq '3' "$(grep -Ec ' -D ' "${IPTABLES_MOCK_LOG}")" 'interactive delete should emit three delete commands' +assert_eq '4' "$(grep -Ec ' -A ' "${IPTABLES_MOCK_LOG}")" 'interactive add flow should emit four IPv4 add commands' +assert_eq '4' "$(grep -Ec ' -D ' "${IPTABLES_MOCK_LOG}")" 'interactive delete should emit four delete commands' assert_eq '3' "$(grep -Ec 'persist-mock\.sh save' "${PERSIST_MOCK_LOG}")" 'interactive add/menu-save/delete flow should persist three times' pass 'test_interactive.sh' diff --git a/tests/test_rules_unit.sh b/tests/test_rules_unit.sh index 16bf083..cfa9e5f 100644 --- a/tests/test_rules_unit.sh +++ b/tests/test_rules_unit.sh @@ -62,15 +62,17 @@ 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_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}" -t nat -D PREROUTING \ - -p tcp --dport 8080 \ - -j DNAT --to-destination 127.0.0.1:80 \ +"${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' @@ -80,11 +82,11 @@ list_output=$(cmd_list 0) 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 '12' "${add_count}" 'both/both add should emit twelve commands' +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 '12' "${del_count}" 'deleting both/both rule should emit twelve delete commands' +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