speedtest3 China 3-network speedtest (experimental)
This experimental script is kept for compatibility. On a VPS outside mainland China, Ookla often cannot list mainland servers and the test may fail entirely. Run the command below only if you still want to try; for everyday speed tests use xyz.sh/speedtest --overseas.
Alias of speedtest-cn
$ curl -fsSL xyz.sh/speedtest3 | bash
cn speedtest3441239aaee06325ef8e8f60effe0c50b62921f0eee293b009182a75452a67372
· Matches the script curl receives. You can also fetch /speedtest3.sh.sha256
Every script passes bash -n and ShellCheck and was tested with curl | bash as root in the official Docker images of Debian 12/11, Ubuntu 24.04/22.04/20.04/18.04, Rocky 9, AlmaLinux 9 and CentOS 7 (see “Container test results” on each page). Containers lack systemd, kernel modules and real public routes, so those parts still need a real VPS.
What it does
This experimental script is kept for compatibility; for everyday use run xyz.sh/speedtest --overseas or xyz.sh/all. Ookla no longer lists mainland servers to clients abroad, so speedtest -s <ID> often fails with “No servers defined”, and China nodes frequently all fail on a VPS outside the mainland. This site does not promise China speed results. If you still want to try, the script attempts, per carrier: (1) the official Ookla CLI (official tarball, SHA256 pinned) with -s <ID>; (2) speedtest-go (official release, SHA256 pinned, run from a temp dir) with --custom-url; (3) an HTTP test against speedtest.cn nodes. Candidates come from the built-in list and the public spiritLHLS lists; full errors are printed on failure.
Container test results
Results of curl | bash as root without a terminal in each distribution's official Docker image. partial means the container could not verify everything (for example no systemd, no kernel module, or the test protocol blocked by the test environment's proxy) and a real VPS is needed to confirm. (2026-11-11)
| OS | Result |
|---|---|
| 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 |
FAQ
- Is this script safe?
- xyz.sh adds no telemetry and performs no hidden actions. Read the full source on the page or download it with curl -fsSL xyz.sh/<name>.sh first; every script has a /<name>.sh.sha256 checksum. Piping a remote script to bash is still a risk — review the source first. Tests are run by Ookla's official CLI, which has its own terms (https://www.speedtest.net/terms); the script shows them and asks you to confirm.
- Which operating systems are supported?
- Declared support: Debian, Ubuntu, CentOS, Rocky Linux, and AlmaLinux. The script detects the distribution and warns on an unknown one. It was tested in Docker containers of these systems (syntax, --dry-run, and whatever can run in a container; see tests/RESULTS.md); parts that need systemd, kernel modules, or the public internet still need a real VPS.
- Do I need root?
- No. It runs as a normal user; as root some tools can be installed system-wide.
- Why are there only a few mainland nodes?
- Ookla has de-listed most mainland China Speedtest servers; only a handful remain and they change. Built-in nodes come from the daily-updated public list spiritLHLS/speedtest.net-CN-ID, and if they all fail the latest list is fetched once. You can also pass your own IDs with --servers.
- How accurate is it?
- It is a single measurement from the VPS to one carrier test node, affected by node load, peak hours and the VPS's own cap. Use it to compare routes; it is not your home connection speed. Test at different times.
Full source
This is exactly what curl receives (common library + script body). Read it before running.
#!/usr/bin/env bash
# Served by xyz.sh/speedtest3
# Human-readable page: https://xyz.sh/speedtest3
# 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="speedtest3"
# 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." "三网测速完成。")"
Recommended VPS
These slots are placeholders. No vendor or affiliate link has been added, and nothing here is a purchase button. Real plans and compliant referral links will be filled in later.
Suggested for:United States
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.