The repo was committed from WSL with core.filemode=false, so the exec bit was never recorded. After actions/checkout the entry script comes down as 100644 and tests/test_cli.sh fails with Permission denied. Set mode 100755 on every script that is invoked directly (entry, installer, test suite, mock binaries). Sourced helpers under lib/ keep 100644 per convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
320 lines
13 KiB
Bash
Executable File
320 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|
# shellcheck source=tests/lib/assert.sh
|
|
source "${ROOT_DIR}/tests/lib/assert.sh"
|
|
|
|
status_of() {
|
|
set +e
|
|
"$@" >/dev/null 2>&1
|
|
local rc=$?
|
|
set -e
|
|
printf '%s\n' "${rc}"
|
|
}
|
|
|
|
maybe_enter_namespace() {
|
|
if (( EUID == 0 )); then
|
|
return 0
|
|
fi
|
|
|
|
if [[ ${IPF_IN_NAMESPACE:-0} == 1 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if command -v unshare >/dev/null 2>&1 && unshare -Urn true >/dev/null 2>&1; then
|
|
exec unshare -Urn env IPF_IN_NAMESPACE=1 bash "$0"
|
|
fi
|
|
|
|
printf 'SKIP: root 或 unshare 不可用,跳过集成测试。\n'
|
|
exit 0
|
|
}
|
|
|
|
maybe_enter_namespace
|
|
|
|
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
|
|
|
|
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
|
|
iptables-restore <"${BACKUP_V4}" >/dev/null 2>&1 || true
|
|
fi
|
|
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 [[ ${TRAFFIC_IPV6_READY} == 1 ]]; then
|
|
ip6tables-save >"${BACKUP_V6}"
|
|
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"
|
|
export IPF_LOCK_FILE="${IPF_STORAGE_DIR}/.lock"
|
|
export IPF_SKIP_ENV_CHECK=1
|
|
export IPF_FORCE_PLAIN_UI=1
|
|
export NETFILTER_PERSISTENT_BIN="${ROOT_DIR}/tests/mocks/persist-fixture.sh"
|
|
export PERSIST_RULES_V4
|
|
export PERSIST_RULES_V6
|
|
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 "${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 "$(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 \
|
|
-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 "${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
|
|
|
|
"${ROOT_DIR}/iptables-forward.sh" --batch delete "${uuid_v4}"
|
|
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 [[ ${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'
|
|
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'
|