Add end-to-end forwarding integration test
This commit is contained in:
@@ -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`
|
||||
|
||||
## 手工验收建议
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user