Files
s-hy2/scripts/outbound-manager.sh
T
sindricn d60041fcf8 更新
2025-09-29 10:45:05 +08:00

2994 lines
98 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Hysteria2 出站规则管理模块
# 功能: 配置和管理 Hysteria2 的出站规则
# 支持: Direct、SOCKS5、HTTP 代理类型
# 特性: 类型唯一性强制、具体参数修改、智能冲突检测
# 适度的错误处理
set -uo pipefail
# 加载公共库
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/common.sh" ]]; then
source "$SCRIPT_DIR/common.sh"
else
echo "错误: 无法加载公共库" >&2
exit 1
fi
# 配置路径 (防止重复定义)
if [[ -z "${HYSTERIA_CONFIG:-}" ]]; then
readonly HYSTERIA_CONFIG="/etc/hysteria/config.yaml"
fi
# 备份功能已移除
# 初始化出站管理
init_outbound_manager() {
log_info "初始化出站规则管理器"
# 模板功能已移除
}
# 显示出站管理菜单
show_outbound_menu() {
clear
echo -e "${CYAN}=== Hysteria2 出站规则管理 ===${NC}"
echo ""
echo -e "${GREEN}1.${NC} 查看出站规则"
echo -e "${GREEN}2.${NC} 新增出站规则"
echo -e "${GREEN}3.${NC} 应用规则到配置"
echo -e "${GREEN}4.${NC} 修改出站规则"
echo -e "${GREEN}5.${NC} 删除出站规则"
echo ""
echo -e "${RED}0.${NC} 返回主菜单"
echo ""
}
# 查看当前出站配置
view_current_outbound() {
log_info "查看当前出站配置"
# 使用统一的检查函数
if ! check_hysteria2_ready "config"; then
return 0 # 友好返回,不退出脚本
fi
echo -e "${BLUE}=== 当前出站配置 ===${NC}"
echo ""
# 检查是否有出站配置 - 改进的匹配模式
if grep -q "^[[:space:]]*outbounds:" "$HYSTERIA_CONFIG"; then
echo -e "${GREEN}出站规则:${NC}"
# 使用更精确的sed匹配,支持缩进
sed -n '/^[[:space:]]*outbounds:/,/^[a-zA-Z]/p' "$HYSTERIA_CONFIG" | sed '$d'
echo ""
# 显示出站规则统计
local outbound_count
outbound_count=$(grep -c "^[[:space:]]*-[[:space:]]*name:" "$HYSTERIA_CONFIG" || echo "0")
echo -e "${CYAN}共找到 $outbound_count 个出站规则${NC}"
echo ""
else
echo -e "${YELLOW}当前配置中没有出站规则(使用默认直连)${NC}"
echo ""
fi
# 检查是否有 ACL 配置 - 改进的匹配和显示
if grep -q "^[[:space:]]*acl:" "$HYSTERIA_CONFIG"; then
echo -e "${GREEN}ACL 规则:${NC}"
# 改进的ACL显示逻辑,完整显示inline内容
local in_acl=false
local acl_indent=""
while IFS= read -r line; do
if [[ "$line" =~ ^[[:space:]]*acl: ]]; then
in_acl=true
echo "$line"
# 记录ACL节点的缩进级别
acl_indent=$(echo "$line" | sed 's/acl:.*//')
elif [[ "$in_acl" == true ]]; then
# 检查是否是同级或更高级的配置节点(结束ACL显示)
if [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(inline|file): ]]; then
local line_indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
# 如果缩进级别等于或小于ACL节点,说明ACL节点结束
if [[ ${#line_indent} -le ${#acl_indent} ]]; then
break
fi
fi
echo "$line"
fi
done < "$HYSTERIA_CONFIG"
else
echo -e "${YELLOW}当前配置中没有 ACL 规则(使用默认路由)${NC}"
fi
echo ""
wait_for_user
}
# 添加新的出站规则
add_outbound_rule() {
log_info "添加新的出站规则"
echo -e "${BLUE}=== 添加出站规则 ===${NC}"
echo ""
echo -e "${YELLOW}注意: 每种类型只能有一个出站规则,添加同类型规则将覆盖现有规则${NC}"
echo ""
echo "选择出站类型:"
echo "1. Direct (直连)"
echo "2. SOCKS5 代理"
echo "3. HTTP/HTTPS 代理"
echo ""
local choice
read -p "请选择 [1-3]: " choice
# 确定选择的类型
local selected_type
case $choice in
1) selected_type="direct" ;;
2) selected_type="socks5" ;;
3) selected_type="http" ;;
*)
log_error "无效选择"
return 1
;;
esac
# 立即进行类型冲突检测
local existing_rule=""
if existing_rule=$(check_existing_outbound_type "$selected_type"); then
echo ""
echo -e "${YELLOW}⚠️ 类型冲突检测 ⚠️${NC}"
echo -e "${YELLOW}检测到现有的 ${selected_type} 类型规则: ${CYAN}$existing_rule${NC}"
echo -e "${YELLOW}根据系统设计,每种类型只能有一个出站规则${NC}"
echo ""
echo -e "${BLUE}选择操作:${NC}"
echo -e "${GREEN}1.${NC} 继续添加并覆盖现有规则 ${CYAN}$existing_rule${NC}"
echo -e "${RED}2.${NC} 取消添加操作"
echo ""
read -p "请选择 [1-2]: " conflict_choice
case $conflict_choice in
1)
echo -e "${BLUE}[INFO]${NC} 将覆盖现有的 $selected_type 规则: $existing_rule"
echo -e "${BLUE}[INFO]${NC} 继续配置新规则..."
echo ""
;;
2)
echo -e "${BLUE}[INFO]${NC} 已取消添加操作"
return 0
;;
*)
echo -e "${RED}[ERROR]${NC} 无效选择,取消操作"
return 1
;;
esac
fi
# 执行对应的配置函数,传入要覆盖的规则名称
case $choice in
1) add_direct_outbound "$existing_rule" ;;
2) add_socks5_outbound "$existing_rule" ;;
3) add_http_outbound "$existing_rule" ;;
esac
}
# 添加直连出站
add_direct_outbound() {
local existing_rule="${1:-}"
echo -e "${BLUE}=== 配置 Direct 直连出站 ===${NC}"
echo ""
local name interface ipv4 ipv6
# 获取出站名称
read -p "出站名称 (例: china_direct): " name
if [[ -z "$name" ]]; then
name="direct_out"
fi
# 是否绑定特定网卡
read -p "是否绑定特定网卡? [y/N]: " bind_interface
if [[ $bind_interface =~ ^[Yy]$ ]]; then
echo "可用网卡:"
# 优化:缓存网卡信息并使用更高效的命令
if [[ -z "${CACHED_INTERFACES:-}" ]]; then
# 使用更快的方法获取网卡列表
if command -v ip >/dev/null 2>&1; then
CACHED_INTERFACES=$(ip -o link show | awk -F': ' '{print $2}' | grep -v "lo")
else
# 降级方案
CACHED_INTERFACES=$(ls /sys/class/net/ | grep -v "lo")
fi
fi
echo "$CACHED_INTERFACES" | nl -w2 -s') '
read -p "请选择网卡名称 (例: eth0): " interface
fi
# 是否绑定特定 IP
read -p "是否绑定特定 IP 地址? [y/N]: " bind_ip
if [[ $bind_ip =~ ^[Yy]$ ]]; then
read -p "IPv4 地址 (可选): " ipv4
read -p "IPv6 地址 (可选): " ipv6
fi
# 保存配置参数供后续使用
export DIRECT_INTERFACE="$interface"
export DIRECT_IPV4="$ipv4"
export DIRECT_IPV6="$ipv6"
# 生成配置
generate_direct_config "$name" "$interface" "$ipv4" "$ipv6"
}
# 添加 SOCKS5 出站
add_socks5_outbound() {
local existing_rule="${1:-}"
echo -e "${BLUE}=== 配置 SOCKS5 代理出站 ===${NC}"
echo ""
local name addr username password
read -p "出站名称 (例: socks5_proxy): " name
if [[ -z "$name" ]]; then
name="socks5_out"
fi
read -p "代理服务器地址:端口 (例: proxy.example.com:1080): " addr
if [[ -z "$addr" ]]; then
log_error "代理地址不能为空"
return 1
fi
read -p "是否需要认证? [y/N]: " need_auth
if [[ $need_auth =~ ^[Yy]$ ]]; then
read -p "用户名: " username
read -s -p "密码: " password
echo ""
fi
# 保存配置参数供后续使用
export SOCKS5_ADDR="$addr"
export SOCKS5_USERNAME="$username"
export SOCKS5_PASSWORD="$password"
# 生成配置
generate_socks5_config "$name" "$addr" "$username" "$password"
}
# 添加 HTTP 出站
add_http_outbound() {
local existing_rule="${1:-}"
echo -e "${BLUE}=== 配置 HTTP/HTTPS 代理出站 ===${NC}"
echo ""
local name url insecure
read -p "出站名称 (例: http_proxy): " name
if [[ -z "$name" ]]; then
name="http_out"
fi
echo "代理类型:"
echo "1. HTTP 代理"
echo "2. HTTPS 代理"
read -p "选择 [1-2]: " proxy_type
if [[ $proxy_type == "1" ]]; then
read -p "HTTP 代理 URL (例: http://user:pass@proxy.com:8080): " url
else
read -p "HTTPS 代理 URL (例: https://user:pass@proxy.com:8080): " url
read -p "是否跳过 TLS 验证? [y/N]: " skip_tls
if [[ $skip_tls =~ ^[Yy]$ ]]; then
insecure="true"
else
insecure="false"
fi
fi
if [[ -z "$url" ]]; then
log_error "代理 URL 不能为空"
return 1
fi
# 保存配置参数供后续使用
export HTTP_URL="$url"
export HTTP_INSECURE="$insecure"
# 生成配置
generate_http_config "$name" "$url" "$insecure"
}
# 生成配置函数
generate_direct_config() {
local name="$1" interface="$2" ipv4="$3" ipv6="$4"
echo "生成的 Direct 出站配置:"
echo "---"
echo "outbounds:"
echo " - name: $name"
echo " type: direct"
echo " direct:"
echo " mode: auto"
if [[ -n "$interface" ]]; then
echo " bindDevice: \"$interface\""
fi
if [[ -n "$ipv4" ]]; then
echo " bindIPv4: \"$ipv4\""
fi
if [[ -n "$ipv6" ]]; then
echo " bindIPv6: \"$ipv6\""
fi
echo "---"
echo ""
apply_outbound_config "$name" "direct" "$existing_rule"
}
generate_socks5_config() {
local name="$1" addr="$2" username="$3" password="$4"
echo "生成的 SOCKS5 出站配置:"
echo "---"
echo "outbounds:"
echo " - name: $name"
echo " type: socks5"
echo " socks5:"
echo " addr: \"$addr\""
if [[ -n "$username" ]]; then
echo " username: \"$username\""
echo " password: \"$password\""
fi
echo "---"
echo ""
apply_outbound_config "$name" "socks5" "$existing_rule"
}
generate_http_config() {
local name="$1" url="$2" insecure="$3"
echo "生成的 HTTP 出站配置:"
echo "---"
echo "outbounds:"
echo " - name: $name"
echo " type: http"
echo " http:"
echo " url: \"$url\""
if [[ -n "$insecure" ]]; then
echo " insecure: $insecure"
fi
echo "---"
echo ""
apply_outbound_config "$name" "http" "$existing_rule"
}
# 应用出站配置 - 极简稳定版本
apply_outbound_config() {
local name="$1" type="$2" existing_rule="${3:-}"
read -p "是否将此配置应用到 Hysteria2 [y/N]: " apply_config
if [[ $apply_config =~ ^[Yy]$ ]]; then
echo -e "${BLUE}[INFO]${NC} 开始应用出站配置: $name ($type)"
# 使用极简稳定的方法
if apply_outbound_simple "$name" "$type" "$existing_rule"; then
echo -e "${GREEN}[SUCCESS]${NC} 出站配置已添加:$name ($type)"
# 询问是否重启服务
read -p "是否重启 Hysteria2 服务以应用配置? [y/N]: " restart_service
if [[ $restart_service =~ ^[Yy]$ ]]; then
if systemctl restart hysteria-server 2>/dev/null; then
echo -e "${GREEN}[SUCCESS]${NC} 服务已重启"
else
echo -e "${RED}[ERROR]${NC} 服务重启失败"
fi
fi
else
echo -e "${RED}[ERROR]${NC} 配置应用失败"
fi
else
echo -e "${BLUE}[INFO]${NC} 操作已取消"
fi
}
# 检查规则库中的类型冲突
check_rule_type_conflict() {
local target_type="$1"
init_rules_library
if [[ ! -f "$RULES_LIBRARY" ]]; then
return 0 # 文件不存在,没有冲突
fi
local in_rules_section=false
local current_rule_name=""
local current_rule_type=""
while IFS= read -r line; do
# 检测rules节点
if [[ "$line" =~ ^rules:[[:space:]]*$ ]]; then
in_rules_section=true
continue
fi
# 离开rules节点 - 只有0级缩进的键才退出
if [[ "$in_rules_section" == true ]] && [[ "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_]*):[[:space:]]*$ ]]; then
break
fi
# 在rules节点中
if [[ "$in_rules_section" == true ]]; then
# 检测规则名(2级缩进)
if [[ "$line" =~ ^[[:space:]]{2}([a-zA-Z_][a-zA-Z0-9_]+):[[:space:]]*$ ]]; then
current_rule_name="${BASH_REMATCH[1]}"
current_rule_type=""
fi
# 检测规则类型(4级缩进)
if [[ "$line" =~ ^[[:space:]]{4}type:[[:space:]]*(.+)$ ]]; then
current_rule_type="${BASH_REMATCH[1]}"
# 如果类型匹配,返回规则名
if [[ "$current_rule_type" == "$target_type" ]]; then
echo "$current_rule_name"
return 0
fi
fi
fi
done < "$RULES_LIBRARY"
return 1 # 没有找到冲突
}
# 检查现有同类型出站规则
check_existing_outbound_type() {
local target_type="$1"
local config_file="${2:-$HYSTERIA_CONFIG}"
if [[ ! -f "$config_file" ]]; then
return 1 # 文件不存在,没有冲突
fi
# 查找同类型的规则
local in_outbounds=false
local current_rule_type=""
local current_rule_name=""
while IFS= read -r line; do
# 检测outbounds节点
if [[ "$line" =~ ^[[:space:]]*outbounds: ]]; then
in_outbounds=true
continue
fi
# 离开outbounds节点
if [[ "$in_outbounds" == true ]] && [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*- ]]; then
in_outbounds=false
fi
# 在outbounds节点中
if [[ "$in_outbounds" == true ]]; then
# 检测规则名
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+)$ ]]; then
current_rule_name="${BASH_REMATCH[1]}"
current_rule_name=$(echo "$current_rule_name" | xargs) # 去除前后空格
fi
# 检测规则类型
if [[ "$line" =~ ^[[:space:]]*type:[[:space:]]*(.+)$ ]]; then
current_rule_type="${BASH_REMATCH[1]}"
current_rule_type=$(echo "$current_rule_type" | xargs) # 去除前后空格
# 检查是否与目标类型匹配
if [[ "$current_rule_type" == "$target_type" ]]; then
echo "$current_rule_name" # 返回现有同类型规则的名称
return 0
fi
fi
fi
done < "$config_file"
return 1 # 未找到同类型规则
}
# 静默删除指定规则(用于类型覆盖,无用户确认)
delete_existing_rule_silent() {
local rule_name="$1"
echo -e "${BLUE}[INFO]${NC} 正在删除现有规则: $rule_name"
# 创建临时文件
local temp_config="/tmp/hysteria_delete_temp_$(date +%s).yaml"
# 智能删除逻辑:完整删除outbound规则和相关ACL条目
local in_outbound_rule=false
local in_acl_section=false
local acl_base_indent=""
while IFS= read -r line || [[ -n "$line" ]]; do
local should_keep=true
# 1. 删除包含规则名的注释
if [[ "$line" =~ ^[[:space:]]*#.*${rule_name} ]]; then
should_keep=false
fi
# 2. 检测outbound规则块
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${rule_name}[[:space:]]*$ ]]; then
in_outbound_rule=true
should_keep=false
elif [[ "$in_outbound_rule" == true ]]; then
# 在outbound规则块中,检查是否结束
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_outbound_rule=false
should_keep=true
else
should_keep=false # 删除outbound规则块内的所有行
fi
fi
# 3. 检测ACL节点
if [[ "$line" =~ ^[[:space:]]*acl: ]]; then
in_acl_section=true
acl_base_indent=$(echo "$line" | sed 's/acl:.*//')
should_keep=true
elif [[ "$in_acl_section" == true ]]; then
# 检查是否离开ACL节点
if [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(inline|file): ]]; then
local line_indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
if [[ ${#line_indent} -le ${#acl_base_indent} ]]; then
in_acl_section=false
should_keep=true
fi
fi
# 在ACL节点中处理 - 删除包含目标规则名的行
if [[ "$in_acl_section" == true ]] && [[ "$line" =~ ${rule_name} ]]; then
should_keep=false # 删除ACL中包含目标规则名的条目
fi
fi
# 写入保留的行
if [[ "$should_keep" == true ]]; then
echo "$line" >> "$temp_config"
fi
done < "$HYSTERIA_CONFIG"
# 检查删除是否成功
if grep -q "name: *$rule_name" "$temp_config" 2>/dev/null; then
echo -e "${RED}[ERROR]${NC} 删除失败,规则仍存在"
rm -f "$temp_config"
return 1
fi
# 应用修改
if mv "$temp_config" "$HYSTERIA_CONFIG" 2>/dev/null; then
echo -e "${GREEN}[SUCCESS]${NC} 现有规则 '$rule_name' 已删除"
return 0
else
echo -e "${RED}[ERROR]${NC} 删除失败,文件操作错误"
rm -f "$temp_config"
return 1
fi
}
# 删除配置文件中的指定outbound规则(用于覆盖操作)
delete_existing_outbound_from_config() {
local rule_name="$1"
local config_file="${2:-$HYSTERIA_CONFIG}"
if [[ -z "$rule_name" ]]; then
log_error "规则名称不能为空"
return 1
fi
if [[ ! -f "$config_file" ]]; then
log_warn "配置文件不存在: $config_file"
return 0 # 文件不存在视为成功删除
fi
# 检查文件是否可写
if [[ ! -w "$config_file" ]]; then
log_warn "配置文件无写权限: $config_file"
return 1
fi
echo -e "${BLUE}[INFO]${NC} 从配置文件中删除规则: $rule_name"
# 创建临时文件
local temp_config
temp_config=$(create_delete_temp_file)
# 删除指定的outbound规则
local in_outbound_rule=false
local in_outbounds_section=false
while IFS= read -r line || [[ -n "$line" ]]; do
local should_keep=true
# 检测outbounds节点
if [[ "$line" =~ ^[[:space:]]*outbounds:[[:space:]]*$ ]]; then
in_outbounds_section=true
should_keep=true
elif [[ "$in_outbounds_section" == true ]]; then
# 在outbounds节点中
# 检测目标规则开始
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${rule_name}[[:space:]]*$ ]]; then
in_outbound_rule=true
should_keep=false
elif [[ "$in_outbound_rule" == true ]]; then
# 在目标规则块中,检查是否结束
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name: ]] || [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(type|direct|socks5|http): ]]; then
# 遇到下一个规则或顶级节点,结束当前规则删除
in_outbound_rule=false
# 检查是否离开outbounds节点
if [[ "$line" =~ ^[a-zA-Z]+:[[:space:]]*$ ]]; then
in_outbounds_section=false
fi
should_keep=true
else
# 仍在目标规则块中,继续删除
should_keep=false
fi
else
# 不在目标规则块中,检查是否离开outbounds节点
if [[ "$line" =~ ^[a-zA-Z]+:[[:space:]]*$ ]]; then
in_outbounds_section=false
fi
should_keep=true
fi
fi
# 保留需要的行
if [[ "$should_keep" == true ]]; then
echo "$line" >> "$temp_config"
fi
done < "$config_file"
# 替换原文件 - 增强错误处理
if mv "$temp_config" "$config_file" 2>/dev/null; then
echo -e "${GREEN}[SUCCESS]${NC} 规则 '$rule_name' 已从配置文件中删除"
return 0
elif cp "$temp_config" "$config_file" 2>/dev/null; then
# mv失败时尝试cp
rm -f "$temp_config"
echo -e "${GREEN}[SUCCESS]${NC} 规则 '$rule_name' 已从配置文件中删除"
return 0
else
log_error "删除规则失败: 文件操作错误,可能是权限问题"
log_info "临时文件保存在: $temp_config"
return 1
fi
}
# 极简稳定的配置应用函数
apply_outbound_simple() {
local name="$1" type="$2" existing_rule="${3:-}"
echo -e "${BLUE}[INFO]${NC} 检查配置文件: $HYSTERIA_CONFIG"
# 检查配置文件
if [[ ! -f "$HYSTERIA_CONFIG" ]]; then
echo -e "${RED}[ERROR]${NC} 配置文件不存在: $HYSTERIA_CONFIG"
return 1
fi
# 如果有要覆盖的规则,先删除它
if [[ -n "$existing_rule" ]]; then
echo -e "${BLUE}[INFO]${NC} 删除现有规则: $existing_rule"
if ! delete_existing_rule_silent "$existing_rule"; then
echo -e "${RED}[ERROR]${NC} 删除现有规则失败"
return 1
fi
fi
# 直接操作,不创建不必要的备份
# 创建临时文件
local temp_file="/tmp/hysteria_temp_$$_$(date +%s).yaml"
echo -e "${BLUE}[INFO]${NC} 创建临时文件: $temp_file"
if ! cp "$HYSTERIA_CONFIG" "$temp_file" 2>/dev/null; then
echo -e "${RED}[ERROR]${NC} 无法创建临时文件"
return 1
fi
# 添加出站配置
echo -e "${BLUE}[INFO]${NC} 添加出站配置到临时文件"
if grep -q "^[[:space:]]*outbounds:" "$temp_file" 2>/dev/null; then
echo -e "${BLUE}[INFO]${NC} 检测到现有outbounds配置,插入新规则"
# 创建新的临时文件用于正确插入
local temp_file2="/tmp/hysteria_merge_$$_$(date +%s).yaml"
local in_outbounds=false
local inserted=false
while IFS= read -r line || [[ -n "$line" ]]; do
# 检测outbounds节点开始
if [[ "$line" =~ ^[[:space:]]*outbounds: ]]; then
in_outbounds=true
echo "$line" >> "$temp_file2"
continue
fi
# 在outbounds节点中,找到合适位置插入
if [[ "$in_outbounds" == true ]] && [[ "$inserted" == false ]]; then
# 如果遇到其他顶级节点,在此之前插入新规则
if [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*- ]]; then
# 插入新规则
cat >> "$temp_file2" << EOF
# 新增出站规则 - $name ($type)
- name: $name
type: $type
EOF
case $type in
"direct")
echo " direct:" >> "$temp_file2"
echo " mode: auto" >> "$temp_file2"
;;
"socks5")
echo " socks5:" >> "$temp_file2"
echo " addr: \"${SOCKS5_ADDR:-127.0.0.1:1080}\"" >> "$temp_file2"
if [[ -n "${SOCKS5_USERNAME:-}" ]]; then
echo " username: \"$SOCKS5_USERNAME\"" >> "$temp_file2"
echo " password: \"$SOCKS5_PASSWORD\"" >> "$temp_file2"
fi
;;
"http")
echo " http:" >> "$temp_file2"
echo " url: \"${HTTP_URL:-http://127.0.0.1:8080}\"" >> "$temp_file2"
if [[ -n "${HTTP_INSECURE:-}" ]]; then
echo " insecure: $HTTP_INSECURE" >> "$temp_file2"
fi
;;
esac
echo "" >> "$temp_file2"
inserted=true
in_outbounds=false
fi
fi
echo "$line" >> "$temp_file2"
done < "$temp_file"
# 如果在文件末尾仍未插入,在outbounds节点末尾添加
if [[ "$inserted" == false ]] && [[ "$in_outbounds" == true ]]; then
cat >> "$temp_file2" << EOF
# 新增出站规则 - $name ($type)
- name: $name
type: $type
EOF
case $type in
"direct")
echo " direct:" >> "$temp_file2"
echo " mode: auto" >> "$temp_file2"
;;
"socks5")
echo " socks5:" >> "$temp_file2"
echo " addr: \"${SOCKS5_ADDR:-127.0.0.1:1080}\"" >> "$temp_file2"
;;
"http")
echo " http:" >> "$temp_file2"
echo " url: \"${HTTP_URL:-http://127.0.0.1:8080}\"" >> "$temp_file2"
;;
esac
fi
# 替换原文件
mv "$temp_file2" "$temp_file"
# 智能ACL规则同步
echo -e "${BLUE}[INFO]${NC} 同步ACL路由规则"
if grep -q "^[[:space:]]*acl:" "$temp_file" 2>/dev/null; then
echo -e "${BLUE}[INFO]${NC} 检测到现有ACL规则,智能添加路由条目"
# 创建ACL添加的临时文件
local temp_acl="/tmp/hysteria_acl_add_$$_$(date +%s).yaml"
local in_acl_section=false
local in_inline_section=false
local acl_base_indent=""
local added_acl_rule=false
while IFS= read -r line || [[ -n "$line" ]]; do
# 检测ACL节点
if [[ "$line" =~ ^[[:space:]]*acl: ]]; then
in_acl_section=true
acl_base_indent=$(echo "$line" | sed 's/acl:.*//')
echo "$line" >> "$temp_acl"
continue
fi
# 在ACL节点中
if [[ "$in_acl_section" == true ]]; then
# 检查是否离开ACL节点
if [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(inline|file): ]]; then
local line_indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
if [[ ${#line_indent} -le ${#acl_base_indent} ]]; then
# 离开ACL节点前,如果还没添加规则,则添加
if [[ "$added_acl_rule" == false ]]; then
echo " - ${name}(all) # 新增出站规则" >> "$temp_acl"
added_acl_rule=true
fi
in_acl_section=false
in_inline_section=false
fi
fi
# 检测inline节点
if [[ "$line" =~ ^[[:space:]]*inline:[[:space:]]*$ ]]; then
in_inline_section=true
echo "$line" >> "$temp_acl"
continue
fi
# 在inline节点中,添加新规则(在第一个条目后)
if [[ "$in_inline_section" == true ]] && [[ "$added_acl_rule" == false ]] && [[ "$line" =~ ^[[:space:]]*-[[:space:]] ]]; then
echo "$line" >> "$temp_acl"
echo " - ${name}(all) # 新增出站规则" >> "$temp_acl"
added_acl_rule=true
continue
fi
fi
echo "$line" >> "$temp_acl"
done < "$temp_file"
# 如果文件末尾仍在ACL中且未添加规则
if [[ "$in_acl_section" == true ]] && [[ "$added_acl_rule" == false ]]; then
echo " - ${name}(all) # 新增出站规则" >> "$temp_acl"
fi
# 替换原文件
mv "$temp_acl" "$temp_file"
else
echo -e "${BLUE}[INFO]${NC} 创建新的ACL规则配置"
cat >> "$temp_file" << EOF
# ACL规则 - 路由配置
acl:
inline:
- ${name}(all) # 新增出站规则路由
EOF
fi
else
echo -e "${BLUE}[INFO]${NC} 未检测到outbounds配置,创建新节点"
case $type in
"direct")
cat >> "$temp_file" << EOF
# 出站规则配置
outbounds:
- name: $name
type: direct
direct:
mode: auto
# ACL规则 - 路由配置
acl:
inline:
- $name(all) # 所有流量通过此规则直连
EOF
;;
"socks5")
cat >> "$temp_file" << EOF
# 出站规则配置
outbounds:
- name: $name
type: socks5
socks5:
addr: "${SOCKS5_ADDR:-127.0.0.1:1080}"
# ACL规则 - 路由配置
acl:
inline:
- $name(all) # 所有流量通过此规则代理
EOF
;;
esac
fi
# 语法验证功能已移除 - 验证结果不准确且没有实际作用
# 应用配置
echo -e "${BLUE}[INFO]${NC} 应用新配置"
if mv "$temp_file" "$HYSTERIA_CONFIG" 2>/dev/null; then
echo -e "${GREEN}[SUCCESS]${NC} 配置已成功应用"
return 0
else
echo -e "${RED}[ERROR]${NC} 配置应用失败"
rm -f "$temp_file" 2>/dev/null
return 1
fi
}
# 创建安全的临时文件 - 兼容性改进版
create_temp_config() {
local temp_config
# 尝试不同的mktemp选项以确保兼容性
if command -v mktemp >/dev/null 2>&1; then
# 尝试标准方式
if temp_config=$(mktemp -t hysteria_config_XXXXXX.yaml 2>/dev/null); then
log_debug "使用mktemp -t创建临时文件: $temp_config"
# 备选方式1: 不使用-t选项
elif temp_config=$(mktemp /tmp/hysteria_config_XXXXXX.yaml 2>/dev/null); then
log_debug "使用mktemp备选方式创建临时文件: $temp_config"
# 备选方式2: 手动创建
else
temp_config="/tmp/hysteria_config_$$_$(date +%s).yaml"
if ! touch "$temp_config" 2>/dev/null; then
log_error "无法创建临时文件: $temp_config"
return 1
fi
log_debug "手动创建临时文件: $temp_config"
fi
else
# 如果没有mktemp命令,手动创建
temp_config="/tmp/hysteria_config_$$_$(date +%s).yaml"
if ! touch "$temp_config" 2>/dev/null; then
log_error "无法创建临时文件: $temp_config"
return 1
fi
log_debug "手动创建临时文件(无mktemp: $temp_config"
fi
# 设置适当权限
if ! chmod 600 "$temp_config" 2>/dev/null; then
log_warn "无法设置临时文件权限,继续执行"
fi
echo "$temp_config"
}
# 智能合并outbounds配置
merge_outbound_config() {
local config_file="$1" name="$2" type="$3"
# 检查是否已存在outbounds节点
if grep -q "^[[:space:]]*outbounds:" "$config_file"; then
log_info "检测到现有outbounds配置,添加到现有列表"
add_to_existing_outbounds "$config_file" "$name" "$type"
else
log_info "未检测到outbounds配置,创建新的outbounds节点"
add_new_outbounds_section "$config_file" "$name" "$type"
fi
}
# 添加到现有outbounds列表
add_to_existing_outbounds() {
local config_file="$1" name="$2" type="$3"
case $type in
"direct")
# 在outbounds节点下添加新项
cat >> "$config_file" << EOF
# 新增出站规则 - $name (Direct)
- name: $name
type: direct
direct:
mode: auto
EOF
if [[ -n "${DIRECT_INTERFACE:-}" ]]; then
echo " bindDevice: \"$DIRECT_INTERFACE\"" >> "$config_file"
fi
if [[ -n "${DIRECT_IPV4:-}" ]]; then
echo " bindIPv4: \"$DIRECT_IPV4\"" >> "$config_file"
fi
if [[ -n "${DIRECT_IPV6:-}" ]]; then
echo " bindIPv6: \"$DIRECT_IPV6\"" >> "$config_file"
fi
;;
"socks5")
cat >> "$config_file" << EOF
# 新增出站规则 - $name (SOCKS5)
- name: $name
type: socks5
socks5:
addr: "${SOCKS5_ADDR:-proxy.example.com:1080}"
EOF
if [[ -n "${SOCKS5_USERNAME:-}" ]]; then
echo " username: \"$SOCKS5_USERNAME\"" >> "$config_file"
echo " password: \"$SOCKS5_PASSWORD\"" >> "$config_file"
fi
;;
"http")
cat >> "$config_file" << EOF
# 新增出站规则 - $name (HTTP)
- name: $name
type: http
http:
url: "${HTTP_URL:-http://proxy.example.com:8080}"
EOF
if [[ -n "${HTTP_INSECURE:-}" ]]; then
echo " insecure: $HTTP_INSECURE" >> "$config_file"
fi
;;
esac
}
# 创建新的outbounds节点
add_new_outbounds_section() {
local config_file="$1" name="$2" type="$3"
echo "" >> "$config_file"
echo "# 出站规则配置" >> "$config_file"
generate_direct_yaml_config "$name" >> "$config_file"
}
# 实际应用配置到文件的函数 - 改进版
apply_outbound_to_config() {
local name="$1" type="$2"
# 检查配置文件是否存在
if [[ ! -f "$HYSTERIA_CONFIG" ]]; then
log_error "Hysteria2 配置文件不存在: $HYSTERIA_CONFIG"
return 1
fi
# 创建安全的临时文件
local temp_config
log_info "开始创建临时文件..."
temp_config=$(create_temp_config)
if [[ $? -ne 0 ]] || [[ -z "$temp_config" ]]; then
log_error "创建临时文件失败"
return 1
fi
log_info "临时文件已创建: $temp_config"
# 复制原配置并检查结果
log_info "复制配置文件到临时位置..."
if ! cp "$HYSTERIA_CONFIG" "$temp_config"; then
log_error "无法复制配置文件到临时位置"
log_error "源文件: $HYSTERIA_CONFIG"
log_error "目标文件: $temp_config"
rm -f "$temp_config"
return 1
fi
log_info "配置文件复制成功"
# 备份功能已移除,直接应用配置
# 智能合并配置
case $type in
"direct"|"socks5"|"http")
merge_outbound_config "$temp_config" "$name" "$type"
;;
*)
log_error "不支持的出站类型: $type"
rm -f "$temp_config"
return 1
;;
esac
# 语法验证功能已移除 - 验证结果不准确且没有实际作用
# 原子性替换配置文件
if mv "$temp_config" "$HYSTERIA_CONFIG"; then
log_success "配置已成功应用到: $HYSTERIA_CONFIG"
return 0
else
log_error "配置应用失败,请检查文件权限和磁盘空间"
rm -f "$temp_config"
return 1
fi
}
# 生成 Direct 类型的 YAML 配置
generate_direct_yaml_config() {
local name="$1"
echo ""
echo "# 出站规则 - $name (Direct)"
echo "outbounds:"
echo " - name: $name"
echo " type: direct"
echo " direct:"
echo " mode: auto"
if [[ -n "${DIRECT_INTERFACE:-}" ]]; then
echo " bindDevice: \"$DIRECT_INTERFACE\""
fi
if [[ -n "${DIRECT_IPV4:-}" ]]; then
echo " bindIPv4: \"$DIRECT_IPV4\""
fi
if [[ -n "${DIRECT_IPV6:-}" ]]; then
echo " bindIPv6: \"$DIRECT_IPV6\""
fi
}
# 生成 SOCKS5 类型的 YAML 配置
generate_socks5_yaml_config() {
local name="$1"
echo ""
echo "# 出站规则 - $name (SOCKS5)"
echo "outbounds:"
echo " - name: $name"
echo " type: socks5"
echo " socks5:"
echo " addr: \"${SOCKS5_ADDR:-proxy.example.com:1080}\""
if [[ -n "${SOCKS5_USERNAME:-}" ]]; then
echo " username: \"$SOCKS5_USERNAME\""
echo " password: \"$SOCKS5_PASSWORD\""
fi
}
# 生成 HTTP 类型的 YAML 配置
generate_http_yaml_config() {
local name="$1"
echo ""
echo "# 出站规则 - $name (HTTP)"
echo "outbounds:"
echo " - name: $name"
echo " type: http"
echo " http:"
echo " url: \"${HTTP_URL:-http://proxy.example.com:8080}\""
if [[ -n "${HTTP_INSECURE:-}" ]]; then
echo " insecure: $HTTP_INSECURE"
fi
}
# 备份当前配置
# 备份功能已移除
# 修改现有出站配置
modify_outbound_config() {
log_info "修改现有出站配置"
echo -e "${BLUE}=== 修改出站配置 ===${NC}"
echo ""
# 检查是否有出站配置
if ! grep -q "^outbounds:" "$HYSTERIA_CONFIG"; then
echo -e "${YELLOW}当前没有出站配置可修改${NC}"
echo "请先添加出站规则"
wait_for_user
return
fi
# 列出现有的出站配置
echo -e "${GREEN}当前出站规则:${NC}"
local outbound_names=($(grep -A 1 "^[[:space:]]*-[[:space:]]*name:" "$HYSTERIA_CONFIG" | grep "name:" | sed 's/.*name:[[:space:]]*//' | tr -d '"'))
if [[ ${#outbound_names[@]} -eq 0 ]]; then
echo -e "${YELLOW}没有找到出站规则名称${NC}"
wait_for_user
return
fi
for i in "${!outbound_names[@]}"; do
echo "$((i+1)). ${outbound_names[$i]}"
done
echo ""
read -p "请选择要修改的出站规则 [1-${#outbound_names[@]}]: " choice
if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt ${#outbound_names[@]} ]]; then
log_error "无效选择"
return
fi
local selected_outbound="${outbound_names[$((choice-1))]}"
echo -e "${BLUE}修改选项:${NC}"
echo "1. 修改规则名称"
echo "2. 修改服务器地址"
echo "3. 修改用户名"
echo "4. 修改密码"
echo "5. 删除此出站规则"
echo ""
read -p "请选择操作 [1-5]: " modify_choice
case $modify_choice in
1) modify_rule_name "$selected_outbound" ;;
2) modify_server_address "$selected_outbound" ;;
3) modify_username "$selected_outbound" ;;
4) modify_password "$selected_outbound" ;;
5) delete_outbound_rule "$selected_outbound" ;;
*)
log_error "无效选择"
;;
esac
}
# 删除出站规则
delete_outbound_rule() {
local rule_name="$1"
echo -e "${RED}[WARNING]${NC} 即将删除出站规则: $rule_name"
echo -e "${YELLOW}此操作不可逆,请确认操作${NC}"
echo -n "确认删除? [y/N]: "
local confirm
read -r confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
echo -e "${BLUE}[INFO]${NC} 取消删除操作"
return
fi
echo -e "${BLUE}[INFO]${NC} 开始删除出站规则: $rule_name"
# 直接删除,不创建不必要的备份
# 创建临时文件
local temp_config="/tmp/hysteria_delete_temp_$(date +%s).yaml"
# 智能删除逻辑:完整删除outbound规则和相关ACL条目
local in_outbound_rule=false
local in_acl_section=false
local acl_base_indent=""
local delete_acl_inline=false
while IFS= read -r line || [[ -n "$line" ]]; do
local should_keep=true
# 1. 删除包含规则名的注释
if [[ "$line" =~ ^[[:space:]]*#.*${rule_name} ]]; then
should_keep=false
fi
# 2. 检测outbound规则块
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${rule_name}[[:space:]]*$ ]]; then
in_outbound_rule=true
should_keep=false
elif [[ "$in_outbound_rule" == true ]]; then
# 在outbound规则块中,检查是否结束
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_outbound_rule=false
should_keep=true
else
should_keep=false # 删除outbound规则块内的所有行
fi
fi
# 3. 检测ACL节点
if [[ "$line" =~ ^[[:space:]]*acl:[[:space:]]*$ ]]; then
in_acl_section=true
acl_base_indent=$(echo "$line" | sed 's/acl:.*//')
should_keep=true
elif [[ "$line" =~ ^[[:space:]]*acl: ]]; then
in_acl_section=true
acl_base_indent=$(echo "$line" | sed 's/acl:.*//')
should_keep=true
elif [[ "$in_acl_section" == true ]]; then
# 检查是否离开ACL节点
if [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(inline|file): ]]; then
local line_indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
if [[ ${#line_indent} -le ${#acl_base_indent} ]]; then
in_acl_section=false
should_keep=true
fi
fi
# 在ACL节点中处理
if [[ "$in_acl_section" == true ]]; then
# 检测inline节点开始
if [[ "$line" =~ ^[[:space:]]*inline:[[:space:]]*$ ]]; then
delete_acl_inline=false
should_keep=true
# 在inline节点中检查包含目标规则名的行
elif [[ "$line" =~ ${rule_name} ]]; then
should_keep=false # 删除ACL中包含目标规则名的条目
elif [[ "$line" =~ ^[[:space:]]*-[[:space:]]*${rule_name}[[:space:]]*$ ]]; then
should_keep=false # 删除单独的规则名条目
else
should_keep=true
fi
fi
fi
# 写入保留的行
if [[ "$should_keep" == true ]]; then
echo "$line" >> "$temp_config"
fi
done < "$HYSTERIA_CONFIG"
# 检查删除是否成功
if grep -q "name: *$rule_name" "$temp_config" 2>/dev/null; then
echo -e "${RED}[ERROR]${NC} 删除失败,规则仍存在"
rm -f "$temp_config"
return 1
fi
# 应用修改
if mv "$temp_config" "$HYSTERIA_CONFIG" 2>/dev/null; then
echo -e "${GREEN}[SUCCESS]${NC} 出站规则 '$rule_name' 已删除"
# 询问是否重启服务
echo ""
read -p "是否重启 Hysteria2 服务以应用配置? [y/N]: " restart_service
if [[ $restart_service =~ ^[Yy]$ ]]; then
if systemctl restart hysteria-server 2>/dev/null; then
echo -e "${GREEN}[SUCCESS]${NC} 服务已重启"
else
echo -e "${YELLOW}[WARN]${NC} 服务重启失败,请手动重启"
fi
fi
else
echo -e "${RED}[ERROR]${NC} 配置应用失败"
return 1
fi
echo ""
wait_for_user
}
# 备份和恢复配置
# 备份功能已移除
# 恢复配置备份
# 备份功能已移除
# 列出配置备份
# 备份功能已移除
# 主出站管理函数
manage_outbound() {
init_outbound_manager
while true; do
show_outbound_menu
local choice
read -p "请选择操作 [0-5]: " choice
case $choice in
1) view_outbound_rules ;;
2) add_outbound_rule_new ;;
3) apply_outbound_rule ;;
4) modify_outbound_rule ;;
5) delete_outbound_rule_new ;;
0)
log_info "返回主菜单"
break
;;
*)
log_error "无效选择,请重新输入"
wait_for_user
;;
esac
done
}
# 修改规则名称
modify_rule_name() {
local old_name="$1"
echo -e "${BLUE}=== 修改规则名称 ===${NC}"
echo "当前规则名称: ${CYAN}$old_name${NC}"
echo ""
read -p "请输入新的规则名称: " new_name
if [[ -z "$new_name" ]]; then
log_error "规则名称不能为空"
return
fi
# 检查新名称是否已存在
if grep -q "name: *$new_name" "$HYSTERIA_CONFIG" 2>/dev/null; then
log_error "规则名称 '$new_name' 已存在"
return
fi
# 执行替换
if sed -i.bak "s/name: *$old_name/name: $new_name/g" "$HYSTERIA_CONFIG" 2>/dev/null; then
# 同时更新ACL中的引用
sed -i.bak "s/- $old_name/- $new_name/g" "$HYSTERIA_CONFIG" 2>/dev/null
rm -f "$HYSTERIA_CONFIG.bak"
log_success "规则名称已更新: $old_name$new_name"
ask_restart_service
else
log_error "修改失败"
fi
}
# 修改服务器地址
modify_server_address() {
local rule_name="$1"
echo -e "${BLUE}=== 修改服务器地址 ===${NC}"
echo "规则名称: ${CYAN}$rule_name${NC}"
echo ""
# 获取当前地址
local current_addr=$(sed -n "/- name: $rule_name/,/^ - name:/p" "$HYSTERIA_CONFIG" | grep -E "(addr|url):" | head -1 | sed 's/.*: *//')
if [[ -n "$current_addr" ]]; then
echo "当前地址: ${YELLOW}$current_addr${NC}"
fi
read -p "请输入新的服务器地址: " new_addr
if [[ -z "$new_addr" ]]; then
log_error "服务器地址不能为空"
return
fi
# 创建临时文件进行修改
local temp_config="/tmp/hysteria_modify_addr_$(date +%s).yaml"
local in_target_rule=false
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${rule_name}[[:space:]]*$ ]]; then
in_target_rule=true
echo "$line" >> "$temp_config"
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"
elif [[ "$line" =~ ^[[:space:]]*(addr|url):[[:space:]]* ]]; then
local indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
if [[ "$line" =~ addr: ]]; then
echo "${indent}addr: $new_addr" >> "$temp_config"
else
echo "${indent}url: $new_addr" >> "$temp_config"
fi
else
echo "$line" >> "$temp_config"
fi
else
echo "$line" >> "$temp_config"
fi
done < "$HYSTERIA_CONFIG"
if mv "$temp_config" "$HYSTERIA_CONFIG" 2>/dev/null; then
log_success "服务器地址已更新"
ask_restart_service
else
log_error "修改失败"
rm -f "$temp_config"
fi
}
# 修改用户名
modify_username() {
local rule_name="$1"
echo -e "${BLUE}=== 修改用户名 ===${NC}"
echo "规则名称: ${CYAN}$rule_name${NC}"
echo ""
# 获取当前用户名
local current_username=$(sed -n "/- name: $rule_name/,/^ - name:/p" "$HYSTERIA_CONFIG" | grep "username:" | sed 's/.*username: *//' | tr -d '"')
if [[ -n "$current_username" ]]; then
echo "当前用户名: ${YELLOW}$current_username${NC}"
fi
read -p "请输入新的用户名 (留空则删除): " new_username
# 修改用户名
modify_config_field "$rule_name" "username" "$new_username"
}
# 修改密码
modify_password() {
local rule_name="$1"
echo -e "${BLUE}=== 修改密码 ===${NC}"
echo "规则名称: ${CYAN}$rule_name${NC}"
echo ""
read -s -p "请输入新密码 (留空则删除): " new_password
echo ""
# 修改密码
modify_config_field "$rule_name" "password" "$new_password"
}
# 通用配置字段修改函数
modify_config_field() {
local rule_name="$1"
local field_name="$2"
local new_value="$3"
local temp_config="/tmp/hysteria_modify_${field_name}_$(date +%s).yaml"
local in_target_rule=false
local field_found=false
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*${rule_name}[[:space:]]*$ ]]; then
in_target_rule=true
echo "$line" >> "$temp_config"
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
# 如果没找到字段且有新值,在规则结束前插入
if [[ "$field_found" == false && -n "$new_value" ]]; then
local base_indent=" " # 假设基础缩进
echo "${base_indent}${field_name}: $new_value" >> "$temp_config"
fi
in_target_rule=false
echo "$line" >> "$temp_config"
elif [[ "$line" =~ ^[[:space:]]*${field_name}:[[:space:]]* ]]; then
field_found=true
if [[ -n "$new_value" ]]; then
local indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
echo "${indent}${field_name}: $new_value" >> "$temp_config"
fi
# 如果新值为空,则跳过此行(删除字段)
else
echo "$line" >> "$temp_config"
fi
else
echo "$line" >> "$temp_config"
fi
done < "$HYSTERIA_CONFIG"
if mv "$temp_config" "$HYSTERIA_CONFIG" 2>/dev/null; then
if [[ -n "$new_value" ]]; then
log_success "${field_name} 已更新"
else
log_success "${field_name} 已删除"
fi
ask_restart_service
else
log_error "修改失败"
rm -f "$temp_config"
fi
}
# 询问是否重启服务
ask_restart_service() {
echo ""
read -p "是否重启 Hysteria2 服务以应用配置? [y/N]: " restart_choice
if [[ $restart_choice =~ ^[Yy]$ ]]; then
if systemctl restart hysteria-server 2>/dev/null; then
log_success "服务已重启"
else
log_error "服务重启失败,请手动重启"
fi
fi
}
# ===== 新的核心功能实现 =====
# 规则库文件路径
# 规则库目录变量
RULES_DIR="/etc/hysteria/outbound-rules"
RULES_LIBRARY="$RULES_DIR/rules-library.yaml"
RULES_STATE="$RULES_DIR/rules-state.yaml"
# 初始化规则库
init_rules_library() {
if [[ ! -d "$RULES_DIR" ]]; then
mkdir -p "$RULES_DIR" 2>/dev/null || {
log_error "无法创建规则库目录,将使用临时目录"
RULES_DIR="/tmp/hysteria-rules"
RULES_LIBRARY="$RULES_DIR/rules-library.yaml"
RULES_STATE="$RULES_DIR/rules-state.yaml"
mkdir -p "$RULES_DIR"
}
fi
if [[ ! -f "$RULES_LIBRARY" ]]; then
cat > "$RULES_LIBRARY" << 'EOF'
# Hysteria2 出站规则库
# 格式:每个规则包含type、description和config字段
version: "1.0"
last_modified: ""
rules:
# 示例规则(已注释):
# direct_rule:
# type: direct
# description: "直连规则示例"
# config:
# mode: auto
# bindDevice: eth0
EOF
fi
if [[ ! -f "$RULES_STATE" ]]; then
cat > "$RULES_STATE" << 'EOF'
# Hysteria2 出站规则状态
applied_rules: []
last_sync: ""
EOF
fi
}
# 1. 查看出站规则
view_outbound_rules() {
init_rules_library
echo -e "${BLUE}=== 出站规则总览 ===${NC}"
echo ""
# 显示配置文件中的规则
echo -e "${GREEN}📄 配置文件中的规则:${NC}"
if [[ -f "$HYSTERIA_CONFIG" ]] && grep -q "^[[:space:]]*outbounds:" "$HYSTERIA_CONFIG"; then
local rule_count=0
while IFS= read -r line; do
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+)$ ]]; then
local rule_name="${BASH_REMATCH[1]}"
rule_name=$(echo "$rule_name" | tr -d '"' | xargs)
((rule_count++))
echo " $rule_count. $rule_name"
fi
done < <(sed -n '/^[[:space:]]*outbounds:/,/^[a-zA-Z]/p' "$HYSTERIA_CONFIG" | head -n -1)
if [[ $rule_count -eq 0 ]]; then
echo " (无规则)"
fi
else
echo " (无规则)"
fi
echo ""
# 显示规则库中的规则
echo -e "${CYAN}📚 规则库中的规则:${NC}"
if [[ -f "$RULES_LIBRARY" ]] && grep -q "rules:" "$RULES_LIBRARY"; then
local lib_count=0
# 使用简单可靠的grep方法直接提取规则名
while IFS= read -r rule_name; do
if [[ -n "$rule_name" ]]; then
((lib_count++))
# 检查是否已应用
local status="❌ 未应用"
if grep -q "- $rule_name" "$RULES_STATE" 2>/dev/null; then
status="✅ 已应用"
fi
echo " $lib_count. $rule_name $status"
fi
done < <(grep -o "^[[:space:]]\{2\}[a-zA-Z_][a-zA-Z0-9_]*:" "$RULES_LIBRARY" | sed 's/^[[:space:]]\{2\}\([^:]*\):.*/\1/')
if [[ $lib_count -eq 0 ]]; then
echo " (无规则)"
fi
else
echo " (无规则)"
fi
echo ""
# 询问是否查看单个规则详细参数
echo -e "${BLUE}是否查看特定规则的详细参数?${NC}"
echo -e "${GREEN}1.${NC} 是,选择规则查看详细参数"
echo -e "${YELLOW}2.${NC} 否,返回上级菜单"
echo ""
read -p "请选择 [1-2]: " detail_choice
case $detail_choice in
1)
view_single_rule_details
;;
2)
;;
*)
echo ""
echo -e "${YELLOW}无效选择,返回上级菜单${NC}"
;;
esac
wait_for_user
}
# 查看单个规则详细参数
view_single_rule_details() {
echo ""
echo -e "${BLUE}=== 查看规则详细参数 ===${NC}"
echo ""
# 列出规则库中的规则
local rules=()
local rule_count=0
echo -e "${CYAN}📚 规则库中的规则:${NC}"
while IFS= read -r rule_name; do
if [[ -n "$rule_name" ]]; then
rules+=("$rule_name")
((rule_count++))
# 检查是否已应用
local status="❌ 未应用"
if grep -q "- $rule_name" "$RULES_STATE" 2>/dev/null; then
status="✅ 已应用"
fi
echo " $rule_count. $rule_name $status"
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 " (无规则)"
echo ""
return
fi
echo ""
read -p "请选择要查看的规则 [1-$rule_count]: " choice
if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt $rule_count ]]; then
echo -e "${RED}无效选择${NC}"
return 1
fi
local selected_rule="${rules[$((choice-1))]}"
echo ""
echo -e "${BLUE}=== 规则详细信息: ${CYAN}$selected_rule${NC} ===${NC}"
echo ""
# 获取规则基本信息
echo -e "${GREEN}📋 基本信息:${NC}"
local rule_type=$(awk -v rule="$selected_rule" '
BEGIN { in_rule = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*type:[[:space:]]*/ {
gsub(/^[[:space:]]*type:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0;
exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ { in_rule = 0 }
' "$RULES_LIBRARY")
local rule_desc=$(awk -v rule="$selected_rule" '
BEGIN { in_rule = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*description:[[:space:]]*/ {
gsub(/^[[:space:]]*description:[[:space:]]*"?/, "");
gsub(/"?[[:space:]]*$/, "");
print $0;
exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ { in_rule = 0 }
' "$RULES_LIBRARY")
echo " 规则名称: $selected_rule"
echo " 规则类型: ${rule_type:-"未知"}"
echo " 规则描述: ${rule_desc:-"无描述"}"
# 检查应用状态
local applied_status="❌ 未应用"
if grep -q "- $selected_rule" "$RULES_STATE" 2>/dev/null; then
applied_status="✅ 已应用"
fi
echo " 应用状态: $applied_status"
echo ""
# 显示配置参数
echo -e "${GREEN}⚙️ 配置参数:${NC}"
case "$rule_type" in
"direct")
show_direct_parameters "$selected_rule"
;;
"socks5")
show_socks5_parameters "$selected_rule"
;;
"http")
show_http_parameters "$selected_rule"
;;
*)
echo " 不支持的规则类型: $rule_type"
;;
esac
echo ""
}
# 显示direct类型参数
show_direct_parameters() {
local rule_name="$1"
echo " 类型: Direct (直连)"
local mode=$(get_rule_config_value "$rule_name" "mode")
local bindIPv4=$(get_rule_config_value "$rule_name" "bindIPv4")
local bindIPv6=$(get_rule_config_value "$rule_name" "bindIPv6")
local bindDevice=$(get_rule_config_value "$rule_name" "bindDevice")
local fastOpen=$(get_rule_config_value "$rule_name" "fastOpen")
echo " 连接模式 (mode): ${mode:-"auto (默认)"}"
echo " 绑定IPv4 (bindIPv4): ${bindIPv4:-"未设置"}"
echo " 绑定IPv6 (bindIPv6): ${bindIPv6:-"未设置"}"
echo " 绑定设备 (bindDevice): ${bindDevice:-"未设置"}"
echo " 快速打开 (fastOpen): ${fastOpen:-"false (默认)"}"
}
# 显示socks5类型参数
show_socks5_parameters() {
local rule_name="$1"
echo " 类型: SOCKS5 代理"
local addr=$(get_rule_config_value "$rule_name" "addr")
local username=$(get_rule_config_value "$rule_name" "username")
local password=$(get_rule_config_value "$rule_name" "password")
echo " 代理地址 (addr): ${addr:-"未设置"}"
echo " 用户名 (username): ${username:-"未设置"}"
echo " 密码 (password): ${password:+"***已设置***"}"
[[ -z "$password" ]] && echo " 密码 (password): 未设置"
}
# 显示http类型参数
show_http_parameters() {
local rule_name="$1"
echo " 类型: HTTP/HTTPS 代理"
local url=$(get_rule_config_value "$rule_name" "url")
local insecure=$(get_rule_config_value "$rule_name" "insecure")
echo " 代理URL (url): ${url:-"未设置"}"
echo " 忽略TLS验证 (insecure): ${insecure:-"false (默认)"}"
}
# 2. 新增出站规则
add_outbound_rule_new() {
init_rules_library
echo -e "${BLUE}=== 新增出站规则 ===${NC}"
echo ""
# 获取规则名称
local rule_name
while true; do
read -p "规则名称 (字母、数字、下划线): " rule_name
if [[ -z "$rule_name" ]]; then
echo -e "${RED}规则名称不能为空${NC}"
continue
fi
if [[ ! "$rule_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
echo -e "${RED}规则名称只能包含字母、数字和下划线${NC}"
continue
fi
# 检查是否已存在(检查2级缩进的规则名)
if grep -q "^[[:space:]]\{2\}$rule_name:[[:space:]]*$" "$RULES_LIBRARY" 2>/dev/null; then
echo -e "${RED}规则名称已存在${NC}"
continue
fi
break
done
# 获取规则描述
read -p "规则描述: " rule_desc
if [[ -z "$rule_desc" ]]; then
rule_desc="$rule_name 出站规则"
fi
# 选择规则类型
echo ""
echo "选择规则类型:"
echo "1. Direct (直连)"
echo "2. SOCKS5 代理"
echo "3. HTTP/HTTPS 代理"
echo ""
local rule_type=""
local type_choice
read -p "请选择 [1-3]: " type_choice
case $type_choice in
1) rule_type="direct" ;;
2) rule_type="socks5" ;;
3) rule_type="http" ;;
*)
log_error "无效选择"
return 1
;;
esac
# 检查类型冲突
echo -e "${YELLOW}检查类型冲突...${NC}"
local existing_type_rule=$(check_rule_type_conflict "$rule_type")
if [[ -n "$existing_type_rule" ]]; then
echo -e "${RED}错误: 已存在 $rule_type 类型的规则: $existing_type_rule${NC}"
echo -e "${YELLOW}每种类型只能存在一个规则。${NC}"
read -p "是否要替换现有规则? [y/N]: " replace_choice
if [[ ! $replace_choice =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}操作已取消${NC}"
return 1
fi
echo -e "${BLUE}将替换现有规则: $existing_type_rule${NC}"
fi
# 收集配置
local config_data=""
case $rule_type in
"direct")
echo ""
echo -e "${BLUE}配置 Direct 直连参数${NC}"
read -p "绑定网卡 (可选): " interface
read -p "绑定IPv4 (可选): " ipv4
read -p "绑定IPv6 (可选): " ipv6
config_data="mode: auto"
if [[ -n "$interface" ]]; then
config_data+="\nbindDevice: \"$interface\""
fi
if [[ -n "$ipv4" ]]; then
config_data+="\nbindIPv4: \"$ipv4\""
fi
if [[ -n "$ipv6" ]]; then
config_data+="\nbindIPv6: \"$ipv6\""
fi
;;
"socks5")
echo ""
echo -e "${BLUE}配置 SOCKS5 代理参数${NC}"
read -p "代理地址:端口: " addr
if [[ -z "$addr" ]]; then
log_error "代理地址不能为空"
return 1
fi
config_data="addr: \"$addr\""
read -p "需要认证? [y/N]: " need_auth
if [[ $need_auth =~ ^[Yy]$ ]]; then
read -p "用户名: " username
read -s -p "密码: " password
echo ""
if [[ -n "$username" ]]; then
config_data+="\nusername: \"$username\""
config_data+="\npassword: \"$password\""
fi
fi
;;
"http")
echo ""
echo -e "${BLUE}配置 HTTP/HTTPS 代理参数${NC}"
read -p "代理URL: " url
if [[ -z "$url" ]]; then
log_error "代理URL不能为空"
return 1
fi
config_data="url: \"$url\""
if [[ "$url" =~ ^https:// ]]; then
read -p "跳过TLS验证? [y/N]: " skip_tls
if [[ $skip_tls =~ ^[Yy]$ ]]; then
config_data+="\ninsecure: true"
fi
fi
;;
esac
# 保存到规则库
local temp_file="/tmp/rules_add_$$_$(date +%s).yaml"
# 在rules节点下添加新规则
awk -v rule="$rule_name" -v type="$rule_type" -v desc="$rule_desc" -v config="$config_data" '
/^rules:/ {
print $0
print " " rule ":"
print " type: " type
print " description: \"" desc "\""
print " config:"
# 处理配置数据,添加正确的缩进
n = split(config, lines, "\\n")
for (i = 1; i <= n; i++) {
if (lines[i] != "") {
print " " lines[i]
}
}
print " created_at: \"" strftime("%Y-%m-%dT%H:%M:%SZ") "\""
print " updated_at: \"" strftime("%Y-%m-%dT%H:%M:%SZ") "\""
next
}
/^last_modified:/ {
print "last_modified: \"" strftime("%Y-%m-%dT%H:%M:%SZ") "\""
next
}
{ print }
' "$RULES_LIBRARY" > "$temp_file"
if mv "$temp_file" "$RULES_LIBRARY"; then
log_success "规则 '$rule_name' 已添加到规则库"
echo ""
read -p "是否立即应用此规则? [y/N]: " apply_now
if [[ $apply_now =~ ^[Yy]$ ]]; then
apply_rule_to_config_simple "$rule_name"
fi
else
log_error "规则保存失败"
rm -f "$temp_file"
return 1
fi
wait_for_user
}
# 3. 应用规则到配置
apply_outbound_rule() {
init_rules_library
echo -e "${BLUE}=== 应用规则到配置 ===${NC}"
echo ""
# 列出规则库中未应用的规则 - 使用可靠的grep方法
local unapplied_rules=()
local rule_count=0
while IFS= read -r rule_name; do
if [[ -n "$rule_name" ]]; then
# 检查是否已应用
if ! grep -q "- $rule_name" "$RULES_STATE" 2>/dev/null; then
unapplied_rules+=("$rule_name")
((rule_count++))
echo "$rule_count. $rule_name"
fi
fi
done < <(grep -o "^[[:space:]]\{2\}[a-zA-Z_][a-zA-Z0-9_]*:" "$RULES_LIBRARY" | sed 's/^[[:space:]]\{2\}\([^:]*\):.*/\1/')
if [[ ${#unapplied_rules[@]} -eq 0 ]]; then
echo -e "${YELLOW}没有可应用的规则${NC}"
wait_for_user
return
fi
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="${unapplied_rules[$((choice-1))]}"
apply_rule_to_config_simple "$selected_rule"
wait_for_user
}
# 应用规则到配置的简化实现
# 新的规则应用函数 - 符合Hysteria2官方标准
apply_rule_to_config_simple() {
local rule_name="$1"
if [[ -z "$rule_name" ]]; then
log_error "规则名称不能为空"
return 1
fi
# 简化的YAML解析 - 使用更直接的方法
local rule_type rule_config
# 检查规则是否存在
if ! grep -A 20 "^[[:space:]]*${rule_name}:[[:space:]]*$" "$RULES_LIBRARY" >/dev/null 2>&1; then
log_error "规则 '$rule_name' 不存在于规则库中"
return 1
fi
# 提取规则类型
rule_type=$(awk -v rule="$rule_name" '
BEGIN { found = 0; in_rule = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*type:[[:space:]]*/ {
gsub(/^[[:space:]]*type:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0;
exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*type:/ && !/^[[:space:]]*config:/ && !/^[[:space:]]*description:/ {
in_rule = 0
}
' "$RULES_LIBRARY")
if [[ -z "$rule_type" ]]; then
log_error "无法获取规则 '$rule_name' 的类型"
return 1
fi
log_info "检测到规则类型: $rule_type"
log_debug "开始检查配置文件中的同类型规则: $HYSTERIA_CONFIG"
# 先提取配置参数(在使用前定义变量)- 完整参数支持
local mode="" bindDevice="" bindIPv4="" bindIPv6="" fastOpen=""
local addr="" username="" password="" url="" insecure=""
case "$rule_type" in
"direct")
# 提取direct类型的所有参数
mode=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*mode:[[:space:]]*/ {
gsub(/^[[:space:]]*mode:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
bindDevice=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*bindDevice:[[:space:]]*/ {
gsub(/^[[:space:]]*bindDevice:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
bindIPv4=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*bindIPv4:[[:space:]]*/ {
gsub(/^[[:space:]]*bindIPv4:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
bindIPv6=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*bindIPv6:[[:space:]]*/ {
gsub(/^[[:space:]]*bindIPv6:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
fastOpen=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*fastOpen:[[:space:]]*/ {
gsub(/^[[:space:]]*fastOpen:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
;;
"socks5")
# 提取socks5类型的所有参数
addr=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*addr:[[:space:]]*/ {
gsub(/^[[:space:]]*addr:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
username=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*username:[[:space:]]*/ {
gsub(/^[[:space:]]*username:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
password=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*password:[[:space:]]*/ {
gsub(/^[[:space:]]*password:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
;;
"http")
# 提取http类型的所有参数
url=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*url:[[:space:]]*/ {
gsub(/^[[:space:]]*url:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
insecure=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && /^[[:space:]]*insecure:[[:space:]]*/ {
gsub(/^[[:space:]]*insecure:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0; exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY")
;;
esac
log_debug "提取的配置参数: mode=$mode, bindDevice=$bindDevice, addr=$addr, url=$url"
# 检查是否存在同类型的已应用规则
local existing_rule=""
if existing_rule=$(check_existing_outbound_type "$rule_type"); then
echo ""
echo -e "${YELLOW}⚠️ 类型冲突检测 ⚠️${NC}"
echo -e "${YELLOW}检测到配置文件中已存在 ${rule_type} 类型规则: ${CYAN}$existing_rule${NC}"
echo -e "${YELLOW}根据系统设计,每种类型只能有一个出站规则${NC}"
echo ""
echo -e "${BLUE}选择操作:${NC}"
echo -e "${GREEN}1.${NC} 继续应用并覆盖现有规则 ${CYAN}$existing_rule${NC}"
echo -e "${RED}2.${NC} 取消应用操作"
echo ""
read -p "请选择 [1-2]: " conflict_choice
case $conflict_choice in
1)
echo -e "${BLUE}[INFO]${NC} 将覆盖现有的 $rule_type 规则: $existing_rule"
echo -e "${BLUE}[INFO]${NC} 继续应用新规则..."
echo ""
# 先删除现有的同类型规则
if ! delete_existing_outbound_from_config "$existing_rule"; then
log_warn "删除现有规则失败,将尝试直接覆盖"
# 删除失败时不退出,而是继续尝试应用新规则
fi
;;
2)
echo -e "${BLUE}[INFO]${NC} 已取消应用操作"
return 0
;;
*)
log_error "无效选择"
return 1
;;
esac
fi
# 直接操作,不创建不必要的备份
# 生成符合官方标准的outbound配置
local temp_config
temp_config=$(create_apply_temp_file)
if [[ -f "$HYSTERIA_CONFIG" ]] && grep -q "^[[:space:]]*outbounds:" "$HYSTERIA_CONFIG"; then
# 在现有outbounds中添加新规则 - 修复逻辑错误
awk -v rule="$rule_name" -v type="$rule_type" \
-v mode="$mode" -v device="$bindDevice" -v ipv4="$bindIPv4" -v ipv6="$bindIPv6" -v fastopen="$fastOpen" \
-v addr="$addr" -v user="$username" -v pass="$password" \
-v url="$url" -v insecure="$insecure" '
/^[[:space:]]*outbounds:/ {
print $0
# 根据官方格式添加完整的outbound配置
print " - name: " rule
print " type: " type
if (type == "direct") {
print " direct:"
if (mode != "") print " mode: " mode
if (ipv4 != "") print " bindIPv4: " ipv4
if (ipv6 != "") print " bindIPv6: " ipv6
if (device != "") print " bindDevice: " device
if (fastopen != "") print " fastOpen: " fastopen
} else if (type == "socks5") {
print " socks5:"
if (addr != "") print " addr: " addr
if (user != "") print " username: " user
if (pass != "") print " password: " pass
} else if (type == "http") {
print " http:"
if (url != "") print " url: " url
if (insecure != "") print " insecure: " insecure
}
# 不使用next,继续处理后续行以保留其他现有规则
}
!/^[[:space:]]*outbounds:/ { print }
' "$HYSTERIA_CONFIG" > "$temp_config"
else
# 创建新的outbounds节点
if [[ -f "$HYSTERIA_CONFIG" ]]; then
cp "$HYSTERIA_CONFIG" "$temp_config"
else
echo "# Hysteria2 配置文件" > "$temp_config"
fi
# 添加符合官方标准的outbounds节点
cat >> "$temp_config" << EOF
# 出站配置
outbounds:
- name: $rule_name
type: $rule_type
EOF
# 根据规则类型添加完整的具体配置
case "$rule_type" in
"direct")
echo " direct:" >> "$temp_config"
[[ -n "$mode" ]] && echo " mode: $mode" >> "$temp_config"
[[ -n "$bindIPv4" ]] && echo " bindIPv4: $bindIPv4" >> "$temp_config"
[[ -n "$bindIPv6" ]] && echo " bindIPv6: $bindIPv6" >> "$temp_config"
[[ -n "$bindDevice" ]] && echo " bindDevice: $bindDevice" >> "$temp_config"
[[ -n "$fastOpen" ]] && echo " fastOpen: $fastOpen" >> "$temp_config"
;;
"socks5")
echo " socks5:" >> "$temp_config"
[[ -n "$addr" ]] && echo " addr: $addr" >> "$temp_config"
[[ -n "$username" ]] && echo " username: $username" >> "$temp_config"
[[ -n "$password" ]] && echo " password: $password" >> "$temp_config"
;;
"http")
echo " http:" >> "$temp_config"
[[ -n "$url" ]] && echo " url: $url" >> "$temp_config"
[[ -n "$insecure" ]] && echo " insecure: $insecure" >> "$temp_config"
;;
esac
fi
# 应用配置
if [[ -s "$temp_config" ]]; then
mv "$temp_config" "$HYSTERIA_CONFIG"
log_success "规则 '$rule_name' 已应用到配置文件"
# 更新状态文件
if ! grep -q "- $rule_name" "$RULES_STATE" 2>/dev/null; then
sed -i "/applied_rules:/a\\ - $rule_name" "$RULES_STATE" 2>/dev/null ||
awk -v rule="$rule_name" '
/^applied_rules:/ {
print $0
print " - " rule
next
}
{ print }
' "$RULES_STATE" > "${RULES_STATE}.tmp" && mv "${RULES_STATE}.tmp" "$RULES_STATE"
fi
log_info "状态已更新"
log_success "规则应用完成!"
# 交互式重启确认
echo ""
echo -e "${YELLOW}⚠️ 配置已更新,需要重启服务生效 ⚠️${NC}"
echo -e "${BLUE}是否立即重启 Hysteria2 服务?${NC}"
echo ""
echo -e "${GREEN}1.${NC} 是,立即重启服务(推荐)"
echo -e "${YELLOW}2.${NC} 否,稍后手动重启"
echo ""
read -p "请选择 [1-2]: " restart_choice
case $restart_choice in
1)
echo ""
echo -e "${BLUE}[INFO]${NC} 正在重启 Hysteria2 服务..."
if systemctl restart hysteria-server 2>/dev/null; then
echo -e "${GREEN}✅ 服务重启成功,新配置已生效${NC}"
# 等待服务启动
sleep 2
if systemctl is-active hysteria-server >/dev/null 2>&1; then
echo -e "${GREEN}✅ 服务运行状态正常${NC}"
else
echo -e "${RED}⚠️ 服务重启后状态异常,请检查配置${NC}"
echo -e "${YELLOW}建议执行: journalctl -u hysteria-server -f${NC}"
fi
else
echo -e "${RED}❌ 服务重启失败${NC}"
echo -e "${YELLOW}请手动重启: systemctl restart hysteria-server${NC}"
fi
;;
2)
echo ""
echo -e "${BLUE}[INFO]${NC} 已跳过自动重启"
echo -e "${YELLOW}请稍后手动重启服务生效新配置:${NC}"
echo -e "${CYAN} systemctl restart hysteria-server${NC}"
;;
*)
echo ""
echo -e "${YELLOW}无效选择,已跳过自动重启${NC}"
echo -e "${YELLOW}请手动重启服务: systemctl restart hysteria-server${NC}"
;;
esac
return 0
else
log_error "配置应用失败"
rm -f "$temp_config"
return 1
fi
}
# 4. 修改出站规则
modify_outbound_rule() {
init_rules_library
echo -e "${BLUE}=== 修改出站规则 ===${NC}"
echo ""
# 列出规则库中的规则 - 使用可靠的grep方法
local rules=()
local rule_count=0
while IFS= read -r rule_name; do
if [[ -n "$rule_name" ]]; then
rules+=("$rule_name")
((rule_count++))
echo "$rule_count. $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}"
wait_for_user
return
fi
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))]}"
echo ""
echo "修改选项:"
echo "1. 修改描述"
echo "2. 修改配置参数"
echo ""
read -p "请选择操作 [1-2]: " modify_choice
case $modify_choice in
1)
# 获取当前描述
local current_desc=$(awk -v rule="$selected_rule" '
BEGIN { in_rule = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*description:/ {
gsub(/^[[:space:]]*description:[[:space:]]*"?/, "");
gsub(/"?[[:space:]]*$/, "");
print $0;
exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ { in_rule = 0 }
' "$RULES_LIBRARY")
echo "当前描述: $current_desc"
read -p "新的描述: " new_desc
if [[ -n "$new_desc" ]]; then
# 更新描述
awk -v rule="$selected_rule" -v desc="$new_desc" '
BEGIN { in_rule = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; print; next }
in_rule && /^[[:space:]]*description:/ {
gsub(/^[[:space:]]*/, "")
indent = substr($0, 1, match($0, /[^ ]/) - 1)
print indent "description: \"" desc "\""
next
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ { in_rule = 0 }
{ print }
' "$RULES_LIBRARY" > "${RULES_LIBRARY}.tmp" && mv "${RULES_LIBRARY}.tmp" "$RULES_LIBRARY"
log_success "描述已更新"
fi
;;
2)
# 修改配置参数
modify_rule_parameters "$selected_rule"
;;
*)
log_error "无效选择"
;;
esac
wait_for_user
}
# 修改规则配置参数
modify_rule_parameters() {
local rule_name="$1"
echo ""
echo -e "${BLUE}=== 修改规则配置参数: ${CYAN}$rule_name${NC} ===${NC}"
# 获取规则类型
local rule_type=$(awk -v rule="$rule_name" '
BEGIN { in_rule = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*type:[[:space:]]*/ {
gsub(/^[[:space:]]*type:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0;
exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ { in_rule = 0 }
' "$RULES_LIBRARY")
if [[ -z "$rule_type" ]]; then
log_error "无法获取规则类型"
return 1
fi
echo -e "${BLUE}规则类型: ${CYAN}$rule_type${NC}"
echo ""
case "$rule_type" in
"direct")
modify_direct_parameters "$rule_name"
;;
"socks5")
modify_socks5_parameters "$rule_name"
;;
"http")
modify_http_parameters "$rule_name"
;;
*)
log_error "不支持的规则类型: $rule_type"
return 1
;;
esac
}
# 修改direct类型参数
modify_direct_parameters() {
local rule_name="$1"
echo "Direct 类型参数修改:"
echo "1. mode (auto|64|46|6|4)"
echo "2. bindIPv4"
echo "3. bindIPv6"
echo "4. bindDevice"
echo "5. fastOpen (true|false)"
echo ""
read -p "请选择要修改的参数 [1-5]: " param_choice
local param_name param_value current_value
case $param_choice in
1)
param_name="mode"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
echo "可选值: auto, 64, 46, 6, 4"
read -p "请输入新的mode值: " param_value
;;
2)
param_name="bindIPv4"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的bindIPv4值: " param_value
;;
3)
param_name="bindIPv6"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的bindIPv6值: " param_value
;;
4)
param_name="bindDevice"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的bindDevice值: " param_value
;;
5)
param_name="fastOpen"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
echo "可选值: true, false"
read -p "请输入新的fastOpen值: " param_value
;;
*)
log_error "无效选择"
return 1
;;
esac
if [[ -n "$param_value" ]]; then
update_rule_config_value "$rule_name" "$param_name" "$param_value"
fi
}
# 修改socks5类型参数
modify_socks5_parameters() {
local rule_name="$1"
echo "SOCKS5 类型参数修改:"
echo "1. addr"
echo "2. username"
echo "3. password"
echo ""
read -p "请选择要修改的参数 [1-3]: " param_choice
local param_name param_value current_value
case $param_choice in
1)
param_name="addr"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的地址 (host:port): " param_value
;;
2)
param_name="username"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的用户名: " param_value
;;
3)
param_name="password"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的密码: " param_value
;;
*)
log_error "无效选择"
return 1
;;
esac
if [[ -n "$param_value" ]]; then
update_rule_config_value "$rule_name" "$param_name" "$param_value"
fi
}
# 修改http类型参数
modify_http_parameters() {
local rule_name="$1"
echo "HTTP 类型参数修改:"
echo "1. url"
echo "2. insecure (true|false)"
echo ""
read -p "请选择要修改的参数 [1-2]: " param_choice
local param_name param_value current_value
case $param_choice in
1)
param_name="url"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
read -p "请输入新的URL: " param_value
;;
2)
param_name="insecure"
current_value=$(get_rule_config_value "$rule_name" "$param_name")
echo "当前值: ${current_value:-"未设置"}"
echo "可选值: true, false"
read -p "请输入新的insecure值: " param_value
;;
*)
log_error "无效选择"
return 1
;;
esac
if [[ -n "$param_value" ]]; then
update_rule_config_value "$rule_name" "$param_name" "$param_value"
fi
}
# 获取规则配置值
get_rule_config_value() {
local rule_name="$1"
local param_name="$2"
awk -v rule="$rule_name" -v param="$param_name" '
BEGIN { in_rule = 0; in_config = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; next }
in_rule && in_config && $0 ~ "^[[:space:]]*" param ":[[:space:]]*" {
gsub(/^[[:space:]]*[^:]*:[[:space:]]*/, "");
gsub(/[[:space:]]*$/, "");
print $0;
exit
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ { in_rule = 0 }
' "$RULES_LIBRARY"
}
# 更新规则配置值
update_rule_config_value() {
local rule_name="$1"
local param_name="$2"
local param_value="$3"
# 使用临时文件安全更新
local temp_file=$(create_temp_file)
awk -v rule="$rule_name" -v param="$param_name" -v value="$param_value" '
BEGIN { in_rule = 0; in_config = 0; updated = 0 }
$0 ~ "^[[:space:]]*" rule ":[[:space:]]*$" { in_rule = 1; print; next }
in_rule && /^[[:space:]]*config:[[:space:]]*$/ { in_config = 1; print; next }
in_rule && in_config && $0 ~ "^[[:space:]]*" param ":[[:space:]]*" {
gsub(/^[[:space:]]*/, "")
indent = substr($0, 1, match($0, /[^ ]/) - 1)
print indent param ": " value
updated = 1
next
}
in_rule && in_config && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*/ && !updated {
# 在config段末尾插入新参数
gsub(/^[[:space:]]*/, "")
indent = substr($0, 1, match($0, /[^ ]/) - 1)
print indent param ": " value
print
updated = 1
next
}
in_rule && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ && !/^[[:space:]]*config:/ {
in_rule = 0; in_config = 0
}
{ print }
' "$RULES_LIBRARY" > "$temp_file"
if [[ -s "$temp_file" ]]; then
mv "$temp_file" "$RULES_LIBRARY"
log_success "参数 $param_name 已更新为: $param_value"
else
log_error "参数更新失败"
rm -f "$temp_file"
return 1
fi
}
# 5. 删除出站规则
delete_outbound_rule_new() {
init_rules_library
echo -e "${BLUE}=== 删除出站规则 ===${NC}"
echo ""
# 列出规则库中的规则 - 使用可靠的grep方法
local rules=()
local rule_count=0
while IFS= read -r rule_name; do
if [[ -n "$rule_name" ]]; then
rules+=("$rule_name")
((rule_count++))
# 检查是否已应用
local status="❌ 未应用"
if grep -q "- $rule_name" "$RULES_STATE" 2>/dev/null; then
status="✅ 已应用"
fi
echo "$rule_count. $rule_name $status"
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}"
wait_for_user
return
fi
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))]}"
echo ""
echo -e "${RED}⚠️ 警告: 即将删除规则 '$selected_rule'${NC}"
# 检查是否已应用
if grep -q "- $selected_rule" "$RULES_STATE" 2>/dev/null; then
echo -e "${YELLOW}此规则当前已应用,删除将同时从配置文件中移除${NC}"
fi
echo ""
read -p "确认删除? [y/N]: " confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
echo -e "${BLUE}已取消删除操作${NC}"
return 0
fi
# 如果已应用,先从配置中移除
if grep -q "- $selected_rule" "$RULES_STATE" 2>/dev/null; then
echo "正在从配置文件中移除..."
# 从配置文件中删除
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"
mv "$temp_config" "$HYSTERIA_CONFIG"
fi
# 从状态文件中移除
awk -v rule="$selected_rule" '
$0 == " - " rule { next }
{ print }
' "$RULES_STATE" > "${RULES_STATE}.tmp" && mv "${RULES_STATE}.tmp" "$RULES_STATE"
fi
# 从规则库中删除
local temp_library="/tmp/rules_delete_$$_$(date +%s).yaml"
local in_target_rule=false
local rule_indent=""
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*${selected_rule}:[[:space:]]*$ ]]; then
in_target_rule=true
rule_indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
continue
elif [[ "$in_target_rule" == true ]]; then
# 检查是否离开规则
if [[ "$line" =~ ^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$ ]]; then
local line_indent=$(echo "$line" | sed 's/[a-zA-Z].*//')
if [[ ${#line_indent} -le ${#rule_indent} ]]; then
in_target_rule=false
echo "$line" >> "$temp_library"
fi
elif [[ "$line" =~ ^[[:space:]]*[a-zA-Z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]*(type|description|config|created_at|updated_at): ]]; then
in_target_rule=false
echo "$line" >> "$temp_library"
fi
# 在规则中的行都跳过
else
echo "$line" >> "$temp_library"
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
fi
else
log_error "规则删除失败"
rm -f "$temp_library"
return 1
fi
wait_for_user
}
# ===== 并发安全和临时文件管理函数 =====
# 创建操作锁文件
acquire_operation_lock() {
local operation="${1:-outbound}"
local lock_file="/tmp/s-hy2-${operation}-$(whoami).lock"
local max_wait=30
local wait_count=0
while [[ $wait_count -lt $max_wait ]]; do
if (set -C; echo $$ > "$lock_file") 2>/dev/null; then
# 成功获取锁
echo "$lock_file"
return 0
fi
# 检查锁文件是否过期(超过5分钟)
if [[ -f "$lock_file" ]]; then
local lock_age
lock_age=$(($(date +%s) - $(stat -c %Y "$lock_file" 2>/dev/null || echo 0)))
if [[ $lock_age -gt 300 ]]; then
# 清理过期锁文件
rm -f "$lock_file" 2>/dev/null
continue
fi
fi
sleep 1
((wait_count++))
done
# 获取锁失败
return 1
}
# 释放操作锁
release_operation_lock() {
local lock_file="$1"
[[ -n "$lock_file" && -f "$lock_file" ]] && rm -f "$lock_file"
}
# 创建标准化的Hysteria临时文件
create_hysteria_temp_file() {
local prefix="${1:-hysteria}"
local extension="${2:-yaml}"
# 使用mktemp确保唯一性和安全性
local temp_file
temp_file=$(mktemp "/tmp/${prefix}-XXXXXX.${extension}")
chmod 600 "$temp_file"
# 添加到清理列表
TEMP_FILES="${TEMP_FILES:-} $temp_file"
echo "$temp_file"
}
# 创建配置文件专用临时文件
create_config_temp_file() {
create_hysteria_temp_file "hysteria-config" "yaml"
}
# 创建删除操作专用临时文件
create_delete_temp_file() {
create_hysteria_temp_file "hysteria-delete" "yaml"
}
# 创建应用操作专用临时文件
create_apply_temp_file() {
create_hysteria_temp_file "hysteria-apply" "yaml"
}
# 如果脚本被直接执行
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
manage_outbound
fi