mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-20 21:43:41 +00:00
Merge pull request #3020 from shadowsocks/test/ss-redir-qemu-e2e
Add end-to-end test for ss-redir transparent proxy via QEMU
This commit is contained in:
@@ -45,3 +45,10 @@ jobs:
|
||||
- name: Stress test
|
||||
run: |
|
||||
python3 tests/stress_test.py --bin build/shared/bin/ --size 10
|
||||
|
||||
- name: ss-redir transparent proxy test (QEMU)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get install -y qemu-system-x86
|
||||
bash tests/test_redir_qemu.sh build/shared/bin/
|
||||
timeout-minutes: 8
|
||||
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Guest init script (PID 1) for ss-redir transparent proxy test.
|
||||
# Runs inside a QEMU Alpine Linux VM.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Mount filesystems and load kernel modules
|
||||
# 2. Configure networking (QEMU user-mode: guest 10.0.2.15, host 10.0.2.2)
|
||||
# 3. Start ss-redir pointing at ss-server on the host
|
||||
# 4. Run ss-nat to set up iptables REDIRECT rules
|
||||
# 5. Test with curl through the transparent proxy
|
||||
# 6. Print REDIR_TEST_PASS or REDIR_TEST_FAIL to serial console
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# --- helpers ----------------------------------------------------------------
|
||||
|
||||
pass() {
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "REDIR_TEST_PASS"
|
||||
echo "========================================="
|
||||
sync
|
||||
echo o > /proc/sysrq-trigger
|
||||
sleep 10
|
||||
}
|
||||
|
||||
fail() {
|
||||
local msg="${1:-unknown error}"
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "REDIR_TEST_FAIL: $msg"
|
||||
echo "========================================="
|
||||
sync
|
||||
echo o > /proc/sysrq-trigger
|
||||
sleep 10
|
||||
}
|
||||
|
||||
# --- mount filesystems ------------------------------------------------------
|
||||
|
||||
mount -t proc proc /proc
|
||||
mount -t sysfs sysfs /sys
|
||||
mount -t devtmpfs devtmpfs /dev
|
||||
mkdir -p /dev/pts /tmp /run
|
||||
mount -t devpts devpts /dev/pts
|
||||
mount -t tmpfs tmpfs /tmp
|
||||
mount -t tmpfs tmpfs /run
|
||||
|
||||
# Enable sysrq for clean poweroff
|
||||
echo 1 > /proc/sys/kernel/sysrq
|
||||
|
||||
echo "=== Guest init starting ==="
|
||||
|
||||
# --- load kernel modules ----------------------------------------------------
|
||||
|
||||
for mod in \
|
||||
virtio_net \
|
||||
ip_tables iptable_nat iptable_filter \
|
||||
nf_nat nf_conntrack \
|
||||
xt_REDIRECT xt_set xt_tcpudp \
|
||||
ip_set ip_set_hash_net; do
|
||||
modprobe "$mod" 2>/dev/null || echo "WARN: failed to load $mod (may be built-in)"
|
||||
done
|
||||
|
||||
echo "=== Kernel modules loaded ==="
|
||||
|
||||
# --- configure network ------------------------------------------------------
|
||||
|
||||
# Find the non-lo network interface
|
||||
IFACE=""
|
||||
for f in /sys/class/net/*/type; do
|
||||
iface=$(basename "$(dirname "$f")")
|
||||
if [ "$iface" != "lo" ]; then
|
||||
IFACE="$iface"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$IFACE" ]; then
|
||||
fail "no network interface found"
|
||||
fi
|
||||
|
||||
echo "=== Using network interface: $IFACE ==="
|
||||
|
||||
ip link set lo up
|
||||
ip link set "$IFACE" up
|
||||
ip addr add 10.0.2.15/24 dev "$IFACE"
|
||||
ip route add default via 10.0.2.2
|
||||
|
||||
# DNS (QEMU user-mode DNS forwarder)
|
||||
echo "nameserver 10.0.2.3" > /etc/resolv.conf
|
||||
|
||||
# Wait for link to come up
|
||||
sleep 2
|
||||
|
||||
# Connectivity check
|
||||
echo "=== Checking connectivity to host ==="
|
||||
if ! ping -c 2 -W 5 10.0.2.2; then
|
||||
fail "cannot reach host at 10.0.2.2"
|
||||
fi
|
||||
|
||||
echo "=== Host reachable ==="
|
||||
|
||||
# --- start ss-redir ---------------------------------------------------------
|
||||
|
||||
echo "=== Starting ss-redir ==="
|
||||
/usr/bin/ss-redir -c /etc/ss-redir.json -v &
|
||||
SS_REDIR_PID=$!
|
||||
sleep 3
|
||||
|
||||
if ! kill -0 "$SS_REDIR_PID" 2>/dev/null; then
|
||||
fail "ss-redir died immediately"
|
||||
fi
|
||||
|
||||
echo "=== ss-redir running (PID $SS_REDIR_PID) ==="
|
||||
|
||||
# --- run ss-nat (set up iptables REDIRECT) ----------------------------------
|
||||
|
||||
echo "=== Running ss-nat ==="
|
||||
if ! /usr/bin/ss-nat -s 10.0.2.2 -l 1080 -o; then
|
||||
fail "ss-nat failed"
|
||||
fi
|
||||
|
||||
echo "=== ss-nat rules applied ==="
|
||||
|
||||
# Show iptables state for debugging
|
||||
echo "=== iptables -t nat -L -n ==="
|
||||
iptables -t nat -L -n 2>&1 || true
|
||||
echo "=== ipset list ==="
|
||||
ipset list 2>&1 || true
|
||||
|
||||
# --- test with curl ---------------------------------------------------------
|
||||
|
||||
echo "=== Testing transparent proxy with curl ==="
|
||||
|
||||
# Try multiple URLs for reliability (captive portal detection endpoints
|
||||
# are lightweight and highly available)
|
||||
|
||||
# Test 1: Mozilla captive portal
|
||||
echo "--- Test 1: detectportal.firefox.com ---"
|
||||
BODY=$(curl -s --connect-timeout 15 -m 30 http://detectportal.firefox.com/success.txt 2>&1) || true
|
||||
echo "Response: $BODY"
|
||||
if echo "$BODY" | grep -q "success"; then
|
||||
echo "Test 1 PASSED"
|
||||
pass
|
||||
fi
|
||||
|
||||
# Test 2: Google (expect redirect or 200)
|
||||
echo "--- Test 2: www.google.com ---"
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 15 -m 30 http://www.google.com/ 2>&1) || true
|
||||
echo "HTTP code: $HTTP_CODE"
|
||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then
|
||||
echo "Test 2 PASSED"
|
||||
pass
|
||||
fi
|
||||
|
||||
# Test 3: Apple captive portal
|
||||
echo "--- Test 3: captive.apple.com ---"
|
||||
BODY=$(curl -s --connect-timeout 15 -m 30 http://captive.apple.com/ 2>&1) || true
|
||||
echo "Response: $BODY"
|
||||
if echo "$BODY" | grep -qi "success"; then
|
||||
echo "Test 3 PASSED"
|
||||
pass
|
||||
fi
|
||||
|
||||
# If we get here, all tests failed
|
||||
fail "all curl tests failed"
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"server":"10.0.2.2",
|
||||
"server_port":8389,
|
||||
"local_port":1080,
|
||||
"password":"test_redir_password",
|
||||
"timeout":60,
|
||||
"method":"aes-256-gcm"
|
||||
}
|
||||
Executable
+305
@@ -0,0 +1,305 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# End-to-end test for ss-redir transparent proxy using QEMU.
|
||||
#
|
||||
# Architecture:
|
||||
# Host: ss-server listens on 0.0.0.0:8389
|
||||
# QEMU guest (Alpine Linux):
|
||||
# - ss-redir connects to host ss-server via 10.0.2.2:8389
|
||||
# - ss-nat sets up iptables OUTPUT chain REDIRECT to ss-redir
|
||||
# - curl through the transparent proxy verifies the full chain
|
||||
#
|
||||
# Usage: bash tests/test_redir_qemu.sh [BIN_DIR]
|
||||
# BIN_DIR: directory containing ss-server and ss-redir binaries
|
||||
# (default: build/shared/bin/)
|
||||
#
|
||||
# Requirements (Linux only):
|
||||
# - qemu-system-x86_64
|
||||
# - Internet access (for Alpine download and curl test targets)
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BIN_DIR="${1:-$PROJECT_DIR/build/shared/bin}"
|
||||
|
||||
# Resolve to absolute path
|
||||
BIN_DIR="$(cd "$BIN_DIR" && pwd)"
|
||||
|
||||
WORK_DIR=""
|
||||
SS_SERVER_PID=""
|
||||
QEMU_PID=""
|
||||
|
||||
ALPINE_VERSION="3.21"
|
||||
ALPINE_ARCH="x86_64"
|
||||
ALPINE_MIRROR="https://dl-cdn.alpinelinux.org/alpine"
|
||||
ALPINE_MINIROOTFS="alpine-minirootfs-${ALPINE_VERSION}.0-${ALPINE_ARCH}.tar.gz"
|
||||
ALPINE_URL="${ALPINE_MIRROR}/v${ALPINE_VERSION}/releases/${ALPINE_ARCH}/${ALPINE_MINIROOTFS}"
|
||||
|
||||
TIMEOUT=240
|
||||
SS_SERVER_PORT=8389
|
||||
SS_PASSWORD="test_redir_password"
|
||||
SS_METHOD="aes-256-gcm"
|
||||
|
||||
# --- helpers ----------------------------------------------------------------
|
||||
|
||||
log() {
|
||||
echo "=== [test_redir_qemu] $* ==="
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "FATAL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
log "Cleaning up"
|
||||
if [ -n "$SS_SERVER_PID" ] && kill -0 "$SS_SERVER_PID" 2>/dev/null; then
|
||||
kill "$SS_SERVER_PID" 2>/dev/null || true
|
||||
wait "$SS_SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$QEMU_PID" ] && kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||
kill "$QEMU_PID" 2>/dev/null || true
|
||||
wait "$QEMU_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then
|
||||
sudo rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- pre-flight checks ------------------------------------------------------
|
||||
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
die "This test requires Linux (iptables, QEMU)"
|
||||
fi
|
||||
|
||||
if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then
|
||||
die "qemu-system-x86_64 not found. Install with: sudo apt-get install qemu-system-x86"
|
||||
fi
|
||||
|
||||
if [ ! -x "$BIN_DIR/ss-server" ]; then
|
||||
die "ss-server not found at $BIN_DIR/ss-server"
|
||||
fi
|
||||
|
||||
if [ ! -x "$BIN_DIR/ss-redir" ]; then
|
||||
die "ss-redir not found at $BIN_DIR/ss-redir"
|
||||
fi
|
||||
|
||||
# --- create work directory --------------------------------------------------
|
||||
|
||||
WORK_DIR="$(mktemp -d /tmp/ss-redir-test.XXXXXX)"
|
||||
log "Work directory: $WORK_DIR"
|
||||
|
||||
# --- phase 1: collect ss-redir and its shared library dependencies ----------
|
||||
|
||||
log "Collecting ss-redir shared library dependencies"
|
||||
|
||||
# Copy ss-redir binary
|
||||
SYSROOT="$WORK_DIR/sysroot"
|
||||
mkdir -p "$SYSROOT/usr/bin"
|
||||
cp "$BIN_DIR/ss-redir" "$SYSROOT/usr/bin/ss-redir"
|
||||
|
||||
# Copy all shared library dependencies (including the dynamic linker)
|
||||
ldd "$BIN_DIR/ss-redir" | while read -r line; do
|
||||
# Parse lines like: libev.so.4 => /lib/x86_64-linux-gnu/libev.so.4 (0x...)
|
||||
# or: /lib64/ld-linux-x86-64.so.2 (0x...)
|
||||
lib_path=""
|
||||
if echo "$line" | grep -q "=>"; then
|
||||
lib_path=$(echo "$line" | awk '{print $3}')
|
||||
elif echo "$line" | grep -qE "^\s*/"; then
|
||||
lib_path=$(echo "$line" | awk '{print $1}')
|
||||
fi
|
||||
if [ -n "$lib_path" ] && [ -f "$lib_path" ]; then
|
||||
dest="$SYSROOT$lib_path"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$lib_path" "$dest"
|
||||
fi
|
||||
done
|
||||
|
||||
log "Shared libraries collected into sysroot"
|
||||
|
||||
# --- phase 2: assemble Alpine rootfs ----------------------------------------
|
||||
|
||||
log "Downloading Alpine minirootfs"
|
||||
|
||||
ROOTFS="$WORK_DIR/rootfs"
|
||||
mkdir -p "$ROOTFS"
|
||||
|
||||
curl -sSL "$ALPINE_URL" -o "$WORK_DIR/$ALPINE_MINIROOTFS"
|
||||
sudo tar xzf "$WORK_DIR/$ALPINE_MINIROOTFS" -C "$ROOTFS"
|
||||
|
||||
log "Installing packages in Alpine rootfs"
|
||||
|
||||
# Set up DNS for chroot
|
||||
sudo cp /etc/resolv.conf "$ROOTFS/etc/resolv.conf"
|
||||
|
||||
# Mount necessary filesystems for chroot
|
||||
sudo mount --bind /dev "$ROOTFS/dev"
|
||||
sudo mount --bind /proc "$ROOTFS/proc"
|
||||
sudo mount --bind /sys "$ROOTFS/sys"
|
||||
|
||||
# Install packages inside chroot
|
||||
sudo chroot "$ROOTFS" /sbin/apk add --no-cache \
|
||||
iptables ipset curl iproute2 kmod bash linux-virt
|
||||
|
||||
# Unmount after package installation
|
||||
sudo umount "$ROOTFS/sys" 2>/dev/null || true
|
||||
sudo umount "$ROOTFS/proc" 2>/dev/null || true
|
||||
sudo umount "$ROOTFS/dev" 2>/dev/null || true
|
||||
|
||||
# Extract kernel
|
||||
VMLINUZ=$(find "$ROOTFS/boot" -name 'vmlinuz-*' -type f | head -1)
|
||||
if [ -z "$VMLINUZ" ]; then
|
||||
die "No vmlinuz found in Alpine rootfs"
|
||||
fi
|
||||
cp "$VMLINUZ" "$WORK_DIR/vmlinuz"
|
||||
log "Kernel: $(basename "$VMLINUZ")"
|
||||
|
||||
# --- phase 3: inject artifacts into rootfs ----------------------------------
|
||||
|
||||
log "Injecting test artifacts into rootfs"
|
||||
|
||||
# ss-redir binary and its glibc shared library dependencies
|
||||
# These go into the rootfs at their original absolute paths so the
|
||||
# ELF dynamic linker (PT_INTERP) can find everything.
|
||||
sudo cp -a "$SYSROOT/." "$ROOTFS/"
|
||||
|
||||
# ss-nat script
|
||||
sudo cp "$PROJECT_DIR/src/ss-nat" "$ROOTFS/usr/bin/ss-nat"
|
||||
sudo chmod 755 "$ROOTFS/usr/bin/ss-nat"
|
||||
|
||||
# Config file
|
||||
sudo cp "$SCRIPT_DIR/redir.json" "$ROOTFS/etc/ss-redir.json"
|
||||
|
||||
# Guest init script (becomes PID 1)
|
||||
sudo cp "$SCRIPT_DIR/qemu/guest-init.sh" "$ROOTFS/init"
|
||||
sudo chmod 755 "$ROOTFS/init"
|
||||
|
||||
# --- phase 4: pack initramfs -----------------------------------------------
|
||||
|
||||
log "Packing initramfs"
|
||||
|
||||
(cd "$ROOTFS" && sudo find . | sudo cpio -o -H newc 2>/dev/null | gzip -1 > "$WORK_DIR/initramfs.gz")
|
||||
|
||||
log "Initramfs size: $(du -h "$WORK_DIR/initramfs.gz" | cut -f1)"
|
||||
|
||||
# --- phase 5: start ss-server on host --------------------------------------
|
||||
|
||||
log "Starting ss-server on host"
|
||||
|
||||
"$BIN_DIR/ss-server" \
|
||||
-s 0.0.0.0 \
|
||||
-p "$SS_SERVER_PORT" \
|
||||
-k "$SS_PASSWORD" \
|
||||
-m "$SS_METHOD" \
|
||||
-v &
|
||||
SS_SERVER_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
if ! kill -0 "$SS_SERVER_PID" 2>/dev/null; then
|
||||
die "ss-server failed to start"
|
||||
fi
|
||||
|
||||
log "ss-server running (PID $SS_SERVER_PID)"
|
||||
|
||||
# --- phase 6: boot QEMU ----------------------------------------------------
|
||||
|
||||
log "Booting QEMU"
|
||||
|
||||
KVM_FLAG=""
|
||||
if [ -e /dev/kvm ] && [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
|
||||
KVM_FLAG="-enable-kvm"
|
||||
log "KVM acceleration available"
|
||||
else
|
||||
log "KVM not available, using TCG (slower)"
|
||||
fi
|
||||
|
||||
SERIAL_LOG="$WORK_DIR/serial.log"
|
||||
|
||||
qemu-system-x86_64 \
|
||||
$KVM_FLAG \
|
||||
-m 512 \
|
||||
-kernel "$WORK_DIR/vmlinuz" \
|
||||
-initrd "$WORK_DIR/initramfs.gz" \
|
||||
-append "console=ttyS0 init=/init panic=1" \
|
||||
-nographic \
|
||||
-serial "file:$SERIAL_LOG" \
|
||||
-monitor none \
|
||||
-net nic,model=virtio \
|
||||
-net user \
|
||||
-no-reboot &
|
||||
QEMU_PID=$!
|
||||
|
||||
log "QEMU running (PID $QEMU_PID)"
|
||||
|
||||
# --- phase 7: monitor serial log -------------------------------------------
|
||||
|
||||
log "Waiting for test result (timeout: ${TIMEOUT}s)"
|
||||
|
||||
ELAPSED=0
|
||||
POLL_INTERVAL=2
|
||||
RESULT=""
|
||||
|
||||
while [ $ELAPSED -lt $TIMEOUT ]; do
|
||||
if [ -f "$SERIAL_LOG" ]; then
|
||||
if grep -q "REDIR_TEST_PASS" "$SERIAL_LOG" 2>/dev/null; then
|
||||
RESULT="PASS"
|
||||
break
|
||||
fi
|
||||
if grep -q "REDIR_TEST_FAIL" "$SERIAL_LOG" 2>/dev/null; then
|
||||
RESULT="FAIL"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if QEMU is still running
|
||||
if ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||
RESULT="QEMU_DIED"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep $POLL_INTERVAL
|
||||
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
||||
done
|
||||
|
||||
# --- phase 8: report -------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "Serial console output:"
|
||||
echo "============================================================"
|
||||
if [ -f "$SERIAL_LOG" ]; then
|
||||
cat "$SERIAL_LOG"
|
||||
else
|
||||
echo "(no serial log file)"
|
||||
fi
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
|
||||
case "$RESULT" in
|
||||
PASS)
|
||||
log "TEST PASSED"
|
||||
exit 0
|
||||
;;
|
||||
FAIL)
|
||||
FAIL_MSG=$(grep "REDIR_TEST_FAIL" "$SERIAL_LOG" 2>/dev/null || echo "unknown")
|
||||
log "TEST FAILED: $FAIL_MSG"
|
||||
exit 1
|
||||
;;
|
||||
QEMU_DIED)
|
||||
log "TEST FAILED: QEMU exited unexpectedly"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
log "TEST FAILED: timeout after ${TIMEOUT}s"
|
||||
# Kill QEMU if still running
|
||||
if kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||
kill "$QEMU_PID" 2>/dev/null || true
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user