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