Add end-to-end forwarding integration test

This commit is contained in:
2026-04-17 11:51:59 +08:00
parent af871a67a3
commit bd6dc0279e
2 changed files with 195 additions and 21 deletions

View File

@@ -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`
## 手工验收建议

View File

@@ -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'