#!/usr/bin/env bash set -Eeuo pipefail trap 'echo "[错误] 第 $LINENO 行执行失败:$BASH_COMMAND"' ERR CONFIG_FILE="/etc/hysteria/config.yaml" SERVICE_NAME="hysteria-server.service" MASQUERADE_DEFAULT_URL="https://news.ycombinator.com/" red() { printf '\033[31m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; } yellow() { printf '\033[33m%s\033[0m\n' "$*"; } blue() { printf '\033[36m%s\033[0m\n' "$*"; } require_root() { if [[ "${EUID}" -ne 0 ]]; then red "请使用 root 运行此脚本。" exit 1 fi } require_cmd() { command -v "$1" >/dev/null 2>&1 || { red "缺少命令: $1" exit 1 } } prompt_nonempty() { local prompt="$1" local value="" while true; do read -r -p "$prompt" value if [[ -n "${value// }" ]]; then printf '%s' "$value" return 0 fi yellow "输入不能为空,请重新输入。" done } confirm_yn() { local prompt="${1:-是否继续?}" local answer read -r -p "${prompt} [Y/n]: " answer case "${answer:-Y}" in Y|y|yes|YES|"") return 0 ;; N|n|no|NO) return 1 ;; *) yellow "输入无效,默认按 Y 处理。"; return 0 ;; esac } generate_password() { openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24 echo } generate_sub_prefix() { openssl rand -base64 32 | tr -dc 'a-z0-9' | head -c 8 echo } get_public_ips() { local ipv4="" ipv6="" ipv4="$(curl -4 -fsSL https://api.ipify.org || true)" ipv6="$(curl -6 -fsSL https://api64.ipify.org || true)" printf '%s|%s\n' "$ipv4" "$ipv6" } wait_for_apt_lock() { local timeout="${1:-300}" local waited=0 while pgrep -x apt >/dev/null 2>&1 \ || pgrep -x apt-get >/dev/null 2>&1 \ || pgrep -x dpkg >/dev/null 2>&1 \ || pgrep -f unattended-upgrades >/dev/null 2>&1; do if (( waited == 0 )); then yellow "检测到 APT/DPKG 正被其他进程占用,正在等待..." ps -ef | grep -E 'apt|apt-get|dpkg|unattended' | grep -v grep || true fi if (( waited >= timeout )); then red "等待 APT 锁超时(${timeout} 秒)。" if confirm_yn "是否继续等待?"; then waited=0 else return 1 fi fi sleep 2 waited=$((waited + 2)) done return 0 } apt_update_and_install_base() { if ! confirm_yn "即将执行 apt update 并安装基础依赖 jq curl sed ufw openssl,是否继续?"; then red "已取消。" exit 1 fi export DEBIAN_FRONTEND=noninteractive blue "==> 更新 APT 软件库" wait_for_apt_lock 300 || { red "APT 被占用,无法继续。"; exit 1; } apt update -y blue "==> 安装基础依赖" wait_for_apt_lock 300 || { red "APT 被占用,无法继续。"; exit 1; } apt install -y jq curl sed ufw openssl } disable_existing_firewalls() { if ! confirm_yn "即将关闭系统现有防火墙并清理规则,是否继续?"; then red "已取消关闭现有防火墙。" exit 1 fi blue "==> 自动检测并关闭当前系统防火墙" if command -v ufw >/dev/null 2>&1; then yellow "检测到 UFW,正在关闭并重置" ufw disable || true yes | ufw reset || true fi if systemctl list-unit-files 2>/dev/null | grep -q '^firewalld\.service'; then yellow "检测到 firewalld,正在停止并禁用" systemctl stop firewalld || true systemctl disable firewalld || true systemctl mask firewalld || true fi if systemctl list-unit-files 2>/dev/null | grep -q '^nftables\.service'; then yellow "检测到 nftables,正在停止并禁用" systemctl stop nftables || true systemctl disable nftables || true fi if command -v nft >/dev/null 2>&1; then yellow "清空 nftables 规则" nft flush ruleset || true fi if command -v iptables >/dev/null 2>&1; then yellow "清空 iptables 规则" iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT || true iptables -F || true iptables -X || true iptables -Z || true iptables -P INPUT ACCEPT || true iptables -P FORWARD ACCEPT || true iptables -P OUTPUT ACCEPT || true else yellow "未检测到 iptables,跳过。" fi if command -v ip6tables >/dev/null 2>&1; then yellow "清空 ip6tables 规则" ip6tables -I INPUT 1 -p tcp --dport 22 -j ACCEPT || true ip6tables -F || true ip6tables -X || true ip6tables -Z || true ip6tables -P INPUT ACCEPT || true ip6tables -P FORWARD ACCEPT || true ip6tables -P OUTPUT ACCEPT || true else yellow "未检测到 ip6tables,跳过。" fi green "现有防火墙处理完成。" } configure_ufw() { if ! confirm_yn "即将配置并启用 UFW(开放 22/80/443 TCP 和 443 UDP),是否继续?"; then red "已取消 UFW 配置。" exit 1 fi blue "==> 配置 UFW" if [[ -f /etc/default/ufw ]]; then sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw || true fi ufw default deny incoming || true ufw default allow outgoing || true ufw allow 22/tcp || true ufw allow 80/tcp || true ufw allow 443/tcp || true ufw allow 443/udp || true yes | ufw enable || true green "UFW 配置完成。" } install_hysteria2() { if ! confirm_yn "即将安装 Hysteria 2,是否继续?"; then red "已取消 Hysteria 2 安装。" exit 1 fi blue "==> 安装 Hysteria 2" bash <(curl -fsSL https://get.hy2.sh/) green "Hysteria 2 安装完成。" } cf_api() { local method="$1" local endpoint="$2" local token="$3" local data="${4:-}" if [[ -n "$data" ]]; then curl -fsSL -X "$method" "https://api.cloudflare.com/client/v4${endpoint}" \ -H "Authorization: Bearer ${token}" \ -H "Content-Type: application/json" \ --data "$data" else curl -fsSL -X "$method" "https://api.cloudflare.com/client/v4${endpoint}" \ -H "Authorization: Bearer ${token}" \ -H "Content-Type: application/json" fi } get_cf_zone_id() { local main_domain="$1" local token="$2" local resp zone_id resp="$(cf_api GET "/zones?name=${main_domain}&status=active" "$token")" zone_id="$(printf '%s' "$resp" | jq -r '.result[0].id // empty')" if [[ -z "$zone_id" ]]; then red "未找到 Cloudflare Zone:${main_domain}" red "请确认主域名已接入 Cloudflare,且 API Token 对该 Zone 具有 DNS 编辑权限。" exit 1 fi printf '%s\n' "$zone_id" } delete_existing_dns_record_if_needed() { local zone_id="$1" local full_domain="$2" local type="$3" local token="$4" local resp record_ids rid resp="$(cf_api GET "/zones/${zone_id}/dns_records?type=${type}&name=${full_domain}" "$token")" record_ids="$(printf '%s' "$resp" | jq -r '.result[].id // empty')" if [[ -n "$record_ids" ]]; then while IFS= read -r rid; do [[ -z "$rid" ]] && continue cf_api DELETE "/zones/${zone_id}/dns_records/${rid}" "$token" >/dev/null done <<< "$record_ids" fi } create_cf_dns_record() { local zone_id="$1" local full_domain="$2" local type="$3" local content="$4" local token="$5" local payload resp success payload="$(jq -nc \ --arg type "$type" \ --arg name "$full_domain" \ --arg content "$content" \ '{type:$type,name:$name,content:$content,ttl:120,proxied:false}')" resp="$(cf_api POST "/zones/${zone_id}/dns_records" "$token" "$payload")" success="$(printf '%s' "$resp" | jq -r '.success')" if [[ "$success" != "true" ]]; then red "创建 Cloudflare DNS 记录失败:" printf '%s\n' "$resp" | jq . exit 1 fi } create_cloudflare_dns_records() { local main_domain="$1" local sub_prefix="$2" local token="$3" local ip_info="$4" local ipv4="${ip_info%%|*}" local ipv6="${ip_info##*|}" local full_domain="${sub_prefix}.${main_domain}" local zone_id zone_id="$(get_cf_zone_id "$main_domain" "$token")" blue "==> 在 Cloudflare 中创建 DNS 记录" echo "Zone: ${main_domain}" echo "完整域名: ${full_domain}" if [[ -n "$ipv4" ]]; then delete_existing_dns_record_if_needed "$zone_id" "$full_domain" "A" "$token" create_cf_dns_record "$zone_id" "$full_domain" "A" "$ipv4" "$token" green "已创建 A 记录 -> ${ipv4}" fi if [[ -n "$ipv6" ]]; then delete_existing_dns_record_if_needed "$zone_id" "$full_domain" "AAAA" "$token" create_cf_dns_record "$zone_id" "$full_domain" "AAAA" "$ipv6" "$token" green "已创建 AAAA 记录 -> ${ipv6}" fi if [[ -z "$ipv4" && -z "$ipv6" ]]; then red "未获取到当前机器公网 IPv4/IPv6,无法创建 DNS 记录。" exit 1 fi printf '%s\n' "$full_domain" } backup_existing_config() { if [[ -f "${CONFIG_FILE}" ]]; then local backup_file="${CONFIG_FILE}.bak.$(date +%Y%m%d_%H%M%S)" cp -a "${CONFIG_FILE}" "${backup_file}" yellow "检测到已有配置,已备份到: ${backup_file}" fi } write_config() { local full_domain="$1" local email="$2" local cf_token="$3" local password="$4" local proxy_url="$5" echo blue "==> 即将写入如下配置到 ${CONFIG_FILE}" cat < "${CONFIG_FILE}" < 启动并设置开机自启" systemctl daemon-reload || true systemctl enable --now "${SERVICE_NAME}" systemctl restart "${SERVICE_NAME}" green "服务已启动。" } show_result() { local full_domain="$1" local password="$2" local proxy_url="$3" local ip_info="$4" local ipv4="${ip_info%%|*}" local ipv6="${ip_info##*|}" local share_link="hysteria2://${password}@${full_domain}:443/?sni=${full_domain}&insecure=0" echo green "================= HY2 节点信息 =================" echo "域名: ${full_domain}" echo "端口: 443" echo "密码: ${password}" echo "伪装站点: ${proxy_url}" [[ -n "${ipv4}" ]] && echo "IPv4: ${ipv4}" [[ -n "${ipv6}" ]] && echo "IPv6: ${ipv6}" echo echo "代理链接:" echo "${share_link}" echo echo "hysteria 状态:" systemctl status "${SERVICE_NAME}" --no-pager -l | sed -n '1,8p' echo "================================================" } main() { require_root if ! confirm_yn "本脚本将更新软件源、关闭现有防火墙、配置 UFW、安装并配置 Hysteria 2、自动创建 Cloudflare DNS,是否继续?"; then red "用户取消执行。" exit 1 fi apt_update_and_install_base require_cmd curl require_cmd sed require_cmd systemctl require_cmd ufw require_cmd jq require_cmd openssl local main_domain email cf_token password sub_prefix full_domain ip_info proxy_url main_domain="$(prompt_nonempty '请输入主域名(例如 example.com): ')" email="$(prompt_nonempty '请输入 ACME 邮箱: ')" cf_token="$(prompt_nonempty '请输入 Cloudflare API Token: ')" password="$(generate_password)" sub_prefix="$(generate_sub_prefix)" ip_info="$(get_public_ips)" full_domain="$(create_cloudflare_dns_records "$main_domain" "$sub_prefix" "$cf_token" "$ip_info")" proxy_url="$MASQUERADE_DEFAULT_URL" echo green "自动生成的 Hysteria 域名: ${full_domain}" echo "默认伪装站点: ${proxy_url}" echo "随机密码: ${password}" echo disable_existing_firewalls configure_ufw install_hysteria2 write_config "${full_domain}" "${email}" "${cf_token}" "${password}" "${proxy_url}" start_service show_result "${full_domain}" "${password}" "${proxy_url}" "${ip_info}" } main "$@"