mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
844 lines
26 KiB
Bash
Executable File
844 lines
26 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
CONF="cf-optimizer"
|
|
RUN_DIR="/tmp/run/cf-optimizer"
|
|
TMP_DIR="/tmp/cf-optimizer"
|
|
HOSTS_FILE="/tmp/hosts/cf-optimizer"
|
|
LOG_FILE="/var/log/cf-optimizer.log"
|
|
DEFAULT_IPS_FILE="/usr/share/cf-optimizer/default_ips.txt"
|
|
COLO_MAP_FILE="/usr/share/cf-optimizer/colo_map.json"
|
|
PID_FILE="$RUN_DIR/cf-optimizer.pid"
|
|
COLO_CACHE_FILE="$RUN_DIR/colo_cache.txt"
|
|
STATUS_JSON="$RUN_DIR/status.json"
|
|
CURRENT_IP_FILE="$RUN_DIR/current_ip.txt"
|
|
PRIMARY_IP_FILE="$RUN_DIR/primary_ip.txt"
|
|
STANDBY_IP_FILE="$RUN_DIR/standby_ip.txt"
|
|
BASELINE_TCP_FILE="$RUN_DIR/baseline_tcp.txt"
|
|
FAIL_STREAK_FILE="$RUN_DIR/fail_streak.txt"
|
|
|
|
log() {
|
|
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
|
echo "$msg"
|
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
echo "$msg" >> "$LOG_FILE"
|
|
# 限制日志大小不超过 1000 行
|
|
if [ -f "$LOG_FILE" ] && [ $(wc -l < "$LOG_FILE") -gt 1500 ]; then
|
|
tail -n 800 "$LOG_FILE" > "$LOG_FILE.tmp" && mv -f "$LOG_FILE.tmp" "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
get_colo_name() {
|
|
local code="$1"
|
|
if [ -f "$COLO_MAP_FILE" ] && [ -n "$code" ]; then
|
|
local name
|
|
name=$(grep -E "\"$code\"[[:space:]]*:" "$COLO_MAP_FILE" 2>/dev/null | head -n 1 | sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/')
|
|
if [ -n "$name" ]; then
|
|
echo "$name"
|
|
return
|
|
fi
|
|
fi
|
|
echo "${code:-未知}"
|
|
}
|
|
|
|
update_colo_cache() {
|
|
local ip="$1"
|
|
local colo="$2"
|
|
[ -z "$ip" ] || [ -z "$colo" ] && return
|
|
mkdir -p "$RUN_DIR"
|
|
touch "$COLO_CACHE_FILE"
|
|
grep -v "^$ip[[:space:]]" "$COLO_CACHE_FILE" > "$COLO_CACHE_FILE.tmp" 2>/dev/null
|
|
echo "$ip $colo" >> "$COLO_CACHE_FILE.tmp"
|
|
mv -f "$COLO_CACHE_FILE.tmp" "$COLO_CACHE_FILE"
|
|
}
|
|
|
|
get_cached_colo() {
|
|
local ip="$1"
|
|
[ ! -f "$COLO_CACHE_FILE" ] && return
|
|
grep "^$ip[[:space:]]" "$COLO_CACHE_FILE" 2>/dev/null | tail -n 1 | awk '{print $2}'
|
|
}
|
|
|
|
clean_hosts() {
|
|
mkdir -p "$RUN_DIR"
|
|
if [ -f "$HOSTS_FILE" ]; then
|
|
rm -f "$HOSTS_FILE"
|
|
killall -HUP dnsmasq 2>/dev/null
|
|
log "已清空优选 hosts 映射并重载 dnsmasq"
|
|
fi
|
|
rm -f "$CURRENT_IP_FILE" "$PRIMARY_IP_FILE" "$STANDBY_IP_FILE" "$BASELINE_TCP_FILE" "$FAIL_STREAK_FILE"
|
|
rm -rf "$RUN_DIR/task.lock"
|
|
cat <<EOF > "$STATUS_JSON"
|
|
{
|
|
"running": false,
|
|
"last_update": "",
|
|
"best_ip": "",
|
|
"best_colo": "",
|
|
"best_tcp_lat": 0,
|
|
"best_tls_lat": 0,
|
|
"standby_ip": "",
|
|
"standby_colo": "",
|
|
"standby_tcp_lat": 0,
|
|
"watchdog_status": "未运行",
|
|
"domains": [],
|
|
"rankings": []
|
|
}
|
|
EOF
|
|
}
|
|
|
|
sync_hosts() {
|
|
. /lib/functions.sh
|
|
config_load "$CONF"
|
|
local enabled
|
|
config_get_bool enabled settings enabled 0
|
|
if [ "$enabled" != "1" ]; then
|
|
clean_hosts
|
|
return 0
|
|
fi
|
|
|
|
local domains=""
|
|
append_sync_domain() {
|
|
local d="$1"
|
|
[ -n "$d" ] && domains="$domains $d"
|
|
}
|
|
config_list_foreach settings domains append_sync_domain
|
|
|
|
if [ -z "$domains" ]; then
|
|
clean_hosts
|
|
return 0
|
|
fi
|
|
|
|
local current_ip=""
|
|
[ -f "$CURRENT_IP_FILE" ] && current_ip=$(cat "$CURRENT_IP_FILE" 2>/dev/null)
|
|
|
|
if [ -z "$current_ip" ]; then
|
|
local cand_tmp="$TMP_DIR/cand_init.txt"
|
|
mkdir -p "$TMP_DIR"
|
|
load_candidate_ips "$cand_tmp"
|
|
current_ip=$(head -n 1 "$cand_tmp" 2>/dev/null)
|
|
rm -f "$cand_tmp"
|
|
fi
|
|
|
|
[ -z "$current_ip" ] && return 0
|
|
|
|
local hosts_tmp="$HOSTS_FILE.tmp"
|
|
mkdir -p "$(dirname "$HOSTS_FILE")"
|
|
> "$hosts_tmp"
|
|
echo "# Auto-generated by cf-optimizer sync on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp"
|
|
local d
|
|
for d in $domains; do
|
|
echo "$current_ip $d" >> "$hosts_tmp"
|
|
done
|
|
mv -f "$hosts_tmp" "$HOSTS_FILE"
|
|
killall -HUP dnsmasq 2>/dev/null
|
|
log "已即时同步域名配置至 hosts 并重载 dnsmasq (定向至: $current_ip)"
|
|
}
|
|
|
|
load_candidate_ips() {
|
|
local out_file="$1"
|
|
> "$out_file"
|
|
|
|
local ip_source
|
|
config_get ip_source settings ip_source "builtin"
|
|
|
|
if [ "$ip_source" = "builtin" ] || [ "$ip_source" = "mixed" ]; then
|
|
if [ -f "$DEFAULT_IPS_FILE" ]; then
|
|
grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$DEFAULT_IPS_FILE" >> "$out_file"
|
|
fi
|
|
fi
|
|
|
|
if [ "$ip_source" = "custom" ] || [ "$ip_source" = "mixed" ]; then
|
|
append_custom_ip() {
|
|
local _item="$1"
|
|
local _base_prefix _host
|
|
[ -z "$_item" ] && return
|
|
# 若为 CIDR 简单网段 (如 /24),取前 3 个 IP
|
|
case "$_item" in
|
|
*/*)
|
|
_base_prefix=$(echo "$_item" | cut -d'/' -f1 | cut -d'.' -f1-3)
|
|
for _host in 1 2 3; do
|
|
echo "$_base_prefix.$_host" >> "$out_file"
|
|
done
|
|
;;
|
|
*)
|
|
echo "$_item" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' >> "$out_file"
|
|
;;
|
|
esac
|
|
}
|
|
config_list_foreach settings custom_ips append_custom_ip
|
|
fi
|
|
|
|
# 兜底:若文件为空,填入稳定官方公共 IP
|
|
if [ ! -s "$out_file" ]; then
|
|
echo "104.16.0.1" >> "$out_file"
|
|
echo "104.17.0.1" >> "$out_file"
|
|
echo "104.18.0.1" >> "$out_file"
|
|
echo "172.64.0.1" >> "$out_file"
|
|
fi
|
|
|
|
# 排序去重
|
|
sort -u "$out_file" > "$out_file.tmp" && mv -f "$out_file.tmp" "$out_file"
|
|
}
|
|
|
|
# 粗筛单节点探测
|
|
probe_fast() {
|
|
local ip="$1"
|
|
local res_dir="$2"
|
|
# 仅测 TCP 握手
|
|
local connect_time
|
|
connect_time=$(curl -s -k -o /dev/null --connect-timeout 1 -m 2 -w "%{time_connect}\n" \
|
|
--resolve "speed.cloudflare.com:443:$ip" "https://speed.cloudflare.com/__down?bytes=0" 2>/dev/null)
|
|
|
|
if [ $? -eq 0 ] && [ -n "$connect_time" ] && [ "$connect_time" != "0.000000" ]; then
|
|
local ms=$(awk "BEGIN {printf \"%08.2f\", $connect_time * 1000}")
|
|
echo "$ms $ip" > "$res_dir/$ip"
|
|
fi
|
|
}
|
|
|
|
run_optimization() {
|
|
. /lib/functions.sh
|
|
config_load "$CONF"
|
|
|
|
local enabled
|
|
config_get_bool enabled settings enabled 0
|
|
if [ "$enabled" != "1" ]; then
|
|
log "服务未启用,退出任务"
|
|
clean_hosts
|
|
return 0
|
|
fi
|
|
|
|
local domains=""
|
|
append_domain() {
|
|
local d="$1"
|
|
[ -n "$d" ] && domains="$domains $d"
|
|
}
|
|
config_list_foreach settings domains append_domain
|
|
|
|
if [ -z "$domains" ]; then
|
|
log "未配置目标加速域名,跳过优选"
|
|
return 0
|
|
fi
|
|
|
|
local concurrency sample_count hysteresis_ms
|
|
config_get concurrency settings concurrency 10
|
|
config_get sample_count settings sample_count 4
|
|
config_get hysteresis_ms settings hysteresis_ms 30
|
|
|
|
local colo_filter_mode
|
|
config_get colo_filter_mode settings colo_filter_mode "none"
|
|
|
|
local target_colos=""
|
|
append_target_colo() {
|
|
local _item="$1"
|
|
local _colo _clean_colo
|
|
[ -z "$_item" ] && return
|
|
for _colo in $_item; do
|
|
_clean_colo=$(echo "$_colo" | tr 'a-z' 'A-Z' | tr -d ' \t\r\n')
|
|
if [ -n "$_clean_colo" ]; then
|
|
case " $target_colos " in
|
|
*" $_clean_colo "*) ;;
|
|
*) target_colos="$target_colos $_clean_colo" ;;
|
|
esac
|
|
fi
|
|
done
|
|
}
|
|
if [ "$colo_filter_mode" != "none" ]; then
|
|
local multi_colos=""
|
|
config_get multi_colos settings colo_list ""
|
|
[ -n "$multi_colos" ] && append_target_colo "$multi_colos"
|
|
config_list_foreach settings colo_list append_target_colo
|
|
config_list_foreach settings custom_colos append_target_colo
|
|
fi
|
|
|
|
check_colo_allowed() {
|
|
local _colo="$1"
|
|
[ -z "$_colo" ] && return 0
|
|
[ "$colo_filter_mode" = "none" ] && return 0
|
|
|
|
local _match=0
|
|
local _tc
|
|
for _tc in $target_colos; do
|
|
if [ "$_tc" = "$_colo" ]; then
|
|
_match=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$colo_filter_mode" = "exclude" ]; then
|
|
[ "$_match" -eq 1 ] && return 1 || return 0
|
|
elif [ "$colo_filter_mode" = "include" ]; then
|
|
[ "$_match" -eq 1 ] && return 0 || return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
log "开始执行优选任务,加速域名数: $(echo $domains | wc -w),机房策略: $colo_filter_mode${target_colos:+ ($target_colos)}"
|
|
|
|
local lock_dir="$RUN_DIR/task.lock"
|
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
|
local lock_pid=""
|
|
[ -f "$lock_dir/pid" ] && lock_pid=$(cat "$lock_dir/pid" 2>/dev/null)
|
|
if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
log "检测到残留任务锁 (PID: $lock_pid 已退出),自动释放死锁"
|
|
rm -rf "$lock_dir"
|
|
mkdir "$lock_dir" 2>/dev/null || return 0
|
|
else
|
|
log "已有优选任务正在运行中,跳过本次执行"
|
|
return 0
|
|
fi
|
|
fi
|
|
echo $$ > "$lock_dir/pid"
|
|
|
|
mkdir -p "$TMP_DIR/fast" "$TMP_DIR/deep" "$RUN_DIR"
|
|
rm -rf "$TMP_DIR/fast"/* "$TMP_DIR/deep"/*
|
|
|
|
local cand_file="$TMP_DIR/candidates.txt"
|
|
load_candidate_ips "$cand_file"
|
|
local total_cands=$(wc -l < "$cand_file")
|
|
log "阶段一:开始并发初筛,候选节点总数: $total_cands,并发度: $concurrency"
|
|
|
|
# 并发初筛(采用 POSIX 批次等待机制,确保在任何非交互式 Shell 环境下严格限流)
|
|
local running=0
|
|
while read -r ip; do
|
|
[ -z "$ip" ] && continue
|
|
probe_fast "$ip" "$TMP_DIR/fast" &
|
|
running=$((running + 1))
|
|
if [ "$running" -ge "$concurrency" ]; then
|
|
wait
|
|
running=0
|
|
fi
|
|
done < "$cand_file"
|
|
wait
|
|
|
|
# 汇总初筛候选,结合机房缓存预先剔除黑名单机房
|
|
local fast_sorted_file="$TMP_DIR/fast_sorted.txt"
|
|
cat "$TMP_DIR/fast"/* 2>/dev/null | sort -n | head -n 30 > "$fast_sorted_file"
|
|
|
|
local top_selected_file="$TMP_DIR/top_selected_ips.txt"
|
|
> "$top_selected_file"
|
|
local picked_count=0
|
|
while read -r _lat ip; do
|
|
[ -z "$ip" ] && continue
|
|
local cached_colo=$(get_cached_colo "$ip")
|
|
if [ -n "$cached_colo" ] && ! check_colo_allowed "$cached_colo"; then
|
|
log "初筛预过滤: 节点 $ip ($cached_colo) 命中机房排除策略,自动略过"
|
|
continue
|
|
fi
|
|
echo "$ip" >> "$top_selected_file"
|
|
picked_count=$((picked_count + 1))
|
|
[ "$picked_count" -ge 10 ] && break
|
|
done < "$fast_sorted_file"
|
|
|
|
local top_count=$(wc -l < "$top_selected_file")
|
|
if [ "$top_count" -eq 0 ]; then
|
|
log "阶段一初筛无可用候选节点(或全部被机房区域策略过滤),请检查配置"
|
|
rm -rf "$TMP_DIR"
|
|
rm -rf "$lock_dir"
|
|
return 1
|
|
fi
|
|
|
|
local probe_domain=$(echo "$domains" | awk '{print $1}')
|
|
[ -z "$probe_domain" ] && probe_domain="speed.cloudflare.com"
|
|
|
|
log "阶段一初筛完成,选出 $top_count 个候选节点进入精测采样,探活域名: $probe_domain(每节点轮询交叉采样 $sample_count 轮)"
|
|
|
|
# 阶段二:轮询交叉时序采样(跨越完整测试时段,精准捕获阶段性丢包与网络毛刺)
|
|
mkdir -p "$TMP_DIR/samples"
|
|
|
|
local seq
|
|
for seq in $(seq 1 "$sample_count"); do
|
|
while read -r ip; do
|
|
[ -z "$ip" ] && continue
|
|
[ -f "$TMP_DIR/blocked_${ip}" ] && continue
|
|
[ -f "$TMP_DIR/colo_excluded_${ip}" ] && continue
|
|
|
|
local raw_header="$TMP_DIR/header_${ip}_${seq}.txt"
|
|
local metrics
|
|
metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \
|
|
-A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \
|
|
--resolve "$probe_domain:443:$ip" "https://$probe_domain/" \
|
|
-D "$raw_header" \
|
|
-w "%{time_connect} %{time_appconnect} %{http_code}\n" 2>/dev/null)
|
|
|
|
local tcp_sec=$(echo "$metrics" | awk '{print $1}')
|
|
local tls_sec=$(echo "$metrics" | awk '{print $2}')
|
|
local http_code=$(echo "$metrics" | awk '{print $3}')
|
|
|
|
if [ "$http_code" = "403" ]; then
|
|
touch "$TMP_DIR/blocked_${ip}"
|
|
rm -f "$raw_header"
|
|
continue
|
|
fi
|
|
|
|
local last_colo=""
|
|
if [ -f "$raw_header" ]; then
|
|
local cf_ray=$(grep -i '^cf-ray:' "$raw_header" 2>/dev/null | tr -d '\r\n')
|
|
if [ -n "$cf_ray" ]; then
|
|
last_colo=$(echo "$cf_ray" | sed -E 's/.*-([A-Za-z0-9]+).*/\1/' | tr 'a-z' 'A-Z')
|
|
fi
|
|
rm -f "$raw_header"
|
|
fi
|
|
|
|
if [ -n "$last_colo" ]; then
|
|
update_colo_cache "$ip" "$last_colo"
|
|
if ! check_colo_allowed "$last_colo"; then
|
|
log "节点 $ip 机房 ($last_colo) 不符合区域过滤策略 ($colo_filter_mode),已终止采样并排除"
|
|
touch "$TMP_DIR/colo_excluded_${ip}"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$http_code" ] && [ "$http_code" != "000" ] && [ -n "$tcp_sec" ] && [ "$tcp_sec" != "0.000000" ]; then
|
|
local tcp_ms=$(awk "BEGIN {printf \"%.2f\", $tcp_sec * 1000}")
|
|
local tls_ms=$(awk "BEGIN {printf \"%.2f\", $tls_sec * 1000}")
|
|
echo "1|$tcp_ms|$tls_ms|$last_colo" >> "$TMP_DIR/samples/$ip"
|
|
else
|
|
echo "0|0|0|$last_colo" >> "$TMP_DIR/samples/$ip"
|
|
fi
|
|
done < "$top_selected_file"
|
|
done
|
|
|
|
# 汇总统计各节点采样指标
|
|
while read -r ip; do
|
|
[ -z "$ip" ] && continue
|
|
if [ -f "$TMP_DIR/blocked_${ip}" ]; then
|
|
log "节点 $ip 触发权限阻断 (HTTP 403/企业限制),已自动排除"
|
|
continue
|
|
fi
|
|
if [ -f "$TMP_DIR/colo_excluded_${ip}" ]; then
|
|
continue
|
|
fi
|
|
|
|
local sample_file="$TMP_DIR/samples/$ip"
|
|
[ ! -f "$sample_file" ] && continue
|
|
|
|
local success_count=0
|
|
local total_tcp=0
|
|
local total_tls=0
|
|
local min_tcp=99999
|
|
local max_tcp=0
|
|
local last_colo=""
|
|
|
|
while IFS='|' read -r ok tcp_ms tls_ms colo; do
|
|
[ -n "$colo" ] && last_colo="$colo"
|
|
if [ "$ok" = "1" ]; then
|
|
success_count=$((success_count + 1))
|
|
total_tcp=$(awk "BEGIN {printf \"%.2f\", $total_tcp + $tcp_ms}")
|
|
total_tls=$(awk "BEGIN {printf \"%.2f\", $total_tls + $tls_ms}")
|
|
min_tcp=$(awk "BEGIN {if ($tcp_ms < $min_tcp) print $tcp_ms; else print $min_tcp}")
|
|
max_tcp=$(awk "BEGIN {if ($tcp_ms > $max_tcp) print $tcp_ms; else print $max_tcp}")
|
|
fi
|
|
done < "$sample_file"
|
|
|
|
if [ -z "$last_colo" ]; then
|
|
last_colo=$(get_cached_colo "$ip")
|
|
fi
|
|
|
|
if ! check_colo_allowed "$last_colo"; then
|
|
log "节点 $ip 机房 ($last_colo) 最终区域校验未通过,舍弃"
|
|
continue
|
|
fi
|
|
|
|
local loss_rate=$(awk "BEGIN {printf \"%.1f\", (1 - $success_count / $sample_count) * 100}")
|
|
if [ "$success_count" -gt 0 ]; then
|
|
local avg_tcp=$(awk "BEGIN {printf \"%.2f\", $total_tcp / $success_count}")
|
|
local avg_tls=$(awk "BEGIN {printf \"%.2f\", $total_tls / $success_count}")
|
|
local jitter=$(awk "BEGIN {printf \"%.2f\", $max_tcp - $min_tcp}")
|
|
# 综合评分:平均时延 + 抖动*0.5 + 丢包率*10(采用固定宽度 0 填充)
|
|
local score=$(awk "BEGIN {printf \"%08.2f\", $avg_tcp + $jitter * 0.5 + $loss_rate * 10}")
|
|
|
|
local colo_disp=$(get_colo_name "$last_colo")
|
|
echo "$score|$ip|$avg_tcp|$min_tcp|$max_tcp|$avg_tls|$loss_rate%|$colo_disp|$last_colo" >> "$TMP_DIR/deep_results.txt"
|
|
fi
|
|
done < "$top_selected_file"
|
|
|
|
if [ ! -s "$TMP_DIR/deep_results.txt" ]; then
|
|
log "精测采样后无可用健康节点"
|
|
rm -rf "$TMP_DIR"
|
|
rm -rf "$lock_dir"
|
|
return 1
|
|
fi
|
|
|
|
# 排序:按 score 从低到高数值排序
|
|
sort -n "$TMP_DIR/deep_results.txt" > "$TMP_DIR/deep_sorted.txt"
|
|
|
|
local best_line=$(head -n 1 "$TMP_DIR/deep_sorted.txt")
|
|
local best_score=$(echo "$best_line" | cut -d'|' -f1)
|
|
local best_ip=$(echo "$best_line" | cut -d'|' -f2)
|
|
local best_avg_tcp=$(echo "$best_line" | cut -d'|' -f3)
|
|
local best_min_tcp=$(echo "$best_line" | cut -d'|' -f4)
|
|
local best_max_tcp=$(echo "$best_line" | cut -d'|' -f5)
|
|
local best_avg_tls=$(echo "$best_line" | cut -d'|' -f6)
|
|
local best_loss=$(echo "$best_line" | cut -d'|' -f7)
|
|
local best_colo=$(echo "$best_line" | cut -d'|' -f8)
|
|
|
|
log "测速完成,最优候选: $best_ip ($best_colo), 平均延迟: ${best_avg_tcp}ms, 丢包率: $best_loss"
|
|
|
|
# 迟滞逻辑判断
|
|
local current_ip=""
|
|
[ -f "$CURRENT_IP_FILE" ] && current_ip=$(cat "$CURRENT_IP_FILE")
|
|
|
|
local final_ip="$best_ip"
|
|
local final_colo="$best_colo"
|
|
local final_tcp="$best_avg_tcp"
|
|
local final_tls="$best_avg_tls"
|
|
|
|
if [ -n "$current_ip" ] && [ "$current_ip" != "$best_ip" ]; then
|
|
local cur_info=$(grep "|$current_ip|" "$TMP_DIR/deep_sorted.txt" 2>/dev/null)
|
|
local cur_colo_allowed=1
|
|
local cur_cached_colo=$(get_cached_colo "$current_ip")
|
|
if [ -n "$cur_cached_colo" ] && ! check_colo_allowed "$cur_cached_colo"; then
|
|
cur_colo_allowed=0
|
|
fi
|
|
|
|
if [ -n "$cur_info" ] && [ "$cur_colo_allowed" -eq 1 ]; then
|
|
local cur_tcp=$(echo "$cur_info" | cut -d'|' -f3)
|
|
local cur_loss=$(echo "$cur_info" | cut -d'|' -f7)
|
|
local cur_colo=$(echo "$cur_info" | cut -d'|' -f8)
|
|
|
|
if [ "$cur_loss" = "0.0%" ]; then
|
|
# 测算相比当前在用 IP 的绝对时延差 (ms)
|
|
local diff_ms=$(awk "BEGIN {if ($cur_tcp > $best_avg_tcp) printf \"%.2f\", $cur_tcp - $best_avg_tcp; else print 0}")
|
|
local is_better=$(awk "BEGIN {print ($diff_ms >= $hysteresis_ms) ? 1 : 0}")
|
|
|
|
if [ "$is_better" -eq 0 ]; then
|
|
log "新优选 IP 优化幅度 (${diff_ms}ms) 未达迟滞阈值 (${hysteresis_ms}ms),保持当前在用 IP: $current_ip"
|
|
final_ip="$current_ip"
|
|
final_colo="$cur_colo"
|
|
final_tcp="$cur_tcp"
|
|
final_tls=$(echo "$cur_info" | cut -d'|' -f6)
|
|
else
|
|
log "新优选 IP 性能显著提升 (时延改善 ${diff_ms}ms >= ${hysteresis_ms}ms),触发切换"
|
|
fi
|
|
else
|
|
log "当前在用 IP 发生丢包 ($cur_loss),强制切换至新最优 IP: $best_ip"
|
|
fi
|
|
else
|
|
if [ "$cur_colo_allowed" -eq 0 ]; then
|
|
log "当前在用 IP ($current_ip / $cur_cached_colo) 已被机房过滤策略排除,强制切换至新最优 IP: $best_ip"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 原子写入 /tmp/hosts/cf-optimizer
|
|
local hosts_tmp="$HOSTS_FILE.tmp"
|
|
mkdir -p "$(dirname "$HOSTS_FILE")"
|
|
> "$hosts_tmp"
|
|
echo "# Auto-generated by cf-optimizer on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp"
|
|
local d
|
|
for d in $domains; do
|
|
echo "$final_ip $d" >> "$hosts_tmp"
|
|
done
|
|
mv -f "$hosts_tmp" "$HOSTS_FILE"
|
|
echo "$final_ip" > "$CURRENT_IP_FILE"
|
|
echo "$final_ip" > "$PRIMARY_IP_FILE"
|
|
echo "0" > "$FAIL_STREAK_FILE"
|
|
|
|
# 选拔次优备选节点(排名紧随其后且 IP 不同的健康节点)
|
|
local standby_ip=""
|
|
local standby_colo="无备选"
|
|
local standby_tcp="-"
|
|
while IFS='|' read -r _sc _ip _atip _minip _maxip _attls _lrate _cdisp _craw; do
|
|
if [ -n "$_ip" ] && [ "$_ip" != "$final_ip" ]; then
|
|
standby_ip="$_ip"
|
|
standby_colo="$_cdisp"
|
|
standby_tcp="$_atip"
|
|
break
|
|
fi
|
|
done < "$TMP_DIR/deep_sorted.txt"
|
|
echo "$standby_ip|$standby_colo|$standby_tcp" > "$STANDBY_IP_FILE"
|
|
echo "$final_tcp" > "$BASELINE_TCP_FILE"
|
|
|
|
# 发送 HUP 信号通知 dnsmasq 毫秒级重载
|
|
killall -HUP dnsmasq 2>/dev/null
|
|
log "已更新 $HOSTS_FILE 并热重载 dnsmasq,在用主选: $final_ip ($final_colo),次优备选: ${standby_ip:-无} ($standby_colo)"
|
|
|
|
# 组装状态 JSON 给 LuCI 前端消费
|
|
local now_str=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local domain_json="["
|
|
local first_d=1
|
|
for d in $domains; do
|
|
[ "$first_d" -eq 0 ] && domain_json="$domain_json,"
|
|
domain_json="$domain_json{\"domain\":\"$d\",\"ip\":\"$final_ip\",\"colo\":\"$final_colo\",\"tcp_lat\":\"$final_tcp ms\",\"tls_lat\":\"$final_tls ms\",\"status\":\"正常\"}"
|
|
first_d=0
|
|
done
|
|
domain_json="$domain_json]"
|
|
|
|
local rankings_json="["
|
|
local first_r=1
|
|
local rank_idx=1
|
|
while IFS='|' read -r sc ip atip minip maxip attls lrate cdisp craw; do
|
|
[ -z "$ip" ] && continue
|
|
[ "$first_r" -eq 0 ] && rankings_json="$rankings_json,"
|
|
rankings_json="$rankings_json{\"rank\":$rank_idx,\"ip\":\"$ip\",\"colo\":\"$cdisp\",\"avg_lat\":\"$atip ms\",\"lat_range\":\"$minip / $maxip ms\",\"loss_rate\":\"$lrate\"}"
|
|
first_r=0
|
|
rank_idx=$((rank_idx + 1))
|
|
[ "$rank_idx" -gt 5 ] && break
|
|
done < "$TMP_DIR/deep_sorted.txt"
|
|
rankings_json="$rankings_json]"
|
|
|
|
cat <<EOF > "$STATUS_JSON.tmp"
|
|
{
|
|
"running": true,
|
|
"last_update": "$now_str",
|
|
"best_ip": "$final_ip",
|
|
"best_colo": "$final_colo",
|
|
"best_tcp_lat": "$final_tcp ms",
|
|
"best_tls_lat": "$final_tls ms",
|
|
"standby_ip": "$standby_ip",
|
|
"standby_colo": "$standby_colo",
|
|
"standby_tcp_lat": "$standby_tcp ms",
|
|
"watchdog_status": "正常监控中",
|
|
"domains": $domain_json,
|
|
"rankings": $rankings_json
|
|
}
|
|
EOF
|
|
mv -f "$STATUS_JSON.tmp" "$STATUS_JSON"
|
|
rm -rf "$TMP_DIR"
|
|
rm -rf "$lock_dir"
|
|
return 0
|
|
}
|
|
|
|
apply_fast_failover() {
|
|
local target_ip="$1"
|
|
local target_colo="$2"
|
|
local target_tcp="${3:--}"
|
|
|
|
. /lib/functions.sh
|
|
config_load "$CONF"
|
|
local domains=""
|
|
append_failover_domain() {
|
|
local d="$1"
|
|
[ -n "$d" ] && domains="$domains $d"
|
|
}
|
|
config_list_foreach settings domains append_failover_domain
|
|
|
|
local hosts_tmp="$HOSTS_FILE.tmp"
|
|
mkdir -p "$(dirname "$HOSTS_FILE")"
|
|
> "$hosts_tmp"
|
|
echo "# Auto-generated by cf-optimizer failover on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp"
|
|
local d
|
|
for d in $domains; do
|
|
echo "$target_ip $d" >> "$hosts_tmp"
|
|
done
|
|
mv -f "$hosts_tmp" "$HOSTS_FILE"
|
|
echo "$target_ip" > "$CURRENT_IP_FILE"
|
|
echo "" > "$STANDBY_IP_FILE"
|
|
if [ -n "$target_tcp" ] && [ "$target_tcp" != "-" ]; then
|
|
echo "$target_tcp" > "$BASELINE_TCP_FILE"
|
|
else
|
|
rm -f "$BASELINE_TCP_FILE"
|
|
fi
|
|
|
|
killall -HUP dnsmasq 2>/dev/null
|
|
log "已将 hosts 紧急倒换至次优节点: $target_ip ($target_colo) 并重载 dnsmasq"
|
|
|
|
if [ -f "$STATUS_JSON" ]; then
|
|
local now_str=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local domain_json="["
|
|
local first_d=1
|
|
for d in $domains; do
|
|
[ "$first_d" -eq 0 ] && domain_json="$domain_json,"
|
|
domain_json="$domain_json{\"domain\":\"$d\",\"ip\":\"$target_ip\",\"colo\":\"$target_colo\",\"tcp_lat\":\"$target_tcp ms\",\"tls_lat\":\"-\",\"status\":\"已倒换\"}"
|
|
first_d=0
|
|
done
|
|
domain_json="$domain_json]"
|
|
|
|
local tmp_status="$STATUS_JSON.tmp"
|
|
sed -E "s/\"best_ip\":[[:space:]]*\"[^\"]*\"/\"best_ip\": \"$target_ip\"/" "$STATUS_JSON" | \
|
|
sed -E "s/\"best_colo\":[[:space:]]*\"[^\"]*\"/\"best_colo\": \"$target_colo\"/" | \
|
|
sed -E "s/\"standby_ip\":[[:space:]]*\"[^\"]*\"/\"standby_ip\": \"\"/" | \
|
|
sed -E "s/\"standby_colo\":[[:space:]]*\"[^\"]*\"/\"standby_colo\": \"已倒换转正\"/" | \
|
|
sed -E "s/\"last_update\":[[:space:]]*\"[^\"]*\"/\"last_update\": \"$now_str (已触发倒换)\"/" | \
|
|
sed -E "s/\"watchdog_status\":[[:space:]]*\"[^\"]*\"/\"watchdog_status\": \"已触发故障倒换\"/" | \
|
|
sed -E "s/\"domains\":[[:space:]]*\[[^]]*\]/\"domains\": $domain_json/" > "$tmp_status" 2>/dev/null
|
|
[ -s "$tmp_status" ] && mv -f "$tmp_status" "$STATUS_JSON"
|
|
fi
|
|
}
|
|
|
|
check_health_and_failover() {
|
|
[ ! -f "$CURRENT_IP_FILE" ] && return 0
|
|
local current_ip=$(cat "$CURRENT_IP_FILE" 2>/dev/null)
|
|
[ -z "$current_ip" ] && return 0
|
|
|
|
. /lib/functions.sh
|
|
config_load "$CONF"
|
|
|
|
local failover_enabled fail_threshold latency_multiplier
|
|
config_get_bool failover_enabled settings failover_enabled 1
|
|
[ "$failover_enabled" != "1" ] && return 0
|
|
config_get fail_threshold settings fail_threshold 2
|
|
config_get latency_multiplier settings latency_multiplier "2.0"
|
|
|
|
local domains=""
|
|
append_check_domain() {
|
|
local d="$1"
|
|
[ -n "$d" ] && domains="$domains $d"
|
|
}
|
|
config_list_foreach settings domains append_check_domain
|
|
local probe_domain=$(echo "$domains" | awk '{print $1}')
|
|
[ -z "$probe_domain" ] && probe_domain="speed.cloudflare.com"
|
|
|
|
local metrics
|
|
metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \
|
|
-A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \
|
|
--resolve "$probe_domain:443:$current_ip" "https://$probe_domain/" \
|
|
-w "%{time_connect} %{http_code}\n" 2>/dev/null)
|
|
|
|
local tcp_sec=$(echo "$metrics" | awk '{print $1}')
|
|
local http_code=$(echo "$metrics" | awk '{print $2}')
|
|
local cur_tcp_ms=0
|
|
if [ -n "$tcp_sec" ] && [ "$tcp_sec" != "0.000000" ]; then
|
|
cur_tcp_ms=$(awk "BEGIN {printf \"%.2f\", $tcp_sec * 1000}")
|
|
fi
|
|
|
|
local baseline_tcp=0
|
|
[ -f "$BASELINE_TCP_FILE" ] && baseline_tcp=$(cat "$BASELINE_TCP_FILE" 2>/dev/null)
|
|
baseline_tcp=${baseline_tcp:-0}
|
|
|
|
local streak=0
|
|
[ -f "$FAIL_STREAK_FILE" ] && streak=$(cat "$FAIL_STREAK_FILE" 2>/dev/null)
|
|
streak=${streak:-0}
|
|
|
|
local is_abnormal=0
|
|
local abnormal_reason=""
|
|
|
|
if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "403" ]; then
|
|
is_abnormal=1
|
|
abnormal_reason="HTTP ${http_code:-000} 阻断或超时"
|
|
elif [ "$latency_multiplier" != "0" ] && [ "$latency_multiplier" != "0.0" ]; then
|
|
local is_degraded
|
|
is_degraded=$(awk "BEGIN {
|
|
if ($baseline_tcp > 0 && $cur_tcp_ms >= ($baseline_tcp * $latency_multiplier) && ($cur_tcp_ms - $baseline_tcp) >= 50)
|
|
print 1;
|
|
else
|
|
print 0;
|
|
}")
|
|
if [ "$is_degraded" -eq 1 ]; then
|
|
is_abnormal=1
|
|
abnormal_reason="延迟严重劣化 (${cur_tcp_ms}ms >= 基线 ${baseline_tcp}ms x ${latency_multiplier})"
|
|
fi
|
|
fi
|
|
|
|
if [ "$is_abnormal" -eq 0 ]; then
|
|
if [ "$streak" -gt 0 ]; then
|
|
log "主用优选节点 $current_ip 探活恢复正常 (HTTP $http_code, TCP ${cur_tcp_ms}ms)"
|
|
echo "0" > "$FAIL_STREAK_FILE"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
streak=$((streak + 1))
|
|
echo "$streak" > "$FAIL_STREAK_FILE"
|
|
log "在用优选节点 $current_ip 探活异常 [$abnormal_reason],连续异常: $streak/$fail_threshold"
|
|
|
|
if [ "$streak" -ge "$fail_threshold" ]; then
|
|
echo "0" > "$FAIL_STREAK_FILE"
|
|
local standby_info=""
|
|
[ -f "$STANDBY_IP_FILE" ] && standby_info=$(cat "$STANDBY_IP_FILE" 2>/dev/null)
|
|
local standby_ip=$(echo "$standby_info" | cut -d'|' -f1)
|
|
local standby_colo=$(echo "$standby_info" | cut -d'|' -f2)
|
|
|
|
if [ -n "$standby_ip" ] && [ "$standby_ip" != "$current_ip" ]; then
|
|
log "正在核验次优备选节点健康度: $standby_ip ($standby_colo)"
|
|
local s_metrics
|
|
s_metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \
|
|
-A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \
|
|
--resolve "$probe_domain:443:$standby_ip" "https://$probe_domain/" \
|
|
-w "%{time_connect} %{http_code}\n" 2>/dev/null)
|
|
local s_tcp_sec=$(echo "$s_metrics" | awk '{print $1}')
|
|
local s_code=$(echo "$s_metrics" | awk '{print $2}')
|
|
local s_tcp_ms="-"
|
|
[ -n "$s_tcp_sec" ] && [ "$s_tcp_sec" != "0.000000" ] && s_tcp_ms=$(awk "BEGIN {printf \"%.2f\", $s_tcp_sec * 1000}")
|
|
|
|
if [ -n "$s_code" ] && [ "$s_code" != "000" ] && [ "$s_code" != "403" ]; then
|
|
log "主用节点异常,次优备选节点验证健康 (HTTP $s_code, TCP ${s_tcp_ms}ms),秒级倒换至次优节点"
|
|
apply_fast_failover "$standby_ip" "$standby_colo" "$s_tcp_ms"
|
|
return 0
|
|
else
|
|
log "次优备选节点同样不可用 (HTTP ${s_code:-000}),提前拉起全网节点紧急重测"
|
|
fi
|
|
else
|
|
log "未配置次优备选节点,提前拉起全网节点紧急重测"
|
|
fi
|
|
|
|
run_optimization
|
|
fi
|
|
}
|
|
|
|
daemon_loop() {
|
|
mkdir -p "$RUN_DIR"
|
|
echo $$ > "$PID_FILE"
|
|
trap 'rm -f "$PID_FILE"; [ -f "$RUN_DIR/task.lock/pid" ] && [ "$(cat "$RUN_DIR/task.lock/pid" 2>/dev/null)" = "$$" ] && rm -rf "$RUN_DIR/task.lock"; exit 0' INT TERM EXIT
|
|
|
|
log "守护进程启动 [PID: $$]"
|
|
|
|
local last_full_run=0
|
|
|
|
while true; do
|
|
. /lib/functions.sh
|
|
config_load "$CONF"
|
|
|
|
local enabled
|
|
config_get_bool enabled settings enabled 0
|
|
if [ "$enabled" != "1" ]; then
|
|
log "配置已禁用,清理并退出守护进程"
|
|
clean_hosts
|
|
break
|
|
fi
|
|
|
|
local interval failover_enabled health_interval
|
|
config_get interval settings interval 120
|
|
[ "$interval" -lt 10 ] && interval=10
|
|
local full_interval_sec=$((interval * 60))
|
|
|
|
config_get_bool failover_enabled settings failover_enabled 1
|
|
config_get health_interval settings health_interval 60
|
|
[ "$health_interval" -lt 15 ] && health_interval=15
|
|
|
|
local now=$(date +%s)
|
|
if [ "$last_full_run" -eq 0 ] || [ $((now - last_full_run)) -ge "$full_interval_sec" ]; then
|
|
run_optimization
|
|
last_full_run=$(date +%s)
|
|
elif [ "$failover_enabled" = "1" ]; then
|
|
check_health_and_failover
|
|
fi
|
|
|
|
local sleep_sec="$health_interval"
|
|
if [ "$failover_enabled" != "1" ]; then
|
|
sleep_sec=30
|
|
fi
|
|
|
|
local elapsed=0
|
|
while [ "$elapsed" -lt "$sleep_sec" ]; do
|
|
sleep 5 &
|
|
wait $!
|
|
elapsed=$((elapsed + 5))
|
|
done
|
|
done
|
|
}
|
|
|
|
case "$1" in
|
|
run)
|
|
run_optimization
|
|
;;
|
|
sync-hosts)
|
|
sync_hosts
|
|
;;
|
|
daemon)
|
|
daemon_loop
|
|
;;
|
|
clean)
|
|
clean_hosts
|
|
;;
|
|
status)
|
|
if [ -f "$STATUS_JSON" ]; then
|
|
cat "$STATUS_JSON"
|
|
else
|
|
echo '{"running":false,"domains":[],"rankings":[]}'
|
|
fi
|
|
;;
|
|
*)
|
|
echo "用法: $0 {run|sync-hosts|daemon|clean|status}"
|
|
exit 1
|
|
;;
|
|
esac
|