💋 Sync 2026-11-02 01:02:56

This commit is contained in:
github-actions[bot] committed 2026-11-02 01:02:56 +08:00
1 parent d97dc35e3d
commit 2f941f0ffb
48 files changed
+4891 -1002

No files matched your search

+835
View File
@@ -0,0 +1,835 @@
#!/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"
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"
[ -z "$item" ] && return
# 若为 CIDR 简单网段 (如 /24),取前 3 个 IP
case "$item" in
*/*)
local 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"
[ -z "$item" ] && return
for c in $item; do
local clean_c=$(echo "$c" | tr 'a-z' 'A-Z' | tr -d ' \t\r\n')
if [ -n "$clean_c" ]; then
case " $target_colos " in
*" $clean_c "*) ;;
*) target_colos="$target_colos $clean_c" ;;
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
for c in $target_colos; do
if [ "$c" = "$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"
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"
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"
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() {
echo $$ > "$PID_FILE"
trap 'rm -f "$PID_FILE"; 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