From bd6dc0279ee96ddcbe1f139436247dc78f8dc85faf8c17a7fd23b3815b8bc55d Mon Sep 17 00:00:00 2001 From: ahdoawhfo Date: Fri, 17 Apr 2026 11:51:59 +0800 Subject: [PATCH] Add end-to-end forwarding integration test --- README.md | 3 +- tests/test_integration.sh | 213 ++++++++++++++++++++++++++++++++++---- 2 files changed, 195 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 588741b..5a02d13 100644 --- a/README.md +++ b/README.md @@ -153,13 +153,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 生命周期 + save/reload 持久化回放测试 +- `tests/test_integration.sh`:真实 iptables 生命周期 + 命名空间端到端 IPv4/IPv6 TCP 转发 + save/reload 持久化回放测试 `tests/test_integration.sh` 的执行策略: - root 环境下直接执行 - 非 root 但支持 `unshare -Urn` 时,会进入隔离网络命名空间执行 - 两者都不满足时会输出 `SKIP` +- 流量验证会额外创建 client / router / backend 三段临时 veth + netns,并用 Python TCP server 验证 IPv4/IPv6 add / reload / delete 对真实转发流量的影响 - 持久化验证不会直接改写宿主 `/etc/iptables`,而是通过临时 `netfilter-persistent` 包装器把 `save/reload` 落到测试目录中的 `rules.v4` / `rules.v6` ## 手工验收建议 diff --git a/tests/test_integration.sh b/tests/test_integration.sh index b06f3f9..472a6de 100644 --- a/tests/test_integration.sh +++ b/tests/test_integration.sh @@ -32,26 +32,43 @@ maybe_enter_namespace() { maybe_enter_namespace -if command -v ip >/dev/null 2>&1; then - ip link set lo up >/dev/null 2>&1 || true -fi +for bin in ip iptables iptables-save iptables-restore nsenter python3; do + if ! command -v "${bin}" >/dev/null 2>&1; then + printf 'SKIP: %s 不可用,跳过集成测试。\n' "${bin}" + exit 0 + fi +done -if ! command -v iptables >/dev/null 2>&1; then - printf 'SKIP: iptables 不可用,跳过集成测试。\n' - 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 +ip link set lo up >/dev/null 2>&1 || true TMP_DIR=$(mktemp -d) BACKUP_V4="${TMP_DIR}/iptables.v4.bak" BACKUP_V6="${TMP_DIR}/iptables.v6.bak" +BACKUP_IPV4_FORWARD=$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || printf '') +BACKUP_IPV6_FORWARD=$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || printf '') PERSIST_RULES_V4="${TMP_DIR}/rules.v4" PERSIST_RULES_V6="${TMP_DIR}/rules.v6" PERSIST_FIXTURE_LOG="${TMP_DIR}/persist.log" +TRAFFIC_LISTEN_PORT_V4=65432 +TRAFFIC_TARGET_PORT_V4=18080 +TRAFFIC_ROUTER_EDGE_IP4=10.23.0.1 +TRAFFIC_CLIENT_IP4=10.23.0.2 +TRAFFIC_ROUTER_BACKEND_IP4=10.24.0.1 +TRAFFIC_TARGET_IP4=10.24.0.2 +TRAFFIC_LISTEN_PORT_V6=65433 +TRAFFIC_TARGET_PORT_V6=18081 +TRAFFIC_ROUTER_EDGE_IP6=2001:db8:23::1 +TRAFFIC_CLIENT_IP6=2001:db8:23::2 +TRAFFIC_ROUTER_BACKEND_IP6=2001:db8:24::1 +TRAFFIC_TARGET_IP6=2001:db8:24::2 +TRAFFIC_IPV6_READY=0 +TRAFFIC_CLIENT_PID='' +TRAFFIC_BACKEND_PID='' +TRAFFIC_SERVER_PID='' + +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 + TRAFFIC_IPV6_READY=1 +fi restore_runtime_from_backup() { if [[ -f ${BACKUP_V4} ]]; then @@ -60,19 +77,165 @@ restore_runtime_from_backup() { if [[ -f ${BACKUP_V6} ]] && command -v ip6tables-restore >/dev/null 2>&1; then ip6tables-restore <"${BACKUP_V6}" >/dev/null 2>&1 || true fi + if [[ -n ${BACKUP_IPV4_FORWARD} ]]; then + printf '%s\n' "${BACKUP_IPV4_FORWARD}" >/proc/sys/net/ipv4/ip_forward 2>/dev/null || true + fi + if [[ -n ${BACKUP_IPV6_FORWARD} ]]; then + printf '%s\n' "${BACKUP_IPV6_FORWARD}" >/proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || true + fi +} + +traffic_stop_server() { + [[ -n ${TRAFFIC_SERVER_PID} ]] || return 0 + kill "${TRAFFIC_SERVER_PID}" >/dev/null 2>&1 || true + wait "${TRAFFIC_SERVER_PID}" 2>/dev/null || true + TRAFFIC_SERVER_PID='' +} + +traffic_cleanup_fixture() { + traffic_stop_server + for link in ipfc0 ipfb0; do + ip link del "${link}" >/dev/null 2>&1 || true + done + for pid in "${TRAFFIC_CLIENT_PID}" "${TRAFFIC_BACKEND_PID}"; do + [[ -n ${pid} ]] || continue + kill "${pid}" >/dev/null 2>&1 || true + wait "${pid}" 2>/dev/null || true + done + TRAFFIC_CLIENT_PID='' + TRAFFIC_BACKEND_PID='' } cleanup() { + traffic_cleanup_fixture restore_runtime_from_backup rm -rf "${TMP_DIR}" } trap cleanup EXIT +traffic_ns_exec() { + local pid=$1 + shift + nsenter -t "${pid}" -n "$@" +} + +traffic_setup_fixture() { + traffic_cleanup_fixture + + unshare -n bash -c 'sleep 1000' & + TRAFFIC_CLIENT_PID=$! + unshare -n bash -c 'sleep 1000' & + TRAFFIC_BACKEND_PID=$! + + ip link add ipfc0 type veth peer name ipfc1 + ip link add ipfb0 type veth peer name ipfb1 + ip link set ipfc1 netns "${TRAFFIC_CLIENT_PID}" + ip link set ipfb1 netns "${TRAFFIC_BACKEND_PID}" + + ip addr add "${TRAFFIC_ROUTER_EDGE_IP4}/24" dev ipfc0 + ip addr add "${TRAFFIC_ROUTER_BACKEND_IP4}/24" dev ipfb0 + ip link set ipfc0 up + ip link set ipfb0 up + + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" ip link set lo up + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" ip addr add "${TRAFFIC_CLIENT_IP4}/24" dev ipfc1 + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" ip link set ipfc1 up + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" ip route add default via "${TRAFFIC_ROUTER_EDGE_IP4}" + + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" ip link set lo up + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" ip addr add "${TRAFFIC_TARGET_IP4}/24" dev ipfb1 + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" ip link set ipfb1 up + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" ip route add default via "${TRAFFIC_ROUTER_BACKEND_IP4}" + + if [[ ${TRAFFIC_IPV6_READY} == 1 && -n ${BACKUP_IPV6_FORWARD} ]]; then + ip addr add "${TRAFFIC_ROUTER_EDGE_IP6}/64" dev ipfc0 nodad + ip addr add "${TRAFFIC_ROUTER_BACKEND_IP6}/64" dev ipfb0 nodad + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" ip addr add "${TRAFFIC_CLIENT_IP6}/64" dev ipfc1 nodad + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" ip -6 route add default via "${TRAFFIC_ROUTER_EDGE_IP6}" + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" ip addr add "${TRAFFIC_TARGET_IP6}/64" dev ipfb1 nodad + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" ip -6 route add default via "${TRAFFIC_ROUTER_BACKEND_IP6}" + printf '1\n' >/proc/sys/net/ipv6/conf/all/forwarding + fi + + printf '1\n' >/proc/sys/net/ipv4/ip_forward +} + +traffic_start_server() { + local family=$1 + local host=$2 + local port=$3 + traffic_stop_server + traffic_ns_exec "${TRAFFIC_BACKEND_PID}" python3 -u - "${family}" "${host}" "${port}" <<'PY' & +import socket +import sys + +family = socket.AF_INET6 if sys.argv[1] == '6' else socket.AF_INET +host = sys.argv[2] +port = int(sys.argv[3]) +bind_addr = (host, port, 0, 0) if family == socket.AF_INET6 else (host, port) +with socket.socket(family, socket.SOCK_STREAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(bind_addr) + sock.listen(1) + conn, _addr = sock.accept() + with conn: + conn.recv(1024) + conn.sendall(b'HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nOK') +PY + TRAFFIC_SERVER_PID=$! + sleep 0.2 +} + +traffic_client_http() { + local family=$1 + local host=$2 + local port=$3 + traffic_ns_exec "${TRAFFIC_CLIENT_PID}" python3 - "${family}" "${host}" "${port}" <<'PY' +import socket +import sys + +family = socket.AF_INET6 if sys.argv[1] == '6' else socket.AF_INET +host = sys.argv[2] +port = int(sys.argv[3]) +remote = (host, port, 0, 0) if family == socket.AF_INET6 else (host, port) +chunks = [] +with socket.socket(family, socket.SOCK_STREAM) as sock: + sock.settimeout(2) + sock.connect(remote) + sock.sendall(b'GET / HTTP/1.0\r\nHost: ipf-test\r\n\r\n') + while True: + data = sock.recv(4096) + if not data: + break + chunks.append(data) +sys.stdout.buffer.write(b''.join(chunks)) +PY +} + +traffic_request_ok() { + local family=$1 + local listen_host=$2 + local listen_port=$3 + local target_host=$4 + local target_port=$5 + local output + traffic_start_server "${family}" "${target_host}" "${target_port}" + if ! output=$(traffic_client_http "${family}" "${listen_host}" "${listen_port}"); then + traffic_stop_server + return 1 + fi + wait "${TRAFFIC_SERVER_PID}" + TRAFFIC_SERVER_PID='' + printf '%s\n' "${output}" +} + iptables-save >"${BACKUP_V4}" -if command -v ip6tables-save >/dev/null 2>&1; then +if [[ ${TRAFFIC_IPV6_READY} == 1 ]]; then ip6tables-save >"${BACKUP_V6}" fi +traffic_setup_fixture + export IPF_STORAGE_DIR="${TMP_DIR}/storage" export IPF_STORAGE_DB="${IPF_STORAGE_DIR}/rules.db" export IPF_LOCK_FILE="${IPF_STORAGE_DIR}/.lock" @@ -94,28 +257,33 @@ 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') +uuid_v4=$("${ROOT_DIR}/iptables-forward.sh" --batch add tcp "${TRAFFIC_LISTEN_PORT_V4}" "${TRAFFIC_TARGET_IP4}" "${TRAFFIC_TARGET_PORT_V4}" 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' +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 \ - -p tcp --dport 65432 \ - -j DNAT --to-destination 127.0.0.1:22 \ + -p tcp --dport "${TRAFFIC_LISTEN_PORT_V4}" \ + -j DNAT --to-destination "${TRAFFIC_TARGET_IP4}:${TRAFFIC_TARGET_PORT_V4}" \ -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' +assert_status 1 "$(status_of traffic_client_http 4 "${TRAFFIC_ROUTER_EDGE_IP4}" "${TRAFFIC_LISTEN_PORT_V4}")" 'missing PREROUTING rule should break actual TCP forwarding' persist_reload list_output=$(cmd_list 0) [[ ${list_output} =~ [[:space:]]✓[[:space:]] ]] || fail 'persist_reload should restore healthy status in cmd_list' +assert_contains "$(traffic_request_ok 4 "${TRAFFIC_ROUTER_EDGE_IP4}" "${TRAFFIC_LISTEN_PORT_V4}" "${TRAFFIC_TARGET_IP4}" "${TRAFFIC_TARGET_PORT_V4}")" 'OK' 'persist_reload should restore actual TCP forwarding' -ipt_remove_rule "${uuid_v4}" tcp 65432 127.0.0.1 22 4 +ipt_remove_rule "${uuid_v4}" tcp "${TRAFFIC_LISTEN_PORT_V4}" "${TRAFFIC_TARGET_IP4}" "${TRAFFIC_TARGET_PORT_V4}" 4 assert_status 1 "$(status_of grep -F "MGMT:${uuid_v4}" <(iptables-save))" 'manual runtime removal should clear managed IPv4 rule' +assert_status 1 "$(status_of traffic_client_http 4 "${TRAFFIC_ROUTER_EDGE_IP4}" "${TRAFFIC_LISTEN_PORT_V4}")" 'runtime rule removal should stop actual TCP forwarding' 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' +assert_contains "$(traffic_request_ok 4 "${TRAFFIC_ROUTER_EDGE_IP4}" "${TRAFFIC_LISTEN_PORT_V4}" "${TRAFFIC_TARGET_IP4}" "${TRAFFIC_TARGET_PORT_V4}")" 'OK' 'restored IPv4 rule should resume actual TCP forwarding' iptables -A FORWARD -p tcp --dport 65000 -j ACCEPT @@ -123,18 +291,23 @@ iptables -A FORWARD -p tcp --dport 65000 -j ACCEPT 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' +assert_status 1 "$(status_of traffic_client_http 4 "${TRAFFIC_ROUTER_EDGE_IP4}" "${TRAFFIC_LISTEN_PORT_V4}")" 'deleting managed IPv4 rule should stop actual TCP forwarding' -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') +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' - ipt_remove_rule "${uuid_v6}" tcp 65433 ::1 22 6 + 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' + assert_status 1 "$(status_of traffic_client_http 6 "${TRAFFIC_ROUTER_EDGE_IP6}" "${TRAFFIC_LISTEN_PORT_V6}")" 'runtime IPv6 rule removal should stop actual TCP forwarding' persist_reload assert_contains "$(ip6tables-save)" "MGMT:${uuid_v6}" 'persist_reload should restore IPv6 rule from snapshot' + assert_contains "$(traffic_request_ok 6 "${TRAFFIC_ROUTER_EDGE_IP6}" "${TRAFFIC_LISTEN_PORT_V6}" "${TRAFFIC_TARGET_IP6}" "${TRAFFIC_TARGET_PORT_V6}")" 'OK' 'persist_reload should restore actual IPv6 TCP forwarding' "${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' + assert_status 1 "$(status_of traffic_client_http 6 "${TRAFFIC_ROUTER_EDGE_IP6}" "${TRAFFIC_LISTEN_PORT_V6}")" 'deleting managed IPv6 rule should stop actual TCP forwarding' fi pass 'test_integration.sh'