Files
x-panel/dnsjc.sh

290 lines
9.1 KiB
Bash
Raw Permalink Normal View History

2026-05-03 11:34:48 +08:00
#!/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 "地区: %sslug: %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