From f08d189d13e2626c9a1909f6b53cdf08dcbc4c9a Mon Sep 17 00:00:00 2001 From: sindricn Date: Wed, 1 Oct 2025 10:12:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- hy2-manager.sh | 4 +- scripts/domain-test.sh | 138 ++++++++++---- scripts/outbound-manager.sh | 349 ++++++++++++++++++++++++++++-------- 4 files changed, 388 insertions(+), 108 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2e2fb07..9bb263f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -64,7 +64,10 @@ "Bash(check_rule_type_conflict:*)", "Bash(apply_outbound_rule)", "Bash(check_existing_outbound_type:*)", - "SlashCommand(/sc:analyze:*)" + "SlashCommand(/sc:analyze:*)", + "Bash(test_dns_resolution:*)", + "Bash(echo $?)", + "Bash(git checkout:*)" ], "deny": [] } diff --git a/hy2-manager.sh b/hy2-manager.sh index 2577ae1..a5eff40 100644 --- a/hy2-manager.sh +++ b/hy2-manager.sh @@ -109,7 +109,7 @@ check_script_integrity() { # 检查必需的脚本文件 local required_scripts=( - "install.sh" + # "install.sh" (在根目录) "config.sh" "service.sh" "domain-test.sh" @@ -262,7 +262,7 @@ wait_for_user() { install_hysteria() { log_info "准备安装 Hysteria2..." - if safe_source_script "$SCRIPTS_DIR/install.sh" "安装脚本"; then + if safe_source_script "$SCRIPT_DIR/install.sh" "安装脚本"; then install_hysteria2 fi wait_for_user diff --git a/scripts/domain-test.sh b/scripts/domain-test.sh index fc2c357..46efc4a 100644 --- a/scripts/domain-test.sh +++ b/scripts/domain-test.sh @@ -119,42 +119,89 @@ detect_network_quality() { fi } -# 测试单个域名延迟(优化版本) +# DNS解析验证函数 +test_dns_resolution() { + local domain=$1 + local timeout_val=${2:-3} + + # 使用dig验证DNS解析(优先) + if command -v dig >/dev/null 2>&1; then + if timeout "$timeout_val" dig +short "$domain" A >/dev/null 2>&1; then + return 0 + fi + fi + + # 使用nslookup作为备用 + if command -v nslookup >/dev/null 2>&1; then + if timeout "$timeout_val" nslookup "$domain" >/dev/null 2>&1; then + return 0 + fi + fi + + # 使用host作为最后备用 + if command -v host >/dev/null 2>&1; then + if timeout "$timeout_val" host "$domain" >/dev/null 2>&1; then + return 0 + fi + fi + + return 1 +} + +# 测试单个域名延迟(优化版本 + DNS验证) test_domain_latency() { local domain=$1 local timeout_val=${2:-3} local attempts=${3:-2} # 默认尝试2次 local results=() - + # 检查缓存 local cache_file=$(get_cache_file "$domain") if is_cache_valid "$cache_file"; then - read_from_cache "$cache_file" - return 0 + local cached_result=$(read_from_cache "$cache_file") + # 验证缓存结果是否包含DNS验证状态 + if [[ "$cached_result" =~ dns_ok|dns_fail ]]; then + echo "$cached_result" + return 0 + fi fi - - # 多次测试取最佳结果 + + # 首先验证DNS解析 + local dns_status="dns_fail" + if test_dns_resolution "$domain" "$timeout_val"; then + dns_status="dns_ok" + else + # DNS解析失败,直接返回失败状态 + local result="9999 $domain $dns_status" + write_to_cache "$cache_file" "$result" + return 1 + fi + + # 多次测试取最佳结果(只有DNS解析成功才测试连接) for ((i=1; i<=attempts; i++)); do local start_time=$(date +%s%3N) - + if timeout "$timeout_val" openssl s_client -connect "$domain:443" -servername "$domain" /dev/null 2>&1; then local end_time=$(date +%s%3N) local latency=$((end_time - start_time)) results+=($latency) fi done - + # 如果有成功的测试结果,返回最小值 if [[ ${#results[@]} -gt 0 ]]; then local min_latency=$(printf '%s\n' "${results[@]}" | sort -n | head -1) - local result="$min_latency $domain" - + local result="$min_latency $domain $dns_status" + # 写入缓存 write_to_cache "$cache_file" "$result" echo "$result" return 0 fi - + + # SSL连接失败但DNS解析成功 + local result="9999 $domain $dns_status" + write_to_cache "$cache_file" "$result" return 1 } @@ -187,14 +234,15 @@ test_all_domains_concurrent_silent() { # 等待所有任务完成 wait - # 返回排序结果 + # 返回排序结果(过滤DNS失败的域名) if [[ -s "$results_file" ]]; then - sort -n "$results_file" + # 只返回DNS解析成功且延迟不是9999的结果 + awk '$1 != 9999 && $3 == "dns_ok" {print $1 " " $2 " " $3}' "$results_file" | sort -n local exit_code=0 else local exit_code=1 fi - + rm -f "$results_file" return $exit_code } @@ -253,14 +301,15 @@ test_all_domains_concurrent() { echo -e "${GREEN}测试完成!${NC}" >&2 echo "" >&2 - # 返回排序结果 + # 返回排序结果(过滤DNS失败的域名) if [[ -s "$results_file" ]]; then - sort -n "$results_file" + # 只返回DNS解析成功且延迟不是9999的结果 + awk '$1 != 9999 && $3 == "dns_ok" {print $1 " " $2 " " $3}' "$results_file" | sort -n local exit_code=0 else local exit_code=1 fi - + # 清理临时文件 rm -f "$results_file" "$progress_file" return $exit_code @@ -281,11 +330,11 @@ show_test_results() { return 1 fi - printf "%-5s %-30s %-8s %s\n" "排名" "域名" "延迟(ms)" "评级" - echo "------------------------------------------------" - + printf "%-5s %-30s %-8s %-8s %s\n" "排名" "域名" "延迟(ms)" "DNS状态" "评级" + echo "---------------------------------------------------------" + local rank=1 - echo "$results" | head -n 10 | while read -r latency domain; do + echo "$results" | head -n 10 | while read -r latency domain dns_status; do # 延迟评级 local rating if [[ $latency -lt 50 ]]; then @@ -300,7 +349,15 @@ show_test_results() { rating="${RED}较差${NC}" fi - printf "%-5d %-30s %-8d %b\n" "$rank" "$domain" "$latency" "$rating" + # DNS状态显示 + local dns_display + if [[ "$dns_status" == "dns_ok" ]]; then + dns_display="${GREEN}正常${NC}" + else + dns_display="${RED}失败${NC}" + fi + + printf "%-5d %-30s %-8d %-8s %b\n" "$rank" "$domain" "$latency" "$dns_display" "$rating" rank=$((rank + 1)) done } @@ -310,7 +367,13 @@ get_best_domain() { local best_result=$(test_all_domains_concurrent_silent | head -n 1) if [[ -n "$best_result" ]]; then local domain=$(echo "$best_result" | awk '{print $2}') - echo "https://$domain/" + local dns_status=$(echo "$best_result" | awk '{print $3}') + # 确保DNS解析成功 + if [[ "$dns_status" == "dns_ok" ]]; then + echo "https://$domain/" + else + echo "https://news.ycombinator.com/" + fi else echo "https://news.ycombinator.com/" fi @@ -320,7 +383,14 @@ get_best_domain() { get_best_domain_name() { local best_result=$(test_all_domains_concurrent_silent | head -n 1) if [[ -n "$best_result" ]]; then - echo "$best_result" | awk '{print $2}' + local domain=$(echo "$best_result" | awk '{print $2}') + local dns_status=$(echo "$best_result" | awk '{print $3}') + # 确保DNS解析成功 + if [[ "$dns_status" == "dns_ok" ]]; then + echo "$domain" + else + echo "cdn.jsdelivr.net" + fi else echo "cdn.jsdelivr.net" fi @@ -400,8 +470,9 @@ interactive_domain_selection() { if [[ -n "$line" ]]; then local latency=$(echo "$line" | awk '{print $1}') local domain=$(echo "$line" | awk '{print $2}') + local dns_status=$(echo "$line" | awk '{print $3}') - if [[ "$latency" =~ ^[0-9]+$ ]] && [[ -n "$domain" ]] && [[ ! "$domain" =~ [[:space:]] ]]; then + if [[ "$latency" =~ ^[0-9]+$ ]] && [[ -n "$domain" ]] && [[ ! "$domain" =~ [[:space:]] ]] && [[ "$dns_status" == "dns_ok" ]]; then # 延迟评级 local rating if [[ $latency -lt 100 ]]; then @@ -411,7 +482,7 @@ interactive_domain_selection() { else rating="${RED}一般${NC}" fi - + printf "%-5d %-30s %-8d %b\n" "$index" "$domain" "$latency" "$rating" domains_array+=("$domain") index=$((index + 1)) @@ -544,8 +615,8 @@ test_custom_domains() { echo "" echo -e "${BLUE}开始并发测试 ${#custom_domains[@]} 个自定义域名...${NC}" echo "" - printf "%-30s %-10s %s\n" "域名" "延迟(ms)" "状态" - echo "------------------------------------------------" + printf "%-30s %-10s %-8s %s\n" "域名" "延迟(ms)" "DNS状态" "状态" + echo "-------------------------------------------------------" local results_file=$(mktemp) local network_quality=$(detect_network_quality) @@ -560,10 +631,17 @@ test_custom_domains() { ( if result=$(test_domain_latency "$domain" "$timeout_val" 1); then local latency=$(echo "$result" | awk '{print $1}') - printf "%-30s %-10d %s\n" "$domain" "$latency" "${GREEN}成功${NC}" + local dns_status=$(echo "$result" | awk '{print $3}') + local dns_display + if [[ "$dns_status" == "dns_ok" ]]; then + dns_display="${GREEN}正常${NC}" + else + dns_display="${RED}失败${NC}" + fi + printf "%-30s %-10d %-8s %s\n" "$domain" "$latency" "$dns_display" "${GREEN}成功${NC}" echo "$result" >> "$results_file" else - printf "%-30s %-10s %s\n" "$domain" "-" "${RED}失败${NC}" + printf "%-30s %-10s %-8s %s\n" "$domain" "-" "${RED}失败${NC}" "${RED}失败${NC}" fi ) & done diff --git a/scripts/outbound-manager.sh b/scripts/outbound-manager.sh index 6fdc576..de6d377 100644 --- a/scripts/outbound-manager.sh +++ b/scripts/outbound-manager.sh @@ -2820,54 +2820,200 @@ update_rule_config_value() { fi } -# 5. 删除出站规则 +# 获取配置文件中的所有出站规则名称 +get_config_outbound_rules() { + if [[ ! -f "$HYSTERIA_CONFIG" ]]; then + return 1 + fi + + # 提取配置文件中所有的 outbound 规则名称 + awk ' + /^[[:space:]]*outbound:[[:space:]]*$/ { in_outbound = 1; next } + in_outbound && /^[[:space:]]*[a-zA-Z]+:[[:space:]]*$/ && !/^[[:space:]]*(outbound|transport|auth|masquerade|bandwidth):/ { in_outbound = 0 } + in_outbound && /^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+)$/ { + match($0, /name:[[:space:]]*([^[:space:]]+)/, arr) + if (arr[1]) print arr[1] + } + ' "$HYSTERIA_CONFIG" 2>/dev/null +} + +# 从配置文件中删除指定的出站规则 +remove_rule_from_config() { + local rule_name="$1" + + if [[ ! -f "$HYSTERIA_CONFIG" ]]; then + log_error "配置文件不存在" + return 1 + fi + + local temp_config="/tmp/hysteria_delete_config_$$_$(date +%s).yaml" + local in_target_rule=false + local rule_found=false + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${rule_name}[[:space:]]*$ ]]; then + in_target_rule=true + rule_found=true + continue + elif [[ "$in_target_rule" == true ]]; then + # 检查是否到达下一个规则或段落 + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name: ]] || [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(type|direct|socks5|http|addr|url|mode|username|password|insecure): ]]; then + in_target_rule=false + echo "$line" >> "$temp_config" + fi + # 在目标规则中的行都跳过 + else + echo "$line" >> "$temp_config" + fi + done < "$HYSTERIA_CONFIG" + + if [[ "$rule_found" == true ]]; then + if safe_move_config "$temp_config" "$HYSTERIA_CONFIG"; then + log_success "已从配置文件中删除规则 '$rule_name'" + return 0 + else + log_error "配置文件更新失败" + rm -f "$temp_config" + return 1 + fi + else + log_warn "在配置文件中未找到规则 '$rule_name'" + rm -f "$temp_config" + return 1 + fi +} + +# 5. 删除出站规则 (增强版) delete_outbound_rule_new() { init_rules_library echo -e "${BLUE}=== 删除出站规则 ===${NC}" echo "" - # 列出规则库中的规则 - 使用可靠的grep方法 - local rules=() - local rule_count=0 - + # 收集规则库中的规则 + local library_rules=() while IFS= read -r rule_name; do if [[ -n "$rule_name" ]]; then - rules+=("$rule_name") - ((rule_count++)) - - # 检查是否已应用 - 只根据配置文件实际状态 - local status="❌ 未应用" - if [[ -f "$HYSTERIA_CONFIG" ]] && grep -q "name:[[:space:]]*[\"']*${rule_name}[\"']*[[:space:]]*$" "$HYSTERIA_CONFIG" 2>/dev/null; then - status="✅ 已应用" - fi - echo "$rule_count. $rule_name $status" + library_rules+=("$rule_name") fi done < <(grep -o "^[[:space:]]\{2\}[a-zA-Z_][a-zA-Z0-9_]*:" "$RULES_LIBRARY" | sed 's/^[[:space:]]\{2\}\([^:]*\):.*/\1/') - if [[ ${#rules[@]} -eq 0 ]]; then - echo -e "${YELLOW}没有可删除的规则${NC}" + # 收集配置文件中的规则 + local config_rules=() + while IFS= read -r rule_name; do + if [[ -n "$rule_name" ]]; then + config_rules+=("$rule_name") + fi + done < <(get_config_outbound_rules) + + # 合并去重规则列表 + local all_rules=() + local rule_sources=() # 记录规则来源: library/config/both + local rule_count=0 + + # 添加规则库中的规则 + for rule in "${library_rules[@]}"; do + all_rules+=("$rule") + rule_sources+=("library") + ((rule_count++)) + done + + # 添加配置文件中独有的规则 + for rule in "${config_rules[@]}"; do + local found_in_library=false + for lib_rule in "${library_rules[@]}"; do + if [[ "$rule" == "$lib_rule" ]]; then + found_in_library=true + # 更新来源为both + for i in "${!all_rules[@]}"; do + if [[ "${all_rules[i]}" == "$rule" ]]; then + rule_sources[i]="both" + break + fi + done + break + fi + done + + if [[ "$found_in_library" == false ]]; then + all_rules+=("$rule") + rule_sources+=("config") + ((rule_count++)) + fi + done + + if [[ ${#all_rules[@]} -eq 0 ]]; then + echo -e "${YELLOW}没有找到任何规则${NC}" wait_for_user return fi + echo -e "${CYAN}找到以下规则:${NC}" echo "" - read -p "请选择要删除的规则 [1-$rule_count]: " choice + printf "%-5s %-25s %-12s %s\n" "编号" "规则名称" "位置" "状态" + echo "---------------------------------------------------" + + for i in "${!all_rules[@]}"; do + local rule_name="${all_rules[i]}" + local source="${rule_sources[i]}" + local status="" + + # 确定位置显示 + local location_display + case "$source" in + "library") location_display="${GREEN}规则库${NC}" ;; + "config") location_display="${YELLOW}配置文件${NC}" ;; + "both") location_display="${BLUE}规则库+配置${NC}" ;; + esac + + # 检查应用状态 + if [[ -f "$HYSTERIA_CONFIG" ]] && grep -q "name:[[:space:]]*[\"']*${rule_name}[\"']*[[:space:]]*$" "$HYSTERIA_CONFIG" 2>/dev/null; then + status="✅ 已应用" + else + status="❌ 未应用" + fi + + printf "%-5d %-25s %-12s %s\n" "$((i+1))" "$rule_name" "$location_display" "$status" + done + + echo "" + echo -e "${YELLOW}说明:${NC}" + echo "• 规则库: 规则模板,可重复应用" + echo "• 配置文件: 当前活动的规则" + echo "• 规则库+配置: 存在于两个位置" + echo "" + + read -p "请选择要删除的规则编号 [1-$rule_count]: " choice if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt $rule_count ]]; then log_error "无效选择" return 1 fi - local selected_rule="${rules[$((choice-1))]}" + local selected_rule="${all_rules[$((choice-1))]}" + local selected_source="${rule_sources[$((choice-1))]}" echo "" echo -e "${RED}⚠️ 警告: 即将删除规则 '$selected_rule'${NC}" - # 检查是否已应用 - if grep -q "- $selected_rule" "$RULES_STATE" 2>/dev/null; then - echo -e "${YELLOW}此规则当前已应用,删除将同时从配置文件中移除${NC}" - fi + # 根据规则位置给出不同的提示 + case "$selected_source" in + "library") + echo -e "${YELLOW}此规则仅存在于规则库中${NC}" + ;; + "config") + echo -e "${YELLOW}此规则仅存在于配置文件中,删除后将立即生效${NC}" + ;; + "both") + echo -e "${YELLOW}此规则同时存在于规则库和配置文件中${NC}" + echo "选择删除范围:" + echo "1. 仅从规则库中删除" + echo "2. 仅从配置文件中删除" + echo "3. 同时从两个位置删除" + echo "" + read -p "请选择 [1-3]: " delete_scope + ;; + esac echo "" read -p "确认删除? [y/N]: " confirm @@ -2877,57 +3023,95 @@ delete_outbound_rule_new() { return 0 fi - # 如果已应用,先从配置中移除 - if grep -q "- $selected_rule" "$RULES_STATE" 2>/dev/null; then - echo "正在从配置文件中移除..." + local delete_success=false - # 从配置文件中删除 - if [[ -f "$HYSTERIA_CONFIG" ]]; then - local temp_config="/tmp/hysteria_delete_$$_$(date +%s).yaml" - local in_target_rule=false - - while IFS= read -r line || [[ -n "$line" ]]; do - if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${selected_rule}[[:space:]]*$ ]]; then - in_target_rule=true - continue - elif [[ "$in_target_rule" == true ]]; then - if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name: ]] || [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(type|direct|socks5|http|addr|url|mode|username|password|insecure): ]]; then - in_target_rule=false - echo "$line" >> "$temp_config" - fi - # 在规则中的行都跳过 - else - echo "$line" >> "$temp_config" - fi - done < "$HYSTERIA_CONFIG" - - safe_move_config "$temp_config" "$HYSTERIA_CONFIG" - fi - - # 从状态文件中移除 - local temp_state="${RULES_STATE}.tmp" - if awk -v rule="$selected_rule" ' - $0 == " - " rule { next } - { print } - ' "$RULES_STATE" > "$temp_state" 2>/dev/null; then - if [[ -s "$temp_state" ]]; then - mv "$temp_state" "$RULES_STATE" 2>/dev/null || rm -f "$temp_state" - else - rm -f "$temp_state" + # 执行删除操作 + case "$selected_source" in + "library") + if delete_rule_from_library "$selected_rule"; then + delete_success=true + fi + ;; + "config") + if remove_rule_from_config "$selected_rule"; then + delete_success=true + # 从状态文件中移除 + remove_rule_from_state "$selected_rule" + fi + ;; + "both") + case "$delete_scope" in + 1) + if delete_rule_from_library "$selected_rule"; then + delete_success=true + fi + ;; + 2) + if remove_rule_from_config "$selected_rule"; then + remove_rule_from_state "$selected_rule" + delete_success=true + fi + ;; + 3) + local lib_success=false + local config_success=false + + if delete_rule_from_library "$selected_rule"; then + lib_success=true + fi + + if remove_rule_from_config "$selected_rule"; then + remove_rule_from_state "$selected_rule" + config_success=true + fi + + if [[ "$lib_success" == true ]] || [[ "$config_success" == true ]]; then + delete_success=true + fi + ;; + *) + log_error "无效的删除范围选择" + return 1 + ;; + esac + ;; + esac + + if [[ "$delete_success" == true ]]; then + log_success "规则 '$selected_rule' 删除操作完成" + + # 如果删除了配置文件中的规则,询问是否重启服务 + if [[ "$selected_source" == "config" ]] || [[ "$selected_source" == "both" && ("$delete_scope" == "2" || "$delete_scope" == "3") ]]; then + echo "" + read -p "是否重启 Hysteria2 服务以应用更改? [y/N]: " restart_service + if [[ $restart_service =~ ^[Yy]$ ]]; then + if systemctl restart hysteria-server 2>/dev/null; then + log_success "服务已重启" + else + log_warn "服务重启失败,请手动重启" + fi fi - else - rm -f "$temp_state" fi + else + log_error "规则删除失败" + return 1 fi - # 从规则库中删除 - local temp_library="/tmp/rules_delete_$$_$(date +%s).yaml" + wait_for_user +} + +# 从规则库中删除规则的辅助函数 +delete_rule_from_library() { + local rule_name="$1" + local temp_library="/tmp/rules_delete_library_$$_$(date +%s).yaml" local in_target_rule=false local rule_indent="" + local rule_found=false while IFS= read -r line || [[ -n "$line" ]]; do - if [[ "$line" =~ ^[[:space:]]*${selected_rule}:[[:space:]]*$ ]]; then + if [[ "$line" =~ ^[[:space:]]*${rule_name}:[[:space:]]*$ ]]; then in_target_rule=true + rule_found=true rule_indent=$(echo "$line" | sed 's/[a-zA-Z].*//') continue elif [[ "$in_target_rule" == true ]]; then @@ -2948,24 +3132,39 @@ delete_outbound_rule_new() { fi done < "$RULES_LIBRARY" - if mv "$temp_library" "$RULES_LIBRARY"; then - log_success "规则 '$selected_rule' 已删除" - - read -p "是否重启 Hysteria2 服务? [y/N]: " restart_service - if [[ $restart_service =~ ^[Yy]$ ]]; then - if systemctl restart hysteria-server 2>/dev/null; then - log_success "服务已重启" - else - log_warn "服务重启失败,请手动重启" - fi + if [[ "$rule_found" == true ]]; then + if mv "$temp_library" "$RULES_LIBRARY"; then + log_success "已从规则库中删除规则 '$rule_name'" + return 0 + else + log_error "规则库更新失败" + rm -f "$temp_library" + return 1 fi else - log_error "规则删除失败" + log_warn "在规则库中未找到规则 '$rule_name'" rm -f "$temp_library" return 1 fi +} - wait_for_user +# 从状态文件中移除规则的辅助函数 +remove_rule_from_state() { + local rule_name="$1" + local temp_state="${RULES_STATE}.tmp" + + if [[ -f "$RULES_STATE" ]]; then + awk -v rule="$rule_name" ' + $0 == " - " rule { next } + { print } + ' "$RULES_STATE" > "$temp_state" 2>/dev/null + + if [[ -s "$temp_state" ]]; then + mv "$temp_state" "$RULES_STATE" 2>/dev/null || rm -f "$temp_state" + else + rm -f "$temp_state" + fi + fi } # ===== 并发安全和临时文件管理函数 =====