cn 三网测速(实验性)
这是实验性脚本,仅为兼容保留。从境外 VPS 上,Ookla 经常无法列出国内服务器,测试可能全部失败。若仍要尝试,运行下面的命令;日常测速请用 xyz.sh/speedtest --overseas。
别名 speedtest-cn
$ curl -fsSL xyz.sh/cn | bash
cn speedtest359914f2881747a176129913dd1820fa830798eae3d77adf5d0d2a508a7e46df8
· 与 curl 拿到的脚本一致,也可访问 /cn.sh.sha256
所有脚本都通过 bash -n 与 ShellCheck 检查,并在 Debian 12/11、Ubuntu 24.04/22.04/20.04/18.04、Rocky 9、AlmaLinux 9、CentOS 7 的官方 Docker 镜像中以 root 运行 curl | bash 测试(结果见各页“容器测试结果”)。容器里没有 systemd、内核模块和真实公网线路,相关部分需要在真实 VPS 上确认。
它会做什么
实验性脚本,仅为兼容保留;日常请用 xyz.sh/speedtest --overseas 或 xyz.sh/all。Ookla 不再向海外客户端列出国内服务器,所以 speedtest -s <ID> 经常报 “No servers defined”,从境外 VPS 上国内节点经常全部失败,本站不承诺能测出国内速度。若仍要尝试,脚本会按运营商依次试:① 官方 Ookla CLI(官方 tarball,SHA256 固定)-s <ID>;② speedtest-go(官方发布版,SHA256 固定,只在临时目录运行)用 --custom-url 直连;③ 对 speedtest.cn 节点做 HTTP 测速。候选来自内置列表和 spiritLHLS 的公开列表,失败时打印完整错误。
容器测试结果
在各发行版官方 Docker 镜像中以 root、无终端方式运行 curl | bash 的结果。partial 表示容器内无法完整验证(例如没有 systemd、内核模块或测速协议被测试环境的代理拦截),需要在真实 VPS 上确认。 (2026-11-11)
| OS | 结果 |
|---|---|
| debian:12 | partial |
| debian:11 | partial |
| ubuntu:24.04 | partial |
| ubuntu:22.04 | partial |
| ubuntu:20.04 | partial |
| ubuntu:18.04 | partial |
| rockylinux:9 | partial |
| almalinux:9 | partial |
| centos:7 | partial |
常见问题
- 这个脚本安全吗?
- xyz.sh 不埋点、不偷偷执行未说明的操作,完整源码可在页面上查看,也可以用 curl -fsSL xyz.sh/<name>.sh 下载后先读再运行;每个脚本都有 /<name>.sh.sha256 校验值。把远程脚本管道给 bash 仍有风险,请先阅读源码。测速由 Ookla 官方 CLI 完成,有自己的许可与隐私条款(https://www.speedtest.net/terms),脚本会先显示并请你确认。
- 支持哪些系统?
- 声明支持 Debian、Ubuntu、CentOS、Rocky、AlmaLinux。脚本会检测发行版,无法识别时会提示。已在这些系统的 Docker 容器里做过语法、--dry-run 与可运行部分的测试(结果见 GitHub 上的 tests/RESULTS.md);依赖 systemd、内核模块或公网的部分需要在真实 VPS 上验证。
- 需要 root 吗?
- 不需要。普通用户也能运行;以 root 运行时部分工具可以安装到系统目录。
- 为什么只有几个国内节点?
- Ookla 已下架大部分中国大陆的 Speedtest 节点,目前公开可用的只有少数几个,且会变化。内置节点来自每天更新的公开列表 spiritLHLS/speedtest.net-CN-ID;全部失败时脚本会下载一次最新列表再试。也可以用 --servers 指定你自己知道的节点 ID。
- 结果准吗?
- 结果是 VPS 到某个运营商测速节点的单次带宽,受节点负载、晚高峰、VPS 限速影响,适合比较线路,不代表你家宽带的实际速度。建议在不同时段多测几次。
完整源码
以下就是 curl 实际拿到的内容(公共库 + 脚本正文)。运行前请先读一遍。
#!/usr/bin/env bash
# Served by xyz.sh/cn
# Human-readable page: https://xyz.sh/cn
# This file is the common library followed by the script body.
# No telemetry is added by xyz.sh.
# =============================================================================
# xyz.sh common library
# Shared helpers for every script served by xyz.sh.
#
# Promises:
# - No telemetry, analytics, or background "phone-home" calls
# - No hidden installs or config changes beyond what the script announces
# - Official upstream URLs are printed before any remote code is executed
# - Destructive or system-changing steps ask for confirmation (or YES=1 / -y)
# =============================================================================
if [[ -n "${XYZ_COMMON_LOADED:-}" ]]; then
# When sourced a second time, return. When executed as a file, return fails.
# shellcheck disable=SC2317
return 0 2>/dev/null || true
fi
# shellcheck disable=SC2034
XYZ_COMMON_LOADED=1
set -euo pipefail
XYZ_VERSION="1.3.0"
XYZ_BASE="${XYZ_BASE:-https://xyz.sh}"
XYZ_NAME="${XYZ_NAME:-unknown}"
# --- language ---------------------------------------------------------------
# Override with XYZ_LANG=zh or XYZ_LANG=en.
if [[ -z "${XYZ_LANG:-}" ]]; then
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|ZH*|cmn*|CMN*) XYZ_LANG="zh" ;;
*) XYZ_LANG="en" ;;
esac
fi
xyz_t() {
# xyz_t "English text" "中文文本"
if [[ "${XYZ_LANG}" == "zh" ]]; then
printf '%s' "${2:-$1}"
else
printf '%s' "$1"
fi
}
# --- colors (TTY only) ------------------------------------------------------
# XYZ_COLOR=always|never|auto (default auto: TTY and no NO_COLOR); --no-color.
_xyz_color=0
case "${XYZ_COLOR:-auto}" in
always) _xyz_color=1 ;;
never) _xyz_color=0 ;;
*) [[ -t 1 && -z "${NO_COLOR:-}" ]] && _xyz_color=1 ;;
esac
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && _xyz_color=0
if [[ "${_xyz_color}" == "1" ]]; then
XYZ_C_RESET=$'\033[0m'
XYZ_C_BOLD=$'\033[1m'
XYZ_C_DIM=$'\033[2m'
XYZ_C_RED=$'\033[31m'
XYZ_C_GREEN=$'\033[32m'
XYZ_C_YELLOW=$'\033[33m'
XYZ_C_CYAN=$'\033[36m'
else
XYZ_C_RESET=""
XYZ_C_BOLD=""
XYZ_C_DIM=""
XYZ_C_RED=""
XYZ_C_GREEN=""
XYZ_C_YELLOW=""
XYZ_C_CYAN=""
fi
xyz_info() { printf '%s[INFO]%s %s\n' "${XYZ_C_GREEN}" "${XYZ_C_RESET}" "$*"; }
xyz_warn() { printf '%s[WARN]%s %s\n' "${XYZ_C_YELLOW}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_error() { printf '%s[ERROR]%s %s\n' "${XYZ_C_RED}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_step() { printf '%s==>%s %s\n' "${XYZ_C_CYAN}" "${XYZ_C_RESET}" "$*"; }
# --- args / init ------------------------------------------------------------
XYZ_ARGS=()
XYZ_YES=0
XYZ_DRY_RUN=0
xyz_parse_global_args() {
local filtered=()
local arg
for arg in "$@"; do
case "${arg}" in
-y|--yes)
XYZ_YES=1
;;
--lang=zh|zh)
XYZ_LANG="zh"
;;
--lang=en|en)
XYZ_LANG="en"
;;
--dry-run)
XYZ_DRY_RUN=1
;;
--no-color)
XYZ_NO_COLOR=1
XYZ_C_RESET=""; XYZ_C_BOLD=""; XYZ_C_DIM=""; XYZ_C_RED=""
XYZ_C_GREEN=""; XYZ_C_YELLOW=""; XYZ_C_CYAN=""
;;
--help|-h)
XYZ_HELP=1
filtered+=("${arg}")
;;
*)
filtered+=("${arg}")
;;
esac
done
XYZ_ARGS=("${filtered[@]+"${filtered[@]}"}")
if [[ "${YES:-}" =~ ^(1|yes|true|YES)$ || "${XYZ_ASSUME_YES:-}" == "1" ]]; then
XYZ_YES=1
fi
# Exported so individual scripts can read flags set here.
export XYZ_ARGS XYZ_YES XYZ_LANG XYZ_HELP="${XYZ_HELP:-}" XYZ_DRY_RUN="${XYZ_DRY_RUN:-0}"
export XYZ_NO_COLOR="${XYZ_NO_COLOR:-0}"
if declare -F xyz_ui_init >/dev/null; then xyz_ui_init; fi
}
xyz_banner() {
local title="${XYZ_TITLE:-$XYZ_NAME}"
printf '\n%s%sxyz.sh/%s%s v%s\n' "${XYZ_C_BOLD}" "${XYZ_C_CYAN}" "${XYZ_NAME}" "${XYZ_C_RESET}" "${XYZ_VERSION}"
printf '%s\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
printf ' %s: %s/%s\n' "$(xyz_t "Page" "页面")" "${XYZ_BASE}" "${XYZ_NAME}"
printf ' %s: %s/%s.sh\n' "$(xyz_t "Raw" "源码")" "${XYZ_BASE}" "${XYZ_NAME}"
if [[ -n "${XYZ_UPSTREAM:-}" ]]; then
printf ' %s: %s\n' "$(xyz_t "Upstream" "上游来源")" "${XYZ_UPSTREAM}"
fi
printf ' %s\n' "$(xyz_t "No telemetry, no hidden actions." "无遥测、无隐藏操作。")"
printf '%s\n\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
if [[ -n "${title}" && "${title}" != "${XYZ_NAME}" ]]; then
xyz_step "${title}"
fi
}
xyz_init() {
xyz_parse_global_args "$@"
xyz_detect_os
xyz_banner
}
# --- root / OS --------------------------------------------------------------
xyz_is_root() {
[[ "${EUID:-$(id -u)}" -eq 0 ]]
}
xyz_require_root() {
if ! xyz_is_root; then
xyz_error "$(xyz_t \
"This script must be run as root. Try: sudo bash" \
"请使用 root 运行。可尝试:sudo bash")"
exit 1
fi
}
xyz_prefer_root() {
if ! xyz_is_root; then
xyz_warn "$(xyz_t \
"Not running as root; some information or tests may be limited." \
"当前不是 root,部分信息或测试可能不完整。")"
fi
}
XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY="" # debian | rhel | unknown
XYZ_OS_PRETTY=""
xyz_detect_os() {
XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY="unknown"
XYZ_OS_PRETTY="unknown"
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
XYZ_OS_ID="${ID:-}"
XYZ_OS_LIKE="${ID_LIKE:-}"
XYZ_OS_VERSION="${VERSION_ID:-}"
XYZ_OS_PRETTY="${PRETTY_NAME:-$XYZ_OS_ID}"
export XYZ_OS_ID XYZ_OS_LIKE XYZ_OS_VERSION XYZ_OS_FAMILY XYZ_OS_PRETTY
fi
case "${XYZ_OS_ID}" in
debian|ubuntu)
XYZ_OS_FAMILY="debian"
;;
centos|rhel|rocky|almalinux|alma|fedora)
XYZ_OS_FAMILY="rhel"
;;
*)
case " ${XYZ_OS_ID} ${XYZ_OS_LIKE} " in
*debian*|*ubuntu*) XYZ_OS_FAMILY="debian" ;;
*rhel*|*centos*|*fedora*|*rocky*|*alma*) XYZ_OS_FAMILY="rhel" ;;
*) : ;;
esac
;;
esac
}
xyz_os_supported() {
case "${XYZ_OS_ID}" in
debian|ubuntu|centos|rocky|almalinux) return 0 ;;
*) : ;;
esac
case " ${XYZ_OS_LIKE} " in
*debian*|*ubuntu*|*rhel*|*centos*|*rocky*|*alma*) return 0 ;;
*) : ;;
esac
return 1
}
xyz_require_supported_os() {
xyz_detect_os
if ! xyz_os_supported; then
xyz_warn "$(xyz_t \
"Unrecognized distribution: ${XYZ_OS_ID:-unknown} (${XYZ_OS_PRETTY}). Officially supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux." \
"未能识别的发行版:${XYZ_OS_ID:-unknown}(${XYZ_OS_PRETTY})。官方支持:Debian、Ubuntu、CentOS、Rocky、AlmaLinux。")"
if ! xyz_confirm "$(xyz_t "Continue anyway?" "仍然继续?")"; then
exit 1
fi
else
xyz_info "$(xyz_t "Detected OS:" "检测到系统:") ${XYZ_OS_PRETTY} (${XYZ_OS_FAMILY})"
fi
}
# --- packages / commands ----------------------------------------------------
xyz_need_cmd() {
command -v "$1" >/dev/null 2>&1
}
xyz_pkg_install() {
xyz_detect_os
if (( $# == 0 )); then
return 0
fi
case "${XYZ_OS_FAMILY}" in
debian)
export DEBIAN_FRONTEND=noninteractive
apt-get update -y || xyz_warn "$(xyz_t "apt-get update reported errors (a repository may be unreachable); trying to install anyway." "apt-get update 报错(可能有软件源无法访问),仍尝试安装。")"
apt-get install -y --no-install-recommends "$@"
;;
rhel)
if xyz_need_cmd dnf; then
dnf install -y "$@"
elif xyz_need_cmd yum; then
yum install -y "$@"
else
xyz_error "$(xyz_t "Neither dnf nor yum is available." "找不到 dnf 或 yum。")"
exit 1
fi
;;
*)
xyz_error "$(xyz_t "Cannot install packages on this OS automatically." "无法在此系统上自动安装软件包。")"
exit 1
;;
esac
}
xyz_ensure_cmds() {
local pkg
local missing=()
for pkg in "$@"; do
if ! xyz_need_cmd "${pkg}"; then
missing+=("${pkg}")
fi
done
if ((${#missing[@]} > 0)); then
xyz_info "$(xyz_t "Installing missing commands:" "正在安装缺失命令:") ${missing[*]}"
xyz_pkg_install "${missing[@]}"
fi
}
# --- confirmation (safe with curl | bash) -----------------------------------
# A readable /dev/tty node is not enough: in containers or curl|bash without a
# controlling terminal, open() fails with ENXIO ("No such device or address").
# [[ -r /dev/tty ]] is true in that case and must not be used as the check.
xyz_open_tty() {
# Opens /dev/tty into XYZ_TTY_FD if it is a real terminal. Caller must close.
local fd
XYZ_TTY_FD=""
# Brace group so a failed open cannot leak "No such device or address".
if { exec {fd}<>/dev/tty; } 2>/dev/null && [[ -t "${fd}" ]]; then
XYZ_TTY_FD="${fd}"
return 0
fi
if [[ -n "${fd:-}" ]]; then
exec {fd}>&- 2>/dev/null || true
fi
return 1
}
xyz_close_tty() {
if [[ -n "${XYZ_TTY_FD:-}" ]]; then
exec {XYZ_TTY_FD}>&- 2>/dev/null || true
XYZ_TTY_FD=""
fi
}
xyz_confirm() {
local prompt="$1"
if [[ "${XYZ_YES}" == "1" ]]; then
xyz_info "$(xyz_t "Auto-confirmed (-y / YES=1):" "已自动确认(-y / YES=1):") ${prompt}"
return 0
fi
local reply=""
if xyz_open_tty; then
printf '%s [y/N] ' "${prompt}" >&"${XYZ_TTY_FD}"
# shellcheck disable=SC2162
read -r reply <&"${XYZ_TTY_FD}" || true
xyz_close_tty
else
xyz_warn "$(xyz_t \
"No TTY available for confirmation. Re-run with -y or YES=1 if you accept." \
"没有可用的终端来确认。若你已了解并同意,请加 -y 或 YES=1 重试。")"
return 1
fi
case "${reply}" in
y|Y|yes|YES|Yes) return 0 ;;
*) return 1 ;;
esac
}
# --- download helpers -------------------------------------------------------
# Create the temp dir in the main shell. xyz_tmpdir is usually called inside
# $(...), a subshell, so creating it (and its EXIT trap) there would delete the
# directory as soon as the subshell returned.
xyz_init_tmpdir() {
if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
XYZ_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/xyzsh.XXXXXX")"
# shellcheck disable=SC2064
trap "rm -rf '${XYZ_TMPDIR}'" EXIT
fi
}
xyz_tmpdir() {
if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
xyz_error "internal: temp dir not initialised"
exit 1
fi
printf '%s' "${XYZ_TMPDIR}"
}
xyz_fetch() {
# xyz_fetch URL DEST
local url="$1"
local dest="$2"
xyz_info "$(xyz_t "Downloading" "正在下载"): ${url}"
xyz_info "$(xyz_t "Saving to" "保存到"): ${dest}"
if xyz_need_cmd curl; then
curl -fsSL --connect-timeout 20 --max-time 180 --retry 2 --retry-delay 1 \
-o "${dest}" "${url}"
elif xyz_need_cmd wget; then
wget -q --timeout=60 -O "${dest}" "${url}"
else
xyz_error "$(xyz_t "Need curl or wget to download files." "需要 curl 或 wget 才能下载文件。")"
exit 1
fi
if [[ ! -s "${dest}" ]]; then
xyz_error "$(xyz_t "Download failed or file is empty:" "下载失败或文件为空:") ${url}"
exit 1
fi
if xyz_need_cmd sha256sum; then
xyz_info "SHA256: $(sha256sum "${dest}" | awk '{print $1}')"
elif xyz_need_cmd shasum; then
xyz_info "SHA256: $(shasum -a 256 "${dest}" | awk '{print $1}')"
fi
}
xyz_run_upstream() {
# xyz_run_upstream URL [args...]
# Downloads an official installer, shows the URL and a short preview, then
# runs it only after confirmation (unless -y / YES=1).
local url="$1"
shift
local dest
xyz_init_tmpdir
dest="$(xyz_tmpdir)/upstream.sh"
xyz_fetch "${url}" "${dest}"
chmod +x "${dest}"
xyz_info "$(xyz_t "Preview (first 20 lines):" "预览(前 20 行):")"
sed -n '1,20p' "${dest}" || true
printf '%s\n' "${XYZ_C_DIM}...${XYZ_C_RESET}"
# Default --dry-run: verify the download and preview, do not execute.
# Scripts that forward --dry-run to upstream (e.g. docker) set
# XYZ_FORWARD_DRY_RUN=1 so the official script still runs with that flag.
if [[ "${XYZ_DRY_RUN:-0}" == "1" && "${XYZ_FORWARD_DRY_RUN:-0}" != "1" ]]; then
xyz_info "$(xyz_t \
"Dry-run: official script downloaded and previewed; not executing it." \
"演练模式:已下载并预览官方脚本,不会执行。")"
return 0
fi
if ! xyz_confirm "$(xyz_t \
"Run this official upstream script?" \
"运行这份官方上游脚本?")"; then
xyz_error "$(xyz_t "Aborted by user." "已取消。")"
exit 1
fi
xyz_step "$(xyz_t "Executing upstream script" "正在执行上游脚本")"
# Never let the upstream script inherit our stdin: under `curl | bash` that
# is the rest of this very script. Give it the terminal if there is one,
# otherwise /dev/null. Interactive installers set XYZ_UPSTREAM_NEEDS_TTY=1.
if xyz_open_tty; then
local tty_fd="${XYZ_TTY_FD}"
XYZ_TTY_FD=""
bash "${dest}" "$@" <&"${tty_fd}"
local rc=$?
exec {tty_fd}>&- 2>/dev/null || true
return "${rc}"
fi
if [[ "${XYZ_UPSTREAM_NEEDS_TTY:-0}" == "1" ]]; then
xyz_error "$(xyz_t \
"This official installer is interactive and needs a terminal. Log in with SSH and run: bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})" \
"这个官方安装脚本需要交互,必须在终端里运行。请用 SSH 登录后执行:bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})")"
exit 1
fi
bash "${dest}" "$@" </dev/null
}
# --- small shared helpers ----------------------------------------------------
xyz_strip_ansi() {
sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' -e 's/\r//g'
}
# Effective SSH port: sshd -T (what sshd really uses) > config files > env > 22.
xyz_effective_ssh_port() {
local port=""
if xyz_need_cmd sshd; then
port="$(sshd -T 2>/dev/null | awk '$1=="port" {print $2; exit}' || true)"
fi
if [[ -z "${port}" && -d /etc/ssh/sshd_config.d ]]; then
port="$(cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | awk 'tolower($1)=="port" {print $2; exit}' || true)"
fi
if [[ -z "${port}" && -f /etc/ssh/sshd_config ]]; then
port="$(awk 'tolower($1)=="port" {print $2; exit}' /etc/ssh/sshd_config || true)"
fi
if [[ -z "${port}" && -n "${SSH_CONNECTION:-}" ]]; then
port="$(printf '%s' "${SSH_CONNECTION}" | awk '{print $4}')"
fi
printf '%s' "${port:-22}"
}
xyz_has_systemd() {
[[ -d /run/systemd/system ]] && xyz_need_cmd systemctl
}
# Back up a file once per run as FILE.xyz-bak-YYYYmmddHHMMSS and print the path.
xyz_backup_file() {
local f="$1"
local bak
bak="${f}.xyz-bak-$(date +%Y%m%d%H%M%S)"
if [[ -e "${f}" ]]; then
cp -a "${f}" "${bak}"
printf '%s' "${bak}"
fi
}
# --- misc -------------------------------------------------------------------
xyz_usage_exit() {
if [[ "${1:-}" != "" ]]; then
printf '%s\n' "$1"
fi
exit 0
}
# xyz.sh Ookla helper library (shared by speedtest, speedtest-cn, all).
# Official product : https://www.speedtest.net/apps/cli
# Official tarballs : https://install.speedtest.net/app/cli/ookla-speedtest-<ver>-linux-<arch>.tgz
# Served between lib/common.sh and the script body. No telemetry.
# shellcheck disable=SC2034
XYZ_OOKLA_LOADED=1
# Current tarball version published on https://www.speedtest.net/apps/cli
# (the binary itself reports 1.2.0.84). Override with OOKLA_CLI_VERSION.
OOKLA_CLI_VERSION="${OOKLA_CLI_VERSION:-1.2.0}"
OOKLA_CLI_BASE="https://install.speedtest.net/app/cli"
# Known SHA256 of the official 1.2.0 Linux tarballs (fetched from
# install.speedtest.net, matching the links on speedtest.net/apps/cli).
xyz_ookla_tarball_sha256() {
case "$1" in
x86_64) printf '%s' "5690596c54ff9bed63fa3732f818a05dbc2db19ad36ed68f21ca5f64d5cfeeb7" ;;
aarch64) printf '%s' "3953d231da3783e2bf8904b6dd72767c5c6e533e163d3742fd0437affa431bd3" ;;
armhf) printf '%s' "e45fcdebbd8a185553535533dd032d6b10bc8c64eee4139b1147b9c09835d08d" ;;
armel) printf '%s' "629a455a2879224bd0dbd4b36d8c721dda540717937e4660b4d2c966029466bf" ;;
i386) printf '%s' "9ff7e18dbae7ee0e03c66108445a2fb6ceea6c86f66482e1392f55881b772fe8" ;;
*) return 1 ;;
esac
}
xyz_ookla_arch() {
local machine
machine="$(uname -m)"
case "${machine}" in
x86_64|amd64) printf 'x86_64' ;;
aarch64|arm64) printf 'aarch64' ;;
armv7l|armv7hl|armhf) printf 'armhf' ;;
armv6l|armv6hl|armv5tel|armel) printf 'armel' ;;
arm)
# Official "arm32" / armel build.
printf 'armel' ;;
i386|i486|i586|i686|x86) printf 'i386' ;;
*)
xyz_error "$(xyz_t \
"Unsupported architecture: ${machine}. Official Linux tarballs exist for x86_64, aarch64, armhf, armel, and i386." \
"不支持的架构:${machine}。官方 Linux 包提供 x86_64、aarch64、armhf、armel 和 i386。")"
exit 1
;;
esac
}
xyz_ookla_url() {
local arch="$1"
printf '%s/ookla-speedtest-%s-linux-%s.tgz' \
"${OOKLA_CLI_BASE}" "${OOKLA_CLI_VERSION}" "${arch}"
}
xyz_ookla_version_line() {
local bin="$1"
local line=""
line="$("${bin}" --version 2>&1 || true)"
printf '%s\n' "${line%%$'\n'*}"
}
xyz_have_official_speedtest() {
local bin
bin="$(command -v speedtest 2>/dev/null || true)"
if [[ -z "${bin}" ]]; then
return 1
fi
# Official Ookla binary reports "Speedtest by Ookla" and accepts
# --accept-license. Distro Python packages often ship as speedtest-cli.
if "${bin}" --version 2>/dev/null | grep -q 'Speedtest by Ookla'; then
return 0
fi
return 1
}
# xyz_ookla_fetch_binary DIR
# Downloads the official tarball into DIR, verifies the pinned SHA256 and
# extracts it. Sets XYZ_OOKLA_EXTRACTED to the unpacked binary path.
xyz_ookla_fetch_binary() {
local dir="$1"
local arch url tarball expected actual
arch="$(xyz_ookla_arch)"
url="$(xyz_ookla_url "${arch}")"
xyz_info "$(xyz_t "Official tarball:" "官方压缩包:") ${url}"
tarball="${dir}/ookla-speedtest-${OOKLA_CLI_VERSION}-linux-${arch}.tgz"
xyz_fetch "${url}" "${tarball}"
expected=""
if [[ "${OOKLA_CLI_VERSION}" == "1.2.0" ]]; then
expected="$(xyz_ookla_tarball_sha256 "${arch}")"
fi
if [[ -n "${expected}" ]] && xyz_need_cmd sha256sum; then
actual="$(sha256sum "${tarball}" | awk '{print $1}')"
if [[ "${actual}" != "${expected}" ]]; then
xyz_error "$(xyz_t "SHA256 mismatch for the official tarball." "官方压缩包的 SHA256 不匹配。")"
xyz_error " expected: ${expected}"
xyz_error " actual: ${actual}"
exit 1
fi
xyz_info "$(xyz_t "SHA256 verified." "SHA256 已校验。")"
fi
mkdir -p "${dir}/extract"
tar -xzf "${tarball}" -C "${dir}/extract"
chmod +x "${dir}/extract/speedtest" 2>/dev/null || true
if [[ ! -x "${dir}/extract/speedtest" ]]; then
xyz_error "$(xyz_t "The tarball did not contain an executable 'speedtest' binary." "压缩包中没有可执行的 speedtest 文件。")"
exit 1
fi
XYZ_OOKLA_EXTRACTED="${dir}/extract/speedtest"
}
# xyz_ookla_ensure: make an official binary available as XYZ_OOKLA_BIN.
# Uses an installed official CLI if present. Otherwise downloads the official
# tarball; as root (after confirmation) installs it to /usr/local/bin,
# otherwise runs it from the temp dir for this run only.
xyz_ookla_ensure() {
XYZ_OOKLA_BIN=""
if xyz_have_official_speedtest; then
XYZ_OOKLA_BIN="$(command -v speedtest)"
xyz_info "$(xyz_t "Official Speedtest CLI found:" "已找到官方 Speedtest CLI:") $(xyz_ookla_version_line "${XYZ_OOKLA_BIN}")"
return 0
fi
if ! xyz_need_cmd tar; then
xyz_require_root
xyz_ensure_cmds tar
fi
xyz_init_tmpdir
xyz_ookla_fetch_binary "$(xyz_tmpdir)"
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
XYZ_OOKLA_BIN="${XYZ_OOKLA_EXTRACTED}"
return 0
fi
if xyz_is_root && xyz_confirm "$(xyz_t \
"Install the official Speedtest CLI to /usr/local/bin/speedtest?" \
"将官方 Speedtest CLI 安装到 /usr/local/bin/speedtest?")"; then
install -m 0755 "${XYZ_OOKLA_EXTRACTED}" /usr/local/bin/speedtest
XYZ_OOKLA_BIN="/usr/local/bin/speedtest"
xyz_info "$(xyz_t "Installed:" "已安装:") ${XYZ_OOKLA_BIN}"
else
XYZ_OOKLA_BIN="${XYZ_OOKLA_EXTRACTED}"
xyz_info "$(xyz_t "Using a temporary copy for this run only." "本次仅使用临时副本。")"
fi
}
# Print Ookla's terms once and ask before passing the accept flags.
xyz_ookla_accept() {
xyz_info "$(xyz_t \
"Ookla's official CLI will contact Ookla servers. Ookla has its own license and privacy terms:" \
"接下来会运行 Ookla 官方 CLI,它会连接 Ookla 服务器。Ookla 有自己的许可与隐私条款:")"
printf ' https://www.speedtest.net/about/privacy\n'
printf ' https://www.speedtest.net/terms\n'
xyz_confirm "$(xyz_t \
"Accept Ookla's terms for this run (--accept-license --accept-gdpr)?" \
"本次运行接受 Ookla 条款(--accept-license --accept-gdpr)?")"
}
# xyz.sh terminal report helpers: frames, colored badges, bar charts.
# Shared by speedtest-cn, route, unlock and all. Pure bash, no telemetry.
#
# Colors: on only when stdout is a terminal (or XYZ_COLOR=always). Off with
# --no-color, NO_COLOR=1 or XYZ_COLOR=never.
# Glyphs: Unicode box drawing when the locale is UTF-8, otherwise ASCII
# (force with XYZ_ASCII=1 or XYZ_UTF8=1). Frames are 78 columns wide.
# shellcheck disable=SC2034
XYZ_UI_LOADED=1
XYZ_UI_WIDTH=78
xyz_ui_init() {
local want="${XYZ_COLOR:-auto}"
[[ "${want}" == "auto" && -n "${NO_COLOR:-}" ]] && want="never"
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && want="never"
XYZ_UI_COLOR=0
case "${want}" in
always) XYZ_UI_COLOR=1 ;;
never) XYZ_UI_COLOR=0 ;;
*) [[ -t 1 ]] && XYZ_UI_COLOR=1 ;;
esac
if [[ "${XYZ_UI_COLOR}" == "1" ]]; then
U_X=$'\033[0m'; U_B=$'\033[1m'; U_D=$'\033[2m'
U_RED=$'\033[31m'; U_G=$'\033[32m'; U_Y=$'\033[33m'
U_BL=$'\033[34m'; U_M=$'\033[35m'; U_C=$'\033[36m'
else
U_X=""; U_B=""; U_D=""; U_RED=""; U_G=""; U_Y=""; U_BL=""; U_M=""; U_C=""
fi
XYZ_UI_UTF8=0
if [[ "${XYZ_ASCII:-0}" != "1" ]]; then
case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
*[Uu][Tt][Ff]-8*|*[Uu][Tt][Ff]8*) XYZ_UI_UTF8=1 ;;
*) XYZ_UI_UTF8=0 ;;
esac
[[ "${XYZ_UTF8:-0}" == "1" ]] && XYZ_UI_UTF8=1
fi
if [[ "${XYZ_UI_UTF8}" == "1" ]]; then
G_H="─"; G_V="│"; G_TL="╭"; G_TR="╮"; G_BL="╰"; G_BR="╯"; G_LT="├"; G_RT="┤"
G_FULL="█"; G_EMPTY="░"; G_OK="✔"; G_BAD="✘"; G_UNK="?"; G_DOT="●"; G_STAR="★"
G_DOWN="↓"; G_UP="↑"; G_WARN="⚠"; G_MID="·"; G_TIMES="×"
else
G_H="-"; G_V="|"; G_TL="+"; G_TR="+"; G_BL="+"; G_BR="+"; G_LT="+"; G_RT="+"
G_FULL="#"; G_EMPTY="."; G_OK="+"; G_BAD="x"; G_UNK="?"; G_DOT="*"; G_STAR="*"
G_DOWN="v"; G_UP="^"; G_WARN="!"; G_MID="-"; G_TIMES="x"
fi
}
# Display width of a string: ANSI codes removed, CJK counted as 2 columns.
xyz_ui_w() {
local s="$1" pre rest
while [[ "${s}" == *$'\033['* ]]; do
pre="${s%%$'\033['*}"
rest="${s#*$'\033['}"
rest="${rest#*m}"
s="${pre}${rest}"
done
local LC_ALL=C
local a="${s//[^ -~]/}"
local one="${s//[^$'\xc2'-$'\xdf'$'\xe2']/}"
local two="${s//[^$'\xe3'-$'\xef']/}"
printf '%d' $(( ${#a} + ${#one} + 2 * ${#two} ))
}
xyz_ui_rep() {
# xyz_ui_rep CHAR N
local out="" n="$2"
while (( n > 0 )); do out="${out}$1"; n=$((n - 1)); done
printf '%s' "${out}"
}
xyz_ui_pad() {
# xyz_ui_pad TEXT WIDTH (left aligned, never truncates)
local w
w="$(xyz_ui_w "$1")"
printf '%s%s' "$1" "$(xyz_ui_rep ' ' $(( $2 - w )))"
}
xyz_ui_lpad() {
# xyz_ui_lpad TEXT WIDTH (right aligned)
local w
w="$(xyz_ui_w "$1")"
printf '%s%s' "$(xyz_ui_rep ' ' $(( $2 - w )))" "$1"
}
xyz_ui_top() {
local title=" $1 " w
w="$(xyz_ui_w "${title}")"
printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_TL}${G_H}" "${U_X}${U_B}" "${title}" "${U_X}${U_C}" \
"$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_TR}" "${U_X}"
}
xyz_ui_sep() {
local label="${1:-}" w=0
if [[ -n "${label}" ]]; then
label=" ${label} "
w="$(xyz_ui_w "${label}")"
printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_LT}${G_H}" "${U_X}${U_B}" "${label}" "${U_X}${U_C}" \
"$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_RT}" "${U_X}"
else
printf '%s%s%s\n' "${U_C}" "${G_LT}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_RT}" "${U_X}"
fi
}
xyz_ui_row() {
local text="$1" inner=$(( XYZ_UI_WIDTH - 4 )) plain
if (( $(xyz_ui_w "${text}") > inner )); then
# too wide: drop colors and cut to fit, ending with "~"
plain="$(printf '%s' "${text}" | sed 's/\x1b\[[0-9;]*m//g')"
plain="${plain:0:inner}"
while (( $(xyz_ui_w "${plain}") > inner - 1 )); do plain="${plain%?}"; done
text="${plain}~"
fi
printf '%s%s%s %s %s%s%s\n' "${U_C}" "${G_V}" "${U_X}" "$(xyz_ui_pad "${text}" "${inner}")" "${U_C}" "${G_V}" "${U_X}"
}
xyz_ui_bottom() {
printf '%s%s%s\n' "${U_C}" "${G_BL}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_BR}" "${U_X}"
}
xyz_ui_bar() {
# xyz_ui_bar VALUE MAX WIDTH [COLOR] -> filled/empty bar (at least 1 cell if VALUE > 0)
local n color="${4:-${U_G}}"
n="$(awk -v v="${1:-0}" -v m="${2:-0}" -v w="$3" 'BEGIN {
if (m <= 0 || v <= 0) { print 0; exit }
n = int(v / m * w + 0.5); if (n < 1) n = 1; if (n > w) n = w; print n }')"
printf '%s%s%s%s%s' "${color}" "$(xyz_ui_rep "${G_FULL}" "${n}")" "${U_D}" "$(xyz_ui_rep "${G_EMPTY}" $(( $3 - n )))" "${U_X}"
}
xyz_ui_usage() {
# xyz_ui_usage PERCENT WIDTH -> colored usage bar + percent (red > 90, yellow > 75)
local p="${1:-0}" color="${U_G}"
p="${p%%.*}"; [[ "${p}" =~ ^[0-9]+$ ]] || p=0
(( p > 75 )) && color="${U_Y}"
(( p > 90 )) && color="${U_RED}"
printf '%s %s%3d%%%s' "$(xyz_ui_bar "${p}" 100 "$2" "${color}")" "${color}" "${p}" "${U_X}"
}
xyz_ui_badge() {
# xyz_ui_badge yes|no|partial|unknown|ok|skip [TEXT]
local txt="${2:-}"
case "$1" in
yes|ok) printf '%s%s %s%s' "${U_G}" "${G_OK}" "${txt:-$(xyz_t "yes" "解锁")}" "${U_X}" ;;
no|fail) printf '%s%s %s%s' "${U_RED}" "${G_BAD}" "${txt:-$(xyz_t "no" "不可用")}" "${U_X}" ;;
partial) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "partial" "部分")}" "${U_X}" ;;
skip) printf '%s%s %s%s' "${U_D}" "-" "${txt:-$(xyz_t "skipped" "跳过")}" "${U_X}" ;;
*) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "unknown" "未知")}" "${U_X}" ;;
esac
}
xyz_ui_line_class() {
# premium | good | ordinary | other
case "$1" in
CN2-GIA|9929/CUII|9929|CMIN2) printf 'premium' ;;
CN2-GT) printf 'good' ;;
163|4837|CMI|CMNET) printf 'ordinary' ;;
*) printf 'other' ;;
esac
}
xyz_ui_linetags() {
# xyz_ui_linetags "CN2-GIA 9929/CUII" -> colored tags
local t out="" c
for t in $1; do
case "$(xyz_ui_line_class "${t}")" in
premium) c="${U_G}${U_B}" ;;
good) c="${U_C}" ;;
ordinary) c="${U_Y}" ;;
*) c="${U_D}" ;;
esac
out="${out}${out:+ }${c}${t}${U_X}"
done
printf '%s' "${out:-${U_D}?${U_X}}"
}
xyz_ui_fill() {
# Replace empty TSV fields with "-" (bash read collapses consecutive tabs).
awk -F'\t' -v OFS='\t' '{ for (i = 1; i <= NF; i++) if ($i == "") $i = "-"; print }' "$1"
}
xyz_ui_carrier() {
case "$1" in
nearest) xyz_t "Local" "本地" ;;
ct) xyz_t "Telecom" "电信" ;;
cu) xyz_t "Unicom" "联通" ;;
cm) xyz_t "Mobile" "移动" ;;
custom) xyz_t "Custom" "自定义" ;;
hk) xyz_t "HK" "香港" ;;
tokyo) xyz_t "Tokyo" "东京" ;;
singapore) xyz_t "SGP" "新加坡" ;;
us-west) xyz_t "US West" "美西" ;;
*) printf '%s' "$1" ;;
esac
}
xyz_ui_carrier_color() {
case "$1" in
ct|tokyo) printf '%s' "${U_BL}" ;;
cu) printf '%s' "${U_RED}" ;;
cm|singapore) printf '%s' "${U_G}" ;;
hk) printf '%s' "${U_Y}" ;;
*) printf '%s' "${U_M}" ;;
esac
}
xyz_ui_fmt_num() {
# 2 decimals below 1000, 1 decimal above
awk -v v="${1:-0}" 'BEGIN { if (v >= 1000) printf "%.1f", v; else printf "%.2f", v }'
}
# xyz_ui_speed_rows FILE
# FILE: carrier id label down up ping loss status [engine] [note] (TSV, Mbps / ms)
xyz_ui_speed_rows() {
local f="$1" max carrier id label down up lat loss status engine note head
[[ -s "${f}" ]] || return 0
max="$(awk -F'\t' '$8=="ok" { if ($4+0 > m) m = $4+0; if ($5+0 > m) m = $5+0 } END { print m+0 }' "${f}")"
while IFS=$'\t' read -r carrier id label down up lat loss status engine note; do
[[ "${engine}" == "-" ]] && engine=""
head="$(xyz_ui_carrier_color "${carrier}")$(xyz_ui_pad "$(xyz_ui_carrier "${carrier}")" 8)${U_X}${U_B}${label}${U_X}${U_D} #${id}${engine:+ ${G_MID} ${engine}}${U_X}"
xyz_ui_row "${head}"
if [[ "${status}" == "ok" ]]; then
local pc="${U_G}"
lat="${lat:-?}"
awk -v p="${lat}" 'BEGIN { exit !(p+0 > 150) }' 2>/dev/null && pc="${U_Y}"
awk -v p="${lat}" 'BEGIN { exit !(p+0 > 250) }' 2>/dev/null && pc="${U_RED}"
xyz_ui_row " ${G_DOWN} $(xyz_ui_bar "${down}" "${max}" 14 "${U_G}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${down}")" 7) Mbps ${G_UP} $(xyz_ui_bar "${up}" "${max}" 14 "${U_C}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${up}")" 7) Mbps ${pc}$(xyz_ui_lpad "${lat}" 6)ms${U_X}"
else
[[ "${note}" == "-" ]] && note=""
xyz_ui_row " $(xyz_ui_badge skip "$(xyz_t "skipped" "跳过")")${U_D}${note:+ ${G_MID} ${note:0:62}}${U_X}"
fi
done < <(xyz_ui_fill "${f}")
}
xyz_ui_speed_table() {
# xyz_ui_speed_table FILE TITLE
xyz_ui_top "$2"
xyz_ui_speed_rows "$1"
xyz_ui_bottom
}
# Route grid. FILE: target label kind hops [key] (key like bj-ct)
xyz_ui_route_key() {
case "$1" in
219.141.140.10) printf 'bj-ct' ;; 202.106.195.68) printf 'bj-cu' ;; 221.179.155.161) printf 'bj-cm' ;;
202.96.209.133) printf 'sh-ct' ;; 210.22.97.1) printf 'sh-cu' ;; 211.136.112.200) printf 'sh-cm' ;;
58.60.188.222) printf 'gz-ct' ;; 210.21.196.6) printf 'gz-cu' ;; 120.196.165.24) printf 'gz-cm' ;;
*) printf '' ;;
esac
}
xyz_ui_route_rows() {
local f="$1" target label kind hops key city c cell line extra=0
local -A K=()
[[ -s "${f}" ]] || return 0
while IFS=$'\t' read -r target label kind hops key; do
[[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
if [[ -n "${key}" ]]; then
K["${key}"]="${kind}"
else
extra=1
fi
done < <(xyz_ui_fill "${f}")
if (( ${#K[@]} > 0 )); then
xyz_ui_row "${U_D}$(xyz_ui_pad "" 10)$(xyz_ui_pad "$(xyz_ui_carrier ct)" 21)$(xyz_ui_pad "$(xyz_ui_carrier cu)" 21)$(xyz_ui_carrier cm)${U_X}"
for city in bj sh gz; do
case "${city}" in
bj) line="$(xyz_ui_pad "$(xyz_t "Beijing" "北京")" 10)" ;;
sh) line="$(xyz_ui_pad "$(xyz_t "Shanghai" "上海")" 10)" ;;
*) line="$(xyz_ui_pad "$(xyz_t "Guangzhou" "广州")" 10)" ;;
esac
for c in ct cu cm; do
cell="${K[${city}-${c}]-}"
[[ "${cell}" == *" ("* || "${cell}" == unknown* || "${cell}" == 未识别* ]] && cell="?"
if [[ -z "${cell}" ]]; then cell="${U_D}-${U_X}"; else cell="$(xyz_ui_linetags "${cell}")"; fi
line="${line}$(xyz_ui_pad "${cell}" 21)"
done
xyz_ui_row "${line}"
done
fi
if [[ "${extra}" == "1" ]]; then
while IFS=$'\t' read -r target label kind hops key; do
[[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
[[ -n "${key}" ]] && continue
xyz_ui_row "$(xyz_ui_pad "${label:-${target}}" 22) $(xyz_ui_linetags "${kind}")"
done < <(xyz_ui_fill "${f}")
fi
xyz_ui_row "${U_D}$(xyz_t "Legend:" "图例:")${U_X} ${U_G}${U_B}$(xyz_t "premium" "优质")${U_X} CN2-GIA/9929/CMIN2 ${U_C}CN2-GT${U_X} ${U_Y}$(xyz_t "ordinary" "普通")${U_X} 163/4837/CMI"
}
xyz_ui_route_score() {
# prints 0-100 (average best class per target) or nothing
local f="$1" kind t best n=0 sum=0 cls
[[ -s "${f}" ]] || return 0
while IFS=$'\t' read -r _ _ kind _ _; do
best=0
for t in ${kind}; do
cls="$(xyz_ui_line_class "${t}")"
case "${cls}" in premium) best=100 ;; good) (( best < 75 )) && best=75 ;; ordinary) (( best < 40 )) && best=40 ;; *) : ;; esac
done
n=$((n + 1)); sum=$((sum + best))
done < "${f}"
(( n > 0 )) && printf '%d' $(( sum / n ))
}
# Unlock grid. FILE: service status region note
xyz_ui_unlock_rows() {
local f="$1" svc st reg note cells=() i cell
[[ -s "${f}" ]] || return 0
while IFS=$'\t' read -r svc st reg note; do
cell="$(xyz_ui_badge "${st}" "$(xyz_ui_pad "${svc}" 16)")"
[[ "${reg}" != "-" ]] && cell="${cell} ${U_D}${reg:0:12}${U_X}"
cells+=("${cell}")
done < <(xyz_ui_fill "${f}")
for (( i = 0; i < ${#cells[@]}; i += 2 )); do
xyz_ui_row "$(xyz_ui_pad "${cells[$i]}" 37)${cells[$((i + 1))]-}"
done
}
xyz_ui_unlock_count() {
# prints "yes total"
awk -F'\t' '{ n++; if ($2 == "yes") y++ } END { printf "%d %d\n", y, n }' "$1"
}
xyz_ui_init
XYZ_NAME="cn"
# xyz.sh/speedtest-cn — experimental China three-network (电信 / 联通 / 移动)
# speed test. Kept for compatibility; xyz.sh/all no longer runs this.
# Mainland results are often unavailable from abroad and are not promised.
#
# Why several engines: Ookla's CLI resolves `-s <id>` through Ookla's server
# directory, and Ookla no longer lists mainland China servers to clients
# abroad, so `speedtest -s 5396` fails with "No servers defined" even though
# the server itself is up. This script therefore tries, per carrier:
# 1. Ookla CLI `speedtest -s <id>` (works when the ID is listed)
# 2. speedtest-go --custom-url=<host> (talks to the same Ookla server
# directly, no directory lookup; official release, SHA256-pinned)
# 3. HTTP test against speedtest.cn nodes (curl: /hello, /download, /upload)
# Candidate nodes come from the built-in list and the daily-updated public
# lists https://github.com/spiritLHLS/speedtest.net-CN-ID and
# https://github.com/spiritLHLS/speedtest.cn-CN-ID (thanks!). Unreachable
# nodes are skipped after a 5 s probe; each carrier has a time cap.
#
# Usage:
# curl -fsSL xyz.sh/speedtest-cn | bash -s -- -y
# curl -fsSL xyz.sh/speedtest-cn | bash -s -- --list
# curl -fsSL xyz.sh/speedtest-cn | bash -s -- --region east -y
# curl -fsSL xyz.sh/speedtest-cn | bash -s -- --all-nodes -y
# curl -fsSL xyz.sh/speedtest-cn | bash -s -- --servers 5396,24447 -y
# curl -fsSL xyz.sh/speedtest-cn | bash -s -- --dry-run
#
# Supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux. Root is optional.
# No telemetry is added by this wrapper.
set -euo pipefail
if [[ -z "${XYZ_COMMON_LOADED:-}" ]]; then
_xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${_xyz_dir}/lib/common.sh" ]]; then
# shellcheck source=lib/common.sh
source "${_xyz_dir}/lib/common.sh"
else
echo "xyz.sh: missing lib/common.sh" >&2
exit 1
fi
fi
if [[ -z "${XYZ_OOKLA_LOADED:-}" ]]; then
_xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/ookla.sh
source "${_xyz_dir}/lib/ookla.sh"
fi
if [[ -z "${XYZ_UI_LOADED:-}" ]]; then
_xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/report.sh
source "${_xyz_dir}/lib/report.sh"
fi
XYZ_NAME="${XYZ_NAME:-speedtest-cn}"
[[ "${XYZ_NAME}" == "unknown" ]] && XYZ_NAME="speedtest-cn"
XYZ_TITLE="$(xyz_t "China three-network speed test (experimental)" "三网测速(实验性)")"
XYZ_UPSTREAM="https://www.speedtest.net/apps/cli"
LIVE_NET_BASE="${XYZ_CN_LIVE_NET:-https://raw.githubusercontent.com/spiritLHLS/speedtest.net-CN-ID/main}"
LIVE_CN_BASE="${XYZ_CN_LIVE_CN:-https://raw.githubusercontent.com/spiritLHLS/speedtest.cn-CN-ID/main}"
GO_VERSION="1.8.3"
GO_BASE="https://github.com/showwin/speedtest-go/releases/download/v${GO_VERSION}"
# id|carrier|region|name-zh|name-en|host:port (speedtest.net servers)
CN_NODES=(
"5396|ct|east|江苏苏州 电信5G|Suzhou, Jiangsu (CT 5G)|4gsuzhou1.speedtest.jsinfo.net:8080"
"36663|ct|east|江苏镇江 电信5G|Zhenjiang, Jiangsu (CT 5G)|5gzhenjiang.speedtest.jsinfo.net:8080"
"59387|ct|east|浙江宁波 电信|Ningbo, Zhejiang (CT)|cesu-nb.zjtelecom.com.cn:8080"
"24447|cu|east|上海 联通5G|Shanghai (CU 5G)|mobile.shunicomtest.com:8080"
"43752|cu|north|北京 联通|Beijing (CU)|beijing.unicomtest.com:8080"
"16204|cm|east|江苏苏州 移动|Suzhou, Jiangsu (CM)|speedtest.jsqiuying.com:8080"
)
xyz_init "$@"
REGION="all"
ALL_NODES=0
DO_LIST=0
OFFLINE=0
NEAREST=0
SERVERS=""
CARRIERS="ct,cu,cm"
MAX_TRIES=5
TIME_CAP=180
HTTP_SECS=10
i=0
while [[ $i -lt ${#XYZ_ARGS[@]} ]]; do
arg="${XYZ_ARGS[$i]}"
case "${arg}" in
--list) DO_LIST=1 ;;
--all-nodes) ALL_NODES=1 ;;
--offline) OFFLINE=1 ;;
--nearest) NEAREST=1 ;;
--region) i=$((i + 1)); REGION="${XYZ_ARGS[$i]:-all}" ;;
--region=*) REGION="${arg#--region=}" ;;
--servers) i=$((i + 1)); SERVERS="${XYZ_ARGS[$i]:-}" ;;
--servers=*) SERVERS="${arg#--servers=}" ;;
--carrier|--carriers) i=$((i + 1)); CARRIERS="${XYZ_ARGS[$i]:-ct,cu,cm}" ;;
--carrier=*|--carriers=*) CARRIERS="${arg#*=}" ;;
--max-tries) i=$((i + 1)); MAX_TRIES="${XYZ_ARGS[$i]:-5}" ;;
--max-tries=*) MAX_TRIES="${arg#*=}" ;;
--time-cap) i=$((i + 1)); TIME_CAP="${XYZ_ARGS[$i]:-180}" ;;
--time-cap=*) TIME_CAP="${arg#*=}" ;;
--help|-h) XYZ_HELP=1 ;;
*)
xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
exit 1
;;
esac
i=$((i + 1))
done
case "${REGION}" in
all|east|north|south|west) : ;;
*)
xyz_error "$(xyz_t "--region must be all, east, north, south, or west." "--region 只能是 all、east、north、south 或 west。")"
exit 1
;;
esac
if ! [[ "${MAX_TRIES}" =~ ^[0-9]+$ && "${TIME_CAP}" =~ ^[0-9]+$ ]]; then
xyz_error "$(xyz_t "--max-tries and --time-cap take whole numbers." "--max-tries 和 --time-cap 需要整数。")"
exit 1
fi
if [[ "${XYZ_HELP:-}" == "1" ]]; then
cat <<HINT
$(xyz_t "Usage:" "用法:")
curl -fsSL xyz.sh/speedtest-cn | bash -s -- -y
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --list
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --region east -y
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --carrier ct,cm -y
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --all-nodes -y
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --servers 5396,24447 -y
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --nearest -y
curl -fsSL xyz.sh/speedtest-cn | bash -s -- --dry-run
$(xyz_t "Experimental: kept for compatibility. xyz.sh/all no longer runs this. Mainland servers often cannot be selected from abroad; this site does not promise China speed results. Prefer xyz.sh/speedtest --overseas." "实验性脚本,仅为兼容保留;xyz.sh/all 已不再调用。从境外经常无法选中国内服务器,本站不承诺能测出国内速度。日常请用 xyz.sh/speedtest --overseas。")
$(xyz_t "Default: one working node per carrier. Each carrier tries up to --max-tries nodes (5) within --time-cap seconds (180)." "默认每个运营商测一个可用节点;每个运营商最多尝试 --max-tries 个节点(5),总时长不超过 --time-cap 秒(180)。")
$(xyz_t "Engines, in order: Ookla CLI (-s ID) -> speedtest-go (--custom-url, same Ookla server, no directory lookup) -> HTTP test on speedtest.cn nodes." "测速引擎依次为:Ookla CLI(-s ID)→ speedtest-go(--custom-url 直连同一台 Ookla 服务器,不查目录)→ speedtest.cn 节点 HTTP 测速。")
$(xyz_t "--nearest also tests the server Ookla picks automatically (usually local to the VPS)." "--nearest 额外测试 Ookla 自动选择的最近节点(通常在 VPS 本地)。")
$(xyz_t "--region east|north|south|west filters nodes. --all-nodes tests every candidate. --offline uses built-in nodes only." "--region east|north|south|west 按地区筛选;--all-nodes 测试全部候选节点;--offline 只用内置节点。")
$(xyz_t "--no-color disables colors (also NO_COLOR=1)." "--no-color 关闭颜色(也可设置 NO_COLOR=1)。")
$(xyz_t "Node lists:" "节点来源:") https://github.com/spiritLHLS/speedtest.net-CN-ID https://github.com/spiritLHLS/speedtest.cn-CN-ID
HINT
exit 0
fi
node_field() { printf '%s' "$1" | cut -d'|' -f"$2"; }
node_name() { xyz_t "$(node_field "$1" 5)" "$(node_field "$1" 4)"; }
print_nodes() {
local n
printf ' %-7s %-8s %-6s %s\n' "ID" "$(xyz_t "ISP" "运营商")" "$(xyz_t "Area" "地区")" "$(xyz_t "Node" "节点")"
for n in "${CN_NODES[@]}"; do
printf ' %-7s %-8s %-6s %s\n' "$(node_field "${n}" 1)" "$(xyz_ui_carrier "$(node_field "${n}" 2)")" \
"$(node_field "${n}" 3)" "$(node_name "${n}")"
done
}
if [[ "${DO_LIST}" == "1" ]]; then
xyz_step "$(xyz_t "Built-in China nodes (speedtest.net server IDs)" "内置国内节点(speedtest.net 服务器 ID)")"
print_nodes
printf '\n%s %s\n' "$(xyz_t "More nodes are fetched at run time from:" "运行时还会获取在线节点:")" "${LIVE_CN_BASE}"
exit 0
fi
xyz_prefer_root
xyz_require_supported_os
if [[ -n "${SERVERS}" ]]; then
CN_NODES=()
IFS=',' read -r -a _ids <<< "${SERVERS}"
for _id in "${_ids[@]}"; do
if ! [[ "${_id}" =~ ^[0-9]+$ ]]; then
xyz_error "$(xyz_t "--servers takes numeric speedtest.net server IDs, comma separated." "--servers 需要逗号分隔的数字服务器 ID。")"
exit 1
fi
CN_NODES+=("${_id}|custom|all|自定义 ${_id}|Custom ${_id}|")
done
CARRIERS="custom"
ALL_NODES=1
OFFLINE=1
fi
xyz_step "$(xyz_t "Built-in nodes" "内置节点")"
print_nodes
xyz_ookla_ensure
xyz_init_tmpdir
TMP="$(xyz_tmpdir)"
# ---- speedtest-go (official release, SHA256-pinned) --------------------------
go_asset_arch() {
case "$(uname -m)" in
x86_64|amd64) printf 'x86_64' ;;
aarch64|arm64) printf 'arm64' ;;
armv7l|armv7hl|armhf) printf 'armv7' ;;
armv6l) printf 'armv6' ;;
i386|i486|i586|i686) printf 'i386' ;;
*) return 1 ;;
esac
}
go_sha256() {
case "$1" in
x86_64) printf 'c55caae22927cd719a2f8c0bef96ecf74b5fe8ca919ff9200d76328ae5de9477' ;;
arm64) printf '48f51504548d76d5dc2ce7f72e713bd3b4e35554028b7131467060688754c657' ;;
armv7) printf '29d6f7b1038d765970c4f0a24e2e26a80b2041f58a7ce3c2e1b4318317c0fbe0' ;;
armv6) printf '395fd65509af7297b62d8942a323483b48c176cd9404889cc499870fe291d596' ;;
i386) printf '7b9641833a94c5937d9872f589e017c0fa5e3ef9c4a36a4d88f3b5c7355dc053' ;;
*) return 1 ;;
esac
}
GO_BIN=""
GO_TRIED=0
# fetch_go [noconfirm] -> sets GO_BIN; 1 when unavailable
fetch_go() {
[[ -n "${GO_BIN}" ]] && return 0
[[ "${GO_TRIED}" == "1" ]] && return 1
GO_TRIED=1
local arch asset want got dir="${TMP}/speedtest-go"
arch="$(go_asset_arch)" || { xyz_warn "$(xyz_t "speedtest-go has no build for" "speedtest-go 没有对应架构:") $(uname -m)"; return 1; }
asset="speedtest-go_${GO_VERSION}_Linux_${arch}.tar.gz"
want="$(go_sha256 "${arch}")"
xyz_info "$(xyz_t "speedtest-go (official release, run from a temp dir, nothing installed):" "speedtest-go(官方发布版,仅在临时目录运行,不安装):") ${GO_BASE}/${asset}"
if [[ "${1:-}" != "noconfirm" ]] && ! xyz_confirm "$(xyz_t "Download speedtest-go to test servers Ookla's CLI cannot select?" "下载 speedtest-go 来测试 Ookla CLI 无法选中的节点?")"; then
return 1
fi
mkdir -p "${dir}"
if ! curl -fsSL --connect-timeout 15 --max-time 120 -o "${dir}/${asset}" "${GO_BASE}/${asset}"; then
xyz_warn "$(xyz_t "Could not download speedtest-go." "无法下载 speedtest-go。")"
return 1
fi
got="$(sha256sum "${dir}/${asset}" | awk '{print $1}')"
if [[ "${got}" != "${want}" ]]; then
xyz_warn "$(xyz_t "speedtest-go SHA256 mismatch, not using it:" "speedtest-go SHA256 不匹配,不使用:") ${got}"
return 1
fi
xyz_info "$(xyz_t "SHA256 verified:" "SHA256 已校验:") ${got}"
tar -xzf "${dir}/${asset}" -C "${dir}" speedtest-go 2>/dev/null || tar -xzf "${dir}/${asset}" -C "${dir}"
if [[ ! -x "${dir}/speedtest-go" ]]; then
xyz_warn "$(xyz_t "The speedtest-go archive had no binary." "speedtest-go 压缩包里没有可执行文件。")"
return 1
fi
GO_BIN="${dir}/speedtest-go"
}
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
if [[ "${CARRIERS}" != "none" ]]; then
fetch_go noconfirm || true
fi
xyz_info "$(xyz_t \
"Dry-run: official CLI (and speedtest-go) downloaded and verified; node list printed; no test was run." \
"演练模式:已下载并校验官方 CLI(及 speedtest-go),已列出节点,未进行测速。")"
exit 0
fi
if ! xyz_ookla_accept; then
xyz_error "$(xyz_t "Aborted." "已取消。")"
exit 1
fi
RESULTS="${TMP}/cn-results.tsv"
: > "${RESULTS}"
TRIED_HOSTS=" "
mbps_bytes() { awk -v b="${1:-0}" 'BEGIN { printf "%.2f", b * 8 / 1000000 }'; }
# Collapse a CLI error into one readable line (all JSON "message" fields, or the raw text).
explain_error() {
local f="$1" msgs
msgs="$(grep -o '"message":"[^"]*"' "${f}" 2>/dev/null | cut -d'"' -f4 | paste -sd ';' - || true)"
if [[ -n "${msgs}" ]]; then
printf '%s' "${msgs}"
else
tr '\n\r' ' ' < "${f}" | sed 's/ */ /g; s/^ //; s/ $//'
fi
}
host_key() {
# host_key URL|HOST:PORT -> host:port without Ookla's ".prod.hosts" alias suffix
local h="${1#*://}" port=""
h="${h%%/*}"
[[ "${h}" == *:* ]] && port="${h##*:}" && h="${h%:*}"
printf '%s:%s' "${h%.prod.hosts.ooklaserver.net}" "${port}"
}
record() {
# record carrier id label down up ping loss status engine note
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@" >> "${RESULTS}"
}
skip_node() {
# skip_node carrier id label engine reason
xyz_warn "$(xyz_t "Node failed, skipping:" "节点不可用,已跳过:") $3 (#$2, $4): $5"
record "$1" "$2" "$3" "-" "-" "-" "-" "skip" "$4" "$(printf '%s' "$5" | tr '\t' ' ' | cut -c1-160)"
}
probe() {
# probe URL -> 0 when the server answers HTTP within 5 s
local code
code="$(curl -s -o /dev/null -m 5 --connect-timeout 4 -w '%{http_code}' "$1" 2>/dev/null || true)"
[[ "${code}" =~ ^[234][0-9][0-9]$ ]]
}
run_ookla() {
# run_ookla ID CARRIER LABEL -> 0 ok; 1 failed (OOKLA_ERR set)
local id="$1" carrier="$2" label="$3"
local out="${TMP}/st-${id}.json" err="${TMP}/st-${id}.err" runner=()
xyz_need_cmd timeout && runner=(timeout 120)
local sel=(-s "${id}")
[[ "${id}" == "auto" ]] && sel=()
printf ' %s %s (#%s, Ookla CLI)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}"
OOKLA_ERR=""
if "${runner[@]+"${runner[@]}"}" "${XYZ_OOKLA_BIN}" --accept-license --accept-gdpr \
"${sel[@]+"${sel[@]}"}" -f json -p no >"${out}" 2>"${err}" \
&& grep -q '"type":"result"' "${out}"; then
local lat down up loss
lat="$(grep -o '"latency":[0-9.]*' "${out}" | head -1 | cut -d: -f2)"
down="$(grep -o '"download":{"bandwidth":[0-9]*' "${out}" | grep -o '[0-9]*$')"
up="$(grep -o '"upload":{"bandwidth":[0-9]*' "${out}" | grep -o '[0-9]*$')"
loss="$(grep -o '"packetLoss":[0-9.]*' "${out}" | head -1 | cut -d: -f2)"
if [[ "${id}" == "auto" ]]; then
local sname sid
sname="$(grep -o '"server":{[^}]*' "${out}" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)"
sid="$(grep -o '"server":{"id":[0-9]*' "${out}" | grep -o '[0-9]*$')"
label="${label}: ${sname:-?}"
id="${sid:-auto}"
fi
record "${carrier}" "${id}" "${label}" "$(mbps_bytes "${down}")" "$(mbps_bytes "${up}")" "${lat:-?}" "${loss:-n/a}" ok ookla ""
return 0
fi
cat "${out}" >> "${err}" 2>/dev/null || true
OOKLA_ERR="$(explain_error "${err}")"
[[ -z "${OOKLA_ERR}" ]] && OOKLA_ERR="$(xyz_t "no result (exit code or timeout)" "没有结果(退出或超时)")"
return 1
}
run_go() {
# run_go HOSTPORT ID CARRIER LABEL -> 0 ok; 1 failed (GO_ERR set)
local hp="$1" id="$2" carrier="$3" label="$4" out="${TMP}/go-$2.json" runner=()
GO_ERR=""
if ! fetch_go; then GO_ERR="$(xyz_t "speedtest-go unavailable" "speedtest-go 不可用")"; return 1; fi
xyz_need_cmd timeout && runner=(timeout 90)
printf ' %s %s (#%s, speedtest-go %s)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}" "${hp}"
if "${runner[@]+"${runner[@]}"}" "${GO_BIN}" --custom-url="http://${hp}" --json >"${out}" 2>&1 \
&& grep -q '"dl_speed"' "${out}"; then
local lat dl ul
lat="$(grep -o '"latency":[0-9]*' "${out}" | head -1 | cut -d: -f2)"
dl="$(grep -o '"dl_speed":[0-9.e+]*' "${out}" | head -1 | cut -d: -f2)"
ul="$(grep -o '"ul_speed":[0-9.e+]*' "${out}" | head -1 | cut -d: -f2)"
if awk -v d="${dl:-0}" -v u="${ul:-0}" 'BEGIN { exit !(d + u > 0) }'; then
record "${carrier}" "${id}" "${label}" "$(mbps_bytes "${dl}")" "$(mbps_bytes "${ul}")" \
"$(awk -v n="${lat:-0}" 'BEGIN { printf "%.1f", n / 1000000 }')" "n/a" ok speedtest-go ""
return 0
fi
fi
GO_ERR="$(explain_error "${out}")"
[[ -z "${GO_ERR}" ]] && GO_ERR="$(xyz_t "no result" "没有结果")"
return 1
}
UPLOAD_FILE=""
http_streams() {
# http_streams down|up URL -> aggregate Mbps over 4 parallel streams
local mode="$1" url="$2" s pids=() f total
for s in 1 2 3 4; do
f="${TMP}/http-${mode}-${s}"
if [[ "${mode}" == "down" ]]; then
curl -s -L -o /dev/null --connect-timeout 5 --max-time "${HTTP_SECS}" \
-w '%{size_download} %{time_total}\n' "${url}" > "${f}" 2>/dev/null &
else
curl -s -o /dev/null --connect-timeout 5 --max-time "${HTTP_SECS}" -X POST \
-H 'Content-Type: application/octet-stream' -T "${UPLOAD_FILE}" \
-w '%{size_upload} %{time_total}\n' "${url}" > "${f}" 2>/dev/null &
fi
pids+=($!)
done
for s in "${pids[@]}"; do wait "${s}" 2>/dev/null || true; done
total="$(cat "${TMP}"/http-"${mode}"-? 2>/dev/null | awk '$2 > 0 { t += $1 / $2 } END { printf "%.2f", t * 8 / 1000000 }')"
printf '%s' "${total:-0.00}"
}
run_http() {
# run_http BASEURL ID CARRIER LABEL -> 0 ok; 1 failed (HTTP_ERR set)
local base="$1" id="$2" carrier="$3" label="$4" lat="" t i code
HTTP_ERR=""
printf ' %s %s (#%s, HTTP %s)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}" "${base}"
for i in 1 2 3; do
t="$(curl -s -o /dev/null -m 5 --connect-timeout 4 -w '%{http_code} %{time_starttransfer} %{time_pretransfer}' "${base}/hello" 2>/dev/null || true)"
code="${t%% *}"
[[ "${code}" =~ ^[23] ]] || continue
t="$(awk -v a="$(echo "${t}" | cut -d' ' -f2)" -v b="$(echo "${t}" | cut -d' ' -f3)" 'BEGIN { printf "%.1f", (a - b) * 1000 }')"
if [[ -z "${lat}" ]] || awk -v x="${t}" -v y="${lat}" 'BEGIN { exit !(x < y) }'; then lat="${t}"; fi
done
if [[ -z "${lat}" ]]; then
HTTP_ERR="$(xyz_t "no answer from" "无响应:") ${base}/hello (HTTP ${code:-000})"
return 1
fi
if [[ -z "${UPLOAD_FILE}" ]]; then
UPLOAD_FILE="${TMP}/upload.bin"
head -c 16777216 /dev/zero > "${UPLOAD_FILE}" 2>/dev/null || true
fi
local down up
down="$(http_streams down "${base}/download?size=100000000")"
up="$(http_streams up "${base}/upload")"
if awk -v d="${down}" -v u="${up}" 'BEGIN { exit !(d + u > 0) }'; then
record "${carrier}" "${id}" "${label}" "${down}" "${up}" "${lat}" "n/a" ok http ""
return 0
fi
HTTP_ERR="$(xyz_t "download and upload both returned 0 bytes" "下载和上传都没有数据")"
return 1
}
# ---- candidates ---------------------------------------------------------------
# engine|id|carrier|region|zh|en|target
fetch_live() {
# fetch_live URL DEST -> 0 when downloaded
[[ "${OFFLINE}" == "1" ]] && return 1
[[ -s "$2" ]] && return 0
xyz_info "$(xyz_t "Fetching the live node list:" "正在获取在线节点列表:") $1" >&2
curl -fsSL --connect-timeout 10 --max-time 30 -o "$2" "$1" 2>/dev/null || return 1
tr -d '\r' < "$2" > "$2.tmp" && mv "$2.tmp" "$2"
}
candidates() {
local carrier="$1" n net cn f1 f2
for n in "${CN_NODES[@]}"; do
[[ "$(node_field "${n}" 2)" == "${carrier}" ]] || continue
printf 'ookla|%s|%s|%s|%s|%s|%s\n' "$(node_field "${n}" 1)" "${carrier}" "$(node_field "${n}" 3)" \
"$(node_field "${n}" 4)" "$(node_field "${n}" 5)" "$(node_field "${n}" 6)"
done
case "${carrier}" in
ct) f1="CN_Telecom.csv"; f2="telecom.csv" ;;
cu) f1="CN_Unicom.csv"; f2="unicom.csv" ;;
cm) f1="CN_Mobile.csv"; f2="mobile.csv" ;;
*) return 0 ;;
esac
net="${TMP}/net-${f1}"
if fetch_live "${LIVE_NET_BASE}/${f1}" "${net}"; then
# id,country_code,country,city,ip,host,port,supplier
awk -F',' -v c="${carrier}" '$1 ~ /^[0-9]+$/ && $6 != "" {
h = $6; sub(/\.prod\.hosts\.ooklaserver\.net$/, "", h)
printf "ookla|%s|%s|all|%s %s|%s (%s)|%s:%s\n", $1, c, $4, $8, $4, $8, h, $7 }' "${net}"
fi
cn="${TMP}/cn-${f2}"
if fetch_live "${LIVE_CN_BASE}/${f2}" "${cn}"; then
# id,active,https,cros,preferred,host,cc,province,city,ver,operator,lon,lat,...,sponsor(16),...,pingUrl(20)
awk -F',' -v c="${carrier}" 'NR > 1 && NF >= 22 && $20 ~ /\/hello$/ {
p = $8; sub(/省$|市$/, "", p)
r = "all"
if (p ~ /北京|天津|河北|山西|内蒙古|辽宁|吉林|黑龙江|山东|河南/) r = "north"
else if (p ~ /上海|江苏|浙江|安徽|福建|江西/) r = "east"
else if (p ~ /广东|广西|海南|湖南|湖北/) r = "south"
else if (p ~ /重庆|四川|贵州|云南|西藏|陕西|甘肃|青海|宁夏|新疆/) r = "west"
city = $9; sub(/市$/, "", city)
base = $20; sub(/\/hello$/, "", base)
printf "http|%s|%s|%s|%s%s %s|%s, %s (%s)|%s\n", $1, c, r, p, (city == p ? "" : city), $16, city, p, $16, base }' "${cn}"
fi
}
test_carrier() {
local carrier="$1" ok=0 tries=0 start engine id r zh en target label hk why
start="$(date +%s)"
while IFS="|" read -r engine id _ r zh en target; do
[[ -z "${engine}" ]] && continue
if [[ "${REGION}" != "all" && "${r}" != "${REGION}" && "${r}" != "all" ]]; then continue; fi
hk="$(host_key "${target:-id-${id}}")"
[[ "${TRIED_HOSTS}" == *" ${hk} "* ]] && continue
if (( tries >= MAX_TRIES && ALL_NODES == 0 )); then
xyz_warn "$(xyz_t "Tried" "已尝试") ${tries} $(xyz_t "nodes for" "个节点:") $(xyz_ui_carrier "${carrier}") (--max-tries ${MAX_TRIES})"
break
fi
if (( $(date +%s) - start >= TIME_CAP )); then
xyz_warn "$(xyz_t "Time cap reached for" "已达到时长上限:") $(xyz_ui_carrier "${carrier}") (--time-cap ${TIME_CAP}s)"
break
fi
TRIED_HOSTS="${TRIED_HOSTS}${hk} "
tries=$((tries + 1))
label="$(xyz_t "${en}" "${zh}")"
if [[ "${engine}" == "ookla" ]]; then
if run_ookla "${id}" "${carrier}" "${label}"; then ok=1
else
why="Ookla CLI: ${OOKLA_ERR}"
if [[ -n "${target}" ]]; then
xyz_info "$(xyz_t "Ookla CLI could not select" "Ookla CLI 无法选中") #${id}: ${OOKLA_ERR}"
if ! probe "http://${target}/hello"; then
skip_node "${carrier}" "${id}" "${label}" "probe" "${why}; $(xyz_t "server unreachable:" "服务器无法连接:") http://${target}"
continue
fi
if run_go "${target}" "${id}" "${carrier}" "${label}"; then ok=1
elif run_http "http://${target}" "${id}" "${carrier}" "${label}"; then ok=1
else
skip_node "${carrier}" "${id}" "${label}" "ookla/go/http" "${why}; speedtest-go: ${GO_ERR}; HTTP: ${HTTP_ERR}"
fi
else
skip_node "${carrier}" "${id}" "${label}" "ookla" "${why}"
fi
fi
else
if ! probe "${target}/hello"; then
skip_node "${carrier}" "${id}" "${label}" "http" "$(xyz_t "server unreachable:" "服务器无法连接:") ${target}/hello"
continue
fi
if run_http "${target}" "${id}" "${carrier}" "${label}"; then ok=1
else skip_node "${carrier}" "${id}" "${label}" "http" "${HTTP_ERR}"; fi
fi
if [[ "${ok}" == "1" && "${ALL_NODES}" != "1" ]]; then break; fi
done < <(candidates "${carrier}")
if [[ "${ok}" == "0" && "${tries}" == "0" ]]; then
record "${carrier}" "-" "$(xyz_ui_carrier "${carrier}")" "-" "-" "-" "-" "skip" "-" "$(xyz_t "no candidate node for this region" "该地区没有候选节点")"
fi
return 0
}
xyz_step "$(xyz_t "Running tests" "开始测速")"
if [[ "${NEAREST}" == "1" ]]; then
if ! run_ookla auto nearest "$(xyz_t "Nearest" "就近节点")"; then
skip_node nearest auto "$(xyz_t "Nearest" "就近节点")" ookla "${OOKLA_ERR}"
fi
fi
WANT=()
[[ "${CARRIERS}" != "none" ]] && IFS=',' read -r -a WANT <<< "${CARRIERS}"
for carrier in "${WANT[@]+"${WANT[@]}"}"; do
test_carrier "${carrier}"
done
echo
# Dashboard view: every successful row; one summary row per carrier with none.
VIEW="${TMP}/cn-view.tsv"
TRIED_WORD="$(xyz_t "tried" "个已尝试")"
awk -F'\t' -v OFS='\t' -v w="${TRIED_WORD}" '
{ if (!($1 in seen)) { seen[$1] = 1; order[++n] = $1 }
if ($8 == "ok") { good[$1] = 1; rows[++m] = $0; rc[m] = $1 }
else { cnt[$1]++; if (!($1 in first)) first[$1] = $0; last[$1] = $10 } }
END {
for (i = 1; i <= n; i++) {
c = order[i]
if (good[c]) { for (j = 1; j <= m; j++) if (rc[j] == c) print rows[j] }
else { split(first[c], f, "\t"); print c, f[2], f[3], "-", "-", "-", "-", "skip", cnt[c] " " w, last[c] }
}
}' "${RESULTS}" > "${VIEW}"
xyz_ui_speed_table "${VIEW}" "$(xyz_t "Speed test results" "测速结果") (Mbps / ms)"
echo
if [[ -n "${XYZ_RESULT_FILE:-}" ]]; then
sed 's/^/speedtest-cn\t/' "${VIEW}" >> "${XYZ_RESULT_FILE}" 2>/dev/null || true
fi
good="$(awk -F'\t' '$8 == "ok"' "${RESULTS}" | wc -l | tr -d ' ')"
cn_good="$(awk -F'\t' '$8 == "ok" && $1 != "nearest"' "${RESULTS}" | wc -l | tr -d ' ')"
if [[ "${good}" == "0" ]]; then
xyz_error "$(xyz_t \
"No node finished a test. The nodes may be offline, or this network blocks speed-test traffic." \
"没有任何节点完成测速。可能节点已下线,或当前网络屏蔽了测速流量。")"
exit 2
fi
if [[ "${cn_good}" == "0" && "${#WANT[@]}" -gt 0 ]]; then
xyz_warn "$(xyz_t "No China node finished a test; see the warnings above for each node's full error." "没有国内节点完成测速,每个节点的完整错误见上方警告。")"
fi
xyz_info "$(xyz_t "Three-network test finished." "三网测速完成。")"
推荐 VPS
以下为占位展示,尚未接入任何商家或联盟链接,也没有可点击的购买地址。上线推广前会再填入真实套餐与合规的返佣链接。
推荐方向:美国
商家待定
机房、配置与价格将在接入后填写。当前不是购买入口。
商家待定
机房、配置与价格将在接入后填写。当前不是购买入口。
商家待定
机房、配置与价格将在接入后填写。当前不是购买入口。