Files
wangjia 51a5170b9e feat(scripts): 新增 speedtest.sh 隧道测速/质量验证工具
连上 pangolin 后一键测:出口IP确认 / 客户端→节点真实路径(绑en0绕隧道,
RTT+丢包) / 经隧道TTFB / Cloudflare 下行+上行带宽;--node 可 ssh 节点测
其自身出口带宽做理论上限对照。零依赖(curl+ping)。

实测节点(RackNerd 512MB/1核,LA):下行~22-24 / 上行~25 Mbps,RTT 298ms,
0% 丢包(晚高峰偶发瞬时丢包)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:48:44 +08:00

122 lines
6.5 KiB
Bash

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