Files
shadowsocks-libev/tests/test_redir_qemu.sh
Max Lv 201e70c4bd Modernize C builds with offline dependencies and portable runtime helpers (#3050)
Bundle pinned offline dependencies, remove libcork/libipset and submodule requirements, and add portable runtime helpers with relocatable static/shared library installations.

Add Clang static build validation for Linux/musl, macOS and Windows, real TCP/UDP and SIP003 interoperability coverage, and recorded performance tradeoffs. All 21 hosted checks pass at the reviewed PR head 8d504e6218.
2026-09-11 13:42:11 +08:00

306 lines
8.2 KiB
Bash
Executable File

#!/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/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/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