#!/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 < "$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 < "$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