#!/usr/bin/env bash # speedtest.sh — pangolin 隧道速度/质量验证工具 # # 在【pangolin 已连接】状态下运行(全局隧道,本机流量经节点出网),测: # 1. 出口确认 — 出口 IP 是否=节点(确认走的是 pangolin 而非别的代理) # 2. 链路质量 — 绑 en0 直测客户端→节点真实路径 RTT/丢包(绕开隧道) # 3. 代理延迟 — 经隧道访问几个站点的首字节时间(TTFB) # 4. 下行带宽 — 经隧道从 Cloudflare 下载,算 Mbps # 5. 上行带宽 — 经隧道上传到 Cloudflare,算 Mbps # # 用法: # bash scripts/speedtest.sh # 默认下/上行各 50MB / 10MB # bash scripts/speedtest.sh --node # 额外 ssh 节点测其自身出口带宽(理论上限对照) # DL_MB=100 UL_MB=20 bash scripts/speedtest.sh # # 环境变量: # NODE_IP 节点公网 IP(默认 107.172.55.251) # NODE_SSH 节点 ssh 别名(默认 racknerd,仅 --node 用) # DL_MB 下行测试大小 MB(默认 50) # UL_MB 上行测试大小 MB(默认 10) set -uo pipefail NODE_IP="${NODE_IP:-107.172.55.251}" NODE_SSH="${NODE_SSH:-racknerd}" DL_MB="${DL_MB:-50}" UL_MB="${UL_MB:-10}" WITH_NODE=0 [ "${1:-}" = "--node" ] && WITH_NODE=1 command -v curl >/dev/null 2>&1 || { echo "✗ 需要 curl"; exit 1; } # bytes/sec → 人类可读 Mbps(*8/1e6),保留 1 位小数 mbps() { awk -v b="$1" 'BEGIN{ printf "%.1f Mbps", b*8/1000000 }'; } hr_bytes() { awk -v b="$1" 'BEGIN{ split("B KB MB GB",u); i=1; while(b>=1024&&i<4){b/=1024;i++} printf "%.1f%s", b, u[i] }'; } echo "════════════════════════════════════════════════════" echo " pangolin 测速 节点=$NODE_IP 下行=${DL_MB}MB 上行=${UL_MB}MB" echo "════════════════════════════════════════════════════" # ── 1. 出口确认 ────────────────────────────────────────────────────────────── echo echo "==> [1/5] 出口 IP 确认" EGRESS_JSON="$(curl -fsS --max-time 15 https://ipinfo.io/json 2>/dev/null)" EGRESS_IP="$(printf '%s' "$EGRESS_JSON" | awk -F'"' '/"ip"/{print $4; exit}')" EGRESS_CITY="$(printf '%s' "$EGRESS_JSON" | awk -F'"' '/"city"/{print $4; exit}')" if [ -z "$EGRESS_IP" ]; then echo " ✗ 取不到出口 IP(隧道未连?网络异常?)——后续测试可能无意义" elif [ "$EGRESS_IP" = "$NODE_IP" ]; then echo " ✅ 出口 = $EGRESS_IP ($EGRESS_CITY) —— 确认走 pangolin 节点" else echo " ⚠ 出口 = $EGRESS_IP ($EGRESS_CITY) ≠ 节点 $NODE_IP" echo " 你可能没连 pangolin,或还开着别的代理(Shadowrocket/Tailscale)。测的不是 pangolin!" fi # ── 2. 真实链路质量(绕隧道,绑 en0 直测客户端→节点)──────────────────────── echo echo "==> [2/5] 客户端→节点 真实路径(绑 en0 绕隧道,30 包)" IF="$(route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')" IF="${IF:-en0}" PING_OUT="$(ping -b "$IF" -c 30 -i 0.2 "$NODE_IP" 2>/dev/null | tail -2)" if [ -n "$PING_OUT" ]; then printf '%s\n' "$PING_OUT" | sed 's/^/ /' else echo " (ping 失败:ICMP 可能被拦,或 -b $IF 不支持)" fi # ── 3. 经隧道代理延迟(TTFB)────────────────────────────────────────────────── echo echo "==> [3/5] 经隧道访问延迟(首字节 TTFB)" for url in https://www.gstatic.com/generate_204 https://cp.cloudflare.com/generate_204 https://www.youtube.com; do R="$(curl -o /dev/null -s --max-time 20 \ -w '连接=%{time_connect}s TTFB=%{time_starttransfer}s http=%{http_code}' "$url" 2>/dev/null)" printf ' %-40s %s\n' "${url#https://}" "${R:-✗ 超时/失败}" done # ── 4. 下行带宽 ────────────────────────────────────────────────────────────── echo echo "==> [4/5] 下行带宽(从 Cloudflare 下载 ${DL_MB}MB)" DL_BYTES=$((DL_MB * 1024 * 1024)) DL="$(curl -o /dev/null -s --max-time 120 \ -w '%{speed_download} %{time_total} %{size_download}' \ "https://speed.cloudflare.com/__down?bytes=$DL_BYTES" 2>/dev/null)" if [ -n "$DL" ]; then read -r dl_speed dl_time dl_size <<<"$DL" echo " 下行 $(mbps "$dl_speed") (传输 $(hr_bytes "$dl_size") 用时 ${dl_time}s)" else echo " ✗ 下行测试失败/超时" fi # ── 5. 上行带宽 ────────────────────────────────────────────────────────────── echo echo "==> [5/5] 上行带宽(上传 ${UL_MB}MB 到 Cloudflare)" UL_BYTES=$((UL_MB * 1024 * 1024)) UL="$(head -c "$UL_BYTES" /dev/zero | curl -o /dev/null -s --max-time 120 \ -X POST --data-binary @- \ -w '%{speed_upload} %{time_total} %{size_upload}' \ "https://speed.cloudflare.com/__up" 2>/dev/null)" if [ -n "$UL" ]; then read -r ul_speed ul_time ul_size <<<"$UL" echo " 上行 $(mbps "$ul_speed") (传输 $(hr_bytes "$ul_size") 用时 ${ul_time}s)" else echo " ✗ 上行测试失败/超时" fi # ── 可选:节点自身出口带宽(理论上限对照)────────────────────────────────── if [ "$WITH_NODE" = "1" ]; then echo echo "==> [+] 节点自身出口带宽(ssh $NODE_SSH,理论上限对照)" ssh -o ConnectTimeout=10 "$NODE_SSH" "curl -o /dev/null -s --max-time 60 \ -w '节点下行 %{speed_download} B/s 用时 %{time_total}s\n' \ 'https://speed.cloudflare.com/__down?bytes=$DL_BYTES'" 2>/dev/null \ | awk '{b=$2; printf " 节点下行 %.1f Mbps 用时 %ss\n", b*8/1000000, $4}' \ || echo " (ssh $NODE_SSH 失败,跳过)" fi echo echo "════════════════════════════════════════════════════" echo " 说明:节点是 RackNerd 512MB/1核,REALITY 加解密吃 CPU," echo " 几十~一两百 Mbps 属正常;跨境晚高峰会波动。" echo " 对照:退出 pangolin 后再跑一次 = 你的本地直连基线。" echo "════════════════════════════════════════════════════"