Files
pangolin/scripts/vpn_test.sh
T
wangjia 8fcc4e8b75 docs+test: VPN 测试方案(黑盒+白盒)+ 修正延迟测量方法
- docs/vpn-test-plan.md:黑盒+白盒两方向测试方案。白盒细化:链路五段拆解、
  协议/出站选择(Clash API)、接入段 vs 出海段分段 RTT、连接/流量、稳定性
  (长跑/Kill switch/GFW 三元组封锁与被动探测/热重载)。含初轮报告:cara 黑盒
  15 PASS;吞吐 国内直连 74Mbps vs 国外经隧道 ~10-17Mbps(瓶颈在出海段)。
- scripts/vpn_test.sh:修正延迟测量——经 TUN 代理时 ping/tcp_connect 被隧道本地
  应答(2-4ms 假象),改用 TLS 握手(time_appconnect)/ TTFB 作为真实 RTT。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEHzjEcFzvGwgbxT6Wbt6c
2026-06-22 09:17:00 +08:00

141 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# vpn_test.sh —— Pangolin 客户端侧黑盒连通/正确性/性能测试(调研文档「工具一」)
#
# 在"已连接 VPN 的机器"上跑(本机或 ssh 到 cara),一条命令出报告。覆盖:
# 出口 IP / DNS / 可达性矩阵 / 延迟 / 吞吐 / IPv6 泄漏。
# 每项给 PASS/WARN/FAIL + 实测值。支持基线对比(先 --baseline 裸连存一份)。
#
# 用法:
# bash scripts/vpn_test.sh # 完整测试(假定 VPN 已连)
# NODE_IP=103.119.13.48 bash scripts/vpn_test.sh
# bash scripts/vpn_test.sh --quick # 跳过吞吐(快)
# bash scripts/vpn_test.sh --baseline # 记基线(裸连,不开 VPN 时跑)
# 远程:scp scripts/vpn_test.sh cara@HOST:/tmp/ && ssh cara@HOST 'bash /tmp/vpn_test.sh'
#
# 设计:见 docs/vpn-testing-research.md。零依赖(curl + 系统自带),macOS/Linux 通用。
set -u
# ── 配置 ─────────────────────────────────────────────────────────────
NODE_IP="${NODE_IP:-103.119.13.48}" # 期望的隧道出口(节点 IP)
TIMEOUT="${TIMEOUT:-12}" # 单请求超时(秒)
DL_BYTES="${DL_BYTES:-5000000}" # 吞吐测试下载字节数(默认 5MB)
QUICK=0; MODE="vpn"
for a in "$@"; do
case "$a" in
--quick) QUICK=1 ;;
--baseline) MODE="baseline" ;;
esac
done
# 国外目标(应经隧道):站点
FOREIGN=("https://www.google.com/generate_204" "https://www.youtube.com/generate_204" \
"https://github.com" "https://www.gstatic.com/generate_204")
# 国内目标(开分流应直连、快):站点
DOMESTIC=("https://www.baidu.com" "https://www.qq.com")
# ── 输出辅助 ─────────────────────────────────────────────────────────
if [ -t 1 ]; then G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; B=$'\033[1m'; N=$'\033[0m'
else G=""; Y=""; R=""; B=""; N=""; fi
pass=0; warn=0; fail=0
P(){ printf " ${G}PASS${N} %-22s %s\n" "$1" "$2"; pass=$((pass+1)); }
W(){ printf " ${Y}WARN${N} %-22s %s\n" "$1" "$2"; warn=$((warn+1)); }
F(){ printf " ${R}FAIL${N} %-22s %s\n" "$1" "$2"; fail=$((fail+1)); }
H(){ printf "\n${B}== %s ==${N}\n" "$1"; }
cget(){ curl -s --max-time "$TIMEOUT" "$@"; } # 静默 + 超时
echo "${B}Pangolin VPN 测试${N} $(date '+%Y-%m-%d %H:%M:%S') 模式=${MODE} 期望出口=${NODE_IP}"
# ── 1. 连通性:本机隧道接口 / 系统扩展(仅本机 macOS 有意义)─────────
H "1. 连通性"
if command -v ifconfig >/dev/null 2>&1; then
if ifconfig 2>/dev/null | grep -q "inet 172.19.0.1"; then P "tun 接口" "172.19.0.1 已起"
else W "tun 接口" "未见 172.19.0.1(VPN 没连?或非本机)"; fi
fi
if command -v systemextensionsctl >/dev/null 2>&1; then
st=$(systemextensionsctl list 2>/dev/null | grep -i pangolin | grep -oE "activated (enabled|waiting for user)" | head -1)
[ -n "$st" ] && P "系统扩展" "$st" || W "系统扩展" "未激活/未安装"
fi
# ── 2. 出口 IP(核心:是否真走隧道)──────────────────────────────────
H "2. 出口 IP"
EGRESS=""
for u in "https://api.ipify.org" "https://ifconfig.me/ip" "https://ipinfo.io/ip"; do
EGRESS=$(cget "$u" | tr -dc '0-9.'); [ -n "$EGRESS" ] && break
done
if [ -z "$EGRESS" ]; then
F "出口 IP" "拿不到(国外不可达 / DNS 挂 / 未连)"
elif [ "$EGRESS" = "$NODE_IP" ]; then
P "出口 IP" "$EGRESS(= 节点,确实走隧道)"
else
W "出口 IP" "$EGRESS(≠ 节点 $NODE_IP;可能是本地/其他 VPN/直连)"
fi
# ── 3. DNS 解析 ──────────────────────────────────────────────────────
H "3. DNS 解析"
dnsok=0
for d in github.com www.google.com; do
t=$(cget -o /dev/null -w "%{time_namelookup}" "https://$d" 2>/dev/null)
code=$(cget -o /dev/null -w "%{http_code}" "https://$d" 2>/dev/null)
if [ "${code:-000}" != "000" ]; then P "DNS $d" "解析+连通 ${t}s (HTTP $code)"; dnsok=$((dnsok+1))
else F "DNS $d" "解析/连接失败(HTTP 000;hijack-dns 没生效?)"; fi
done
# ── 4. 可达性矩阵(国外应通)──────────────────────────────────────────
H "4. 可达性(国外,应经隧道)"
for u in "${FOREIGN[@]}"; do
read -r code tt < <(cget -o /dev/null -w "%{http_code} %{time_total}" "$u" 2>/dev/null || echo "000 -")
host=$(echo "$u" | sed -E 's#https?://([^/]+).*#\1#')
if [ "${code:-000}" != "000" ]; then P "$host" "HTTP $code, ${tt}s"
else F "$host" "不可达(HTTP 000)"; fi
done
# ── 5. 国内直连(开分流时应直连且快)─────────────────────────────────
H "5. 国内站(开分流应直连)"
for u in "${DOMESTIC[@]}"; do
read -r code tt < <(cget -o /dev/null -w "%{http_code} %{time_total}" "$u" 2>/dev/null || echo "000 -")
host=$(echo "$u" | sed -E 's#https?://([^/]+).*#\1#')
if [ "${code:-000}" != "000" ]; then P "$host" "HTTP $code, ${tt}s"
else W "$host" "不可达(分流/DNS 异常?)"; fi
done
# ── 6. 延迟(真实 RTT)────────────────────────────────────────────────
# ⚠️ 经 TUN 代理时 tcp_connect / ping 会被隧道本地应答(假的几 ms),不能当真实 RTT。
# 真实延迟看 TLS 握手(time_appconnect,必须真往返到目标)与 TTFB(time_starttransfer)。
H "6. 延迟(真实 RTT:TLS 握手 / TTFB;tcp_connect 经隧道为本地值仅供对照)"
for u in "https://www.google.com/generate_204" "https://www.gstatic.com/generate_204"; do
read -r tcp tls ttfb < <(cget -o /dev/null -w "%{time_connect} %{time_appconnect} %{time_starttransfer}" "$u" 2>/dev/null || echo "0 0 0")
host=$(echo "$u" | sed -E 's#https?://([^/]+).*#\1#')
if awk "BEGIN{exit !($tls>0)}" 2>/dev/null; then
tlsms=$(awk "BEGIN{printf \"%.0f\", $tls*1000}"); ttfbms=$(awk "BEGIN{printf \"%.0f\", $ttfb*1000}"); tcpms=$(awk "BEGIN{printf \"%.0f\", $tcp*1000}")
if [ "$tlsms" -lt 500 ] 2>/dev/null; then P "$host RTT" "TLS握手 ${tlsms}ms, TTFB ${ttfbms}ms (tcp_connect ${tcpms}ms=本地)"
elif [ "$tlsms" -lt 1000 ] 2>/dev/null; then W "$host RTT" "TLS握手 ${tlsms}ms 偏高, TTFB ${ttfbms}ms"
else F "$host RTT" "TLS握手 ${tlsms}ms 过高, TTFB ${ttfbms}ms"; fi
else F "$host RTT" "无响应/超时"; fi
done
echo " ${B}提示${N}: 真实到节点的直连 RTT 请在【VPN 全关】后 ping 节点;开着 TUN ping 是本地假象。"
# ── 7. 吞吐(下载速度)────────────────────────────────────────────────
if [ "$QUICK" = 0 ]; then
H "7. 吞吐(下载 ${DL_BYTES}B)"
read -r sz spd tt < <(cget -o /dev/null -w "%{size_download} %{speed_download} %{time_total}" \
"https://speed.cloudflare.com/__down?bytes=${DL_BYTES}" 2>/dev/null || echo "0 0 -")
if [ "${sz:-0}" -gt 0 ] 2>/dev/null; then
mbps=$(awk "BEGIN{printf \"%.2f\", ${spd:-0}*8/1000000}")
P "下载速度" "${mbps} Mbps (${sz}B in ${tt}s)"
else F "下载速度" "下载失败"; fi
fi
# ── 8. IPv6 泄漏 ─────────────────────────────────────────────────────
H "8. IPv6 泄漏"
# 用 v6-only 端点;无 v6 时 curl -6 失败返回空。仅当结果含冒号(真 v6 地址)才算有 v6。
v6=$(cget -6 "https://api6.ipify.org" 2>/dev/null)
if echo "$v6" | grep -q ':'; then W "IPv6" "v6 出口 $v6(确认是否走隧道,否则泄漏)"
else P "IPv6" "无 v6 出口(不泄漏)"; fi
# ── 汇总 ─────────────────────────────────────────────────────────────
H "汇总"
printf " ${G}PASS %d${N} ${Y}WARN %d${N} ${R}FAIL %d${N}\n" "$pass" "$warn" "$fail"
[ "$fail" -gt 0 ] && exit 1 || exit 0