mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-31 22:45:45 +00:00
* Fix findings from the security audit Remote: - TCP: handshake/connect deadlines follow --timeout instead of a 24h minimum, so idle sockets can no longer fill the connection pool. - ACL: IPv4-mapped IPv6 addresses are matched against the IPv4 lists. - UDP server now enforces the peer ACL and the outbound block list. - ACL loading fails closed on bad files or invalid entries. - TCP half-close is relayed instead of tearing the connection down. - AEAD-2022 UDP sessions are created only after timestamp/replay checks. ss-manager: - Owner-only conf files (deleted on remove), 0600 control socket, umask 022, safe working directory, loopback-only IP addresses, per-user sender check on Linux, plugins only over the UNIX socket. - Port canonicalisation, list() overflow, add/remove waits for the server, stale pid files can't signal unrelated processes. Also: plugins drop to --user, daemonize readiness/pid file handling, resolver cancellation, replay-filter fail-closed, key wiping, spec strictness in AEAD/2022 parsing, JSON surrogate validation, ss:// plugin name checks, embedding API state reset, Android protect_socket replies. Adds tests/test_server_acl.py and tests/test_half_close.py and extends the manager, ACL, AEAD-2022 and ssurl tests. Co-Authored-By: Claude <noreply@anthropic.com> * Fix CI on Ubuntu 26.04 runners; use string_view over raw string pointers - Define _GLIBCXX_NO_ASSERTIONS: libstdc++ 15 enables assertions at -O0, which pulls __glibcxx_assert_fail (C++ runtime) into the programs. - test_redir_qemu: boot the guest with -cpu max so the runner's glibc loader runs under TCG. - find_program: std::string_view PATH splitting into a fixed buffer, no heap and no null snprintf destination (GCC -Wformat-truncation). - manager: parse /proc/net/udp numbers with strtoul (cert-err34-c); walk /proc/<pid>/cmdline with std::string_view. - ssurl: sanitise the tag through std::span. Co-Authored-By: Claude <noreply@anthropic.com> * coverage: tolerate gcov 15 lambda inconsistencies in lcov 2 Co-Authored-By: Claude <noreply@anthropic.com> * resolver: replace init's cleanup lambda with a member; lcov --list ignores inconsistent gcov 15 marks the lambda as not hit while its lines are, which lcov 2 rejects. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
307 lines
8.2 KiB
Bash
Executable File
307 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 \
|
|
-cpu max \
|
|
-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
|