290 lines
9.1 KiB
Bash
290 lines
9.1 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
set -u
|
||
set -o pipefail
|
||
|
||
COUNT=${COUNT:-10}
|
||
TOPN=${TOPN:-30}
|
||
SLEEP_META=${SLEEP_META:-0.15}
|
||
META_CACHE_DIR="${META_CACHE_DIR:-/tmp/dns_meta_cache}"
|
||
mkdir -p "$META_CACHE_DIR" >/dev/null 2>&1 || true
|
||
|
||
# deps
|
||
require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "missing dependency: $1"; exit 1; }; }
|
||
require_cmd curl
|
||
require_cmd awk
|
||
require_cmd sed
|
||
require_cmd tr
|
||
require_cmd grep
|
||
require_cmd ping
|
||
|
||
PING_BIN="ping"
|
||
PING6_BIN="ping -6"
|
||
if command -v ping6 >/dev/null 2>&1; then
|
||
PING6_BIN="ping6"
|
||
fi
|
||
|
||
# color banner (red)
|
||
if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
|
||
RED=$'\033[1;31m'
|
||
RESET=$'\033[0m'
|
||
else
|
||
RED=""
|
||
RESET=""
|
||
fi
|
||
echo ""
|
||
printf "%b\n\n" "${RED}〔X-Panel-Pro 面板〕专属 “服务器 DNS 检测”${RESET}"
|
||
|
||
# extract valid IPs (IPv4 strict / IPv6 loose)
|
||
extract_ips() {
|
||
sed -E 's/<[^>]+>/ /g' \
|
||
| tr -c '0-9A-Fa-f:.' '\n' \
|
||
| grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^([0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}$)' \
|
||
| awk '!seen[$0]++' \
|
||
| awk -F. '
|
||
$0 ~ /:/ { print; next }
|
||
NF==4 {
|
||
ok=1
|
||
for(i=1;i<=4;i++){
|
||
if($i !~ /^[0-9]+$/ || $i<0 || $i>255){ ok=0; break }
|
||
}
|
||
if(ok) print $0
|
||
}
|
||
'
|
||
}
|
||
|
||
# filter private/reserved IPv4
|
||
is_public_ipv4() {
|
||
local ip="$1"; IFS=. read -r a b c d <<<"$ip" || return 1
|
||
if ((a==10)) || ((a==127)) || ((a==192 && b==168)) || ((a==169 && b==254)) \
|
||
|| ((a==172 && b>=16 && b<=31)) || ((a==100 && b>=64 && b<=127)) \
|
||
|| ((a==0)) || ((a>=224)); then
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# normalize country to slug
|
||
normalize_country_to_slug() {
|
||
local raw="$1"
|
||
local x compact
|
||
x=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')
|
||
x=$(printf '%s' "$x" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
|
||
compact=$(printf '%s' "$x" | sed -E 's/[^a-z0-9]+//g')
|
||
case "$compact" in
|
||
jp) echo "japan"; return;;
|
||
kr|rok) echo "southkorea"; return;;
|
||
us|usa) echo "unitedstates"; return;;
|
||
uk|gb) echo "unitedkingdom"; return;;
|
||
ae|uae) echo "unitedarabemirates"; return;;
|
||
hk) echo "hongkong"; return;;
|
||
tw) echo "taiwan"; return;;
|
||
cn) echo "china"; return;;
|
||
de) echo "germany"; return;;
|
||
fr) echo "france"; return;;
|
||
it) echo "italy"; return;;
|
||
es) echo "spain"; return;;
|
||
ru) echo "russia"; return;;
|
||
sg) echo "singapore"; return;;
|
||
th) echo "thailand"; return;;
|
||
vn) echo "vietnam"; return;;
|
||
ph) echo "philippines"; return;;
|
||
id) echo "indonesia"; return;;
|
||
my) echo "malaysia"; return;;
|
||
au) echo "australia"; return;;
|
||
nz) echo "newzealand"; return;;
|
||
nl) echo "netherlands"; return;;
|
||
be) echo "belgium"; return;;
|
||
pl) echo "poland"; return;;
|
||
cz) echo "czechia"; return;;
|
||
ch) echo "switzerland"; return;;
|
||
at) echo "austria"; return;;
|
||
se) echo "sweden"; return;;
|
||
no) echo "norway"; return;;
|
||
fi) echo "finland"; return;;
|
||
pt) echo "portugal"; return;;
|
||
ro) echo "romania"; return;;
|
||
hu) echo "hungary"; return;;
|
||
sk) echo "slovakia"; return;;
|
||
si) echo "slovenia"; return;;
|
||
gr) echo "greece"; return;;
|
||
ie) echo "ireland"; return;;
|
||
mx) echo "mexico"; return;;
|
||
ca) echo "canada"; return;;
|
||
br) echo "brazil"; return;;
|
||
ar) echo "argentina"; return;;
|
||
cl) echo "chile"; return;;
|
||
za) echo "southafrica"; return;;
|
||
esac
|
||
x=$(printf '%s' "$x" | sed -E 's/[[:space:]]+//g')
|
||
echo "$x"
|
||
}
|
||
|
||
# fetch IPs from publicdnsserver page
|
||
fetch_country_ips() {
|
||
local slug="$1"
|
||
local url="https://publicdnsserver.com/${slug}/"
|
||
local html
|
||
html=$(curl -sL --max-time 15 "$url" || true)
|
||
[ -z "$html" ] && { echo ""; return 0; }
|
||
printf '%s' "$html" | extract_ips
|
||
}
|
||
|
||
# metadata (ip-api) with 1h cache
|
||
get_meta_json() {
|
||
local ip="$1"
|
||
local cache="$META_CACHE_DIR/$ip.json"
|
||
local epoch mtime age
|
||
if [ -s "$cache" ]; then
|
||
epoch=$(date +%s)
|
||
if mtime=$(stat -c %Y "$cache" 2>/dev/null); then
|
||
age=$((epoch - mtime))
|
||
if [ "$age" -lt 3600 ]; then
|
||
cat "$cache"; return 0
|
||
fi
|
||
fi
|
||
fi
|
||
local resp
|
||
resp=$(curl -s --max-time 5 "http://ip-api.com/json/$ip?fields=status,message,country,city,as,asname,org,isp,query")
|
||
if [ -n "$resp" ]; then
|
||
printf '%s' "$resp" >"$cache" 2>/dev/null || true
|
||
printf '%s' "$resp"
|
||
else
|
||
printf '{"status":"fail","country":"","city":"","as":"","asname":"","org":"","isp":"","query":"%s"}' "$ip"
|
||
fi
|
||
sleep "$SLEEP_META"
|
||
}
|
||
|
||
# poor-man's json getter (no jq)
|
||
json_get() {
|
||
echo "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p"
|
||
}
|
||
|
||
# parse ping output (returns: loss,min,avg,max,mdev)
|
||
parse_ping() {
|
||
local out; out=$(cat)
|
||
local loss min avg max mdev rtt
|
||
loss=$(printf '%s\n' "$out" | LC_ALL=C grep -Eo '[0-9]+(\.[0-9]+)?% packet loss' | sed -E 's/%.*//')
|
||
[ -z "$loss" ] && loss="100.0"
|
||
rtt=$(printf '%s\n' "$out" | LC_ALL=C grep -E 'min/avg/max' | tail -n1 | awk -F'=' '{print $2}' | awk '{print $1}')
|
||
if [ -n "$rtt" ]; then
|
||
IFS=/ read -r min avg max mdev <<<"$rtt"
|
||
else
|
||
min="N/A"; avg="N/A"; max="N/A"; mdev="N/A"
|
||
fi
|
||
printf '%s,%s,%s,%s,%s\n' "$loss" "$min" "$avg" "$max" "$mdev"
|
||
}
|
||
|
||
# main
|
||
read -rp "请输入英文国家名(如 Japan / United States / South Korea;或 ISO 两字母,如 JP): " USER_REGION || USER_REGION=""
|
||
SLUG=$(normalize_country_to_slug "${USER_REGION:-}")
|
||
[ -z "$SLUG" ] && SLUG="japan"
|
||
|
||
MAP_IPS_RAW=$(fetch_country_ips "$SLUG")
|
||
|
||
declare -a TARGETS TMP_LIST RESULTS
|
||
declare -A seen
|
||
|
||
while IFS= read -r ip; do
|
||
[ -z "$ip" ] && continue
|
||
if [[ "$ip" == *:* ]]; then
|
||
TARGETS+=("$ip")
|
||
else
|
||
if is_public_ipv4 "$ip"; then
|
||
TARGETS+=("$ip")
|
||
fi
|
||
fi
|
||
done < <(printf '%s\n' "$MAP_IPS_RAW" | awk 'NF{if(!seen[$0]++){print}}')
|
||
|
||
for ip in "${TARGETS[@]}"; do
|
||
if [ -z "${seen[$ip]+x}" ]; then
|
||
TMP_LIST+=("$ip"); seen["$ip"]=1
|
||
fi
|
||
[ "${#TMP_LIST[@]}" -ge "$TOPN" ] && break
|
||
done
|
||
TARGETS=("${TMP_LIST[@]}")
|
||
|
||
for must in 1.1.1.1 8.8.8.8; do
|
||
if [ -z "${seen[$must]+x}" ]; then
|
||
TARGETS+=("$must"); seen["$must"]=1
|
||
fi
|
||
done
|
||
|
||
N=${#TARGETS[@]}
|
||
printf "地区: %s(slug: %s)| 目标数: %d | 每个目标 ping 次数: %d\n" "${USER_REGION:-N/A}" "$SLUG" "$N" "$COUNT"
|
||
printf "将测试的目标:%s\n" "$(printf '%s ' "${TARGETS[@]}")"
|
||
echo "开始测试 ......"
|
||
|
||
if [ "$N" -eq 0 ]; then
|
||
echo "未找到可用目标。"
|
||
exit 0
|
||
fi
|
||
|
||
idx=0
|
||
for ip in "${TARGETS[@]}"; do
|
||
idx=$((idx+1))
|
||
pct=$(( idx * 100 / (N==0 ? 1 : N) ))
|
||
|
||
printf "进度: [%d/%d | %3d%%] #%d 正在测试: %s\r" "$idx" "$N" "$pct" "$idx" "$ip"
|
||
|
||
if [[ "$ip" == *:* ]]; then
|
||
out=$(LC_ALL=C $PING6_BIN -n -c "$COUNT" -i 0.2 -w $((COUNT+4)) "$ip" 2>&1 || true)
|
||
else
|
||
out=$(LC_ALL=C $PING_BIN -n -c "$COUNT" -i 0.2 -w $((COUNT+4)) "$ip" 2>&1 || true)
|
||
fi
|
||
|
||
stats=$(printf '%s' "$out" | parse_ping)
|
||
IFS=, read -r loss min avg max mdev <<<"$stats"
|
||
|
||
meta=$(get_meta_json "$ip")
|
||
country=$(json_get "$meta" country); [ -z "$country" ] && country="N/A"
|
||
city=$(json_get "$meta" city); [ -z "$city" ] && city="N/A"
|
||
asfull=$(json_get "$meta" as); [ -z "$asfull" ] && asfull="N/A"
|
||
asname=$(json_get "$meta" asname); [ -z "$asname" ] && asname="N/A"
|
||
org=$(json_get "$meta" org)
|
||
isp=$(json_get "$meta" isp)
|
||
|
||
asn=$(printf '%s' "$asfull" | sed -n 's/.*\(AS[0-9][0-9]*\).*/\1/p')
|
||
[ -z "$asn" ] && asn="N/A"
|
||
|
||
company="$asname"
|
||
[ -z "$company" ] || [ "$company" = "N/A" ] && company="$org"
|
||
[ -z "$company" ] || [ "$company" = "N/A" ] && company="$isp"
|
||
[ -z "$company" ] && company="N/A"
|
||
|
||
printf "进度: [%d/%d | %3d%%] #%d 正在测试: %-39s | 丢包 %s%% | 最小 %sms | 平均 %sms | 最大 %sms | 抖动 %sms\n" \
|
||
"$idx" "$N" "$pct" "$idx" "$ip" "$loss" "$min" "$avg" "$max" "$mdev"
|
||
|
||
sortkey="$avg"; [[ "$sortkey" == "N/A" || -z "$sortkey" ]] && sortkey=999999999
|
||
RESULTS+=("$sortkey\t$idx\t$ip\t$country/$city\t$asn\t$company\t$loss\t$min\t$avg\t$max\t$mdev")
|
||
done
|
||
echo
|
||
|
||
HEADER=$'编号\t目标\t地区\tASN\t公司\t丢包\t最小(ms)\t平均(ms)\t最大(ms)\t抖动'
|
||
BODY=$(
|
||
printf '%b\n' "${RESULTS[@]}" \
|
||
| LC_ALL=C sort -t$'\t' -k1,1n \
|
||
| cut -f2- \
|
||
| while IFS=$'\t' read -r idx0 ip region asn company loss min avg max mdev; do
|
||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||
"$idx0" "$ip" "$region" "$asn" "$company" \
|
||
"$(printf '%.1f%%' "${loss:-0}")" \
|
||
"${min:-N/A}" "${avg:-N/A}" "${max:-N/A}" "${mdev:-N/A}"
|
||
done
|
||
)
|
||
|
||
if command -v column >/dev/null 2>&1; then
|
||
{ printf '%s\n' "$HEADER"; printf '%s\n' "$BODY"; } | column -t -s $'\t'
|
||
else
|
||
printf "%-4s %-39s %-18s %-8s %-28s %-6s %-9s %-9s %-9s %-7s\n" \
|
||
"编号" "目标" "地区" "ASN" "公司" "丢包" "最小(ms)" "平均(ms)" "最大(ms)" "抖动"
|
||
printf -- "-----------------------------------------------------------------------------------------------\n"
|
||
printf '%s\n' "$BODY" | while IFS=$'\t' read -r idx0 ip region asn company loss min avg max mdev; do
|
||
printf "%-4s %-39s %-18s %-8s %-28s %-6s %-9s %-9s %-9s %-7s\n" \
|
||
"$idx0" "$ip" "$region" "$asn" "$company" \
|
||
"$loss" "$min" "$avg" "$max" "$mdev"
|
||
done
|
||
|
||
fi
|
||
|