#!/usr/bin/env bash set -Eeuo pipefail trap 'echo "[错误] 第 $LINENO 行执行失败:$BASH_COMMAND" >&2' ERR CONFIG_FILE="/etc/hysteria/config.yaml" HYSTERIA_BIN="/usr/local/bin/hysteria" HYSTERIA_WORK_DIR="/var/lib/hysteria" SYSTEMD_SERVICE_NAME="hysteria-server.service" OPENRC_SERVICE_NAME="hysteria-server" OPENRC_SERVICE_FILE="/etc/init.d/${OPENRC_SERVICE_NAME}" OPENRC_LOG_FILE="/var/log/${OPENRC_SERVICE_NAME}.log" SERVICE_NAME="${SYSTEMD_SERVICE_NAME}" DEVICE_USERS=(macminim4 iphone12p ipadmini5 pve_debian other) USERPASS_ENTRIES=() TRAFFIC_STATS_SECRET="" PACKAGE_MANAGER="" INIT_SYSTEM="" OS_ID="" OS_ID_LIKE="" 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 } } require_python3_json() { python3 - <<'PY' >/dev/null 2>&1 import json PY } is_systemd() { [[ "${INIT_SYSTEM}" == "systemd" ]] } is_openrc() { [[ "${INIT_SYSTEM}" == "openrc" ]] } load_os_release() { if [[ -r /etc/os-release ]]; then OS_ID="$(awk -F= '/^ID=/{gsub(/"/, "", $2); print $2; exit}' /etc/os-release)" OS_ID_LIKE="$(awk -F= '/^ID_LIKE=/{gsub(/"/, "", $2); print $2; exit}' /etc/os-release)" fi } platform_label() { printf '%s/%s/%s' "${OS_ID:-unknown}" "${PACKAGE_MANAGER:-unknown}" "${INIT_SYSTEM:-unknown}" } detect_platform() { load_os_release if command -v apk >/dev/null 2>&1 || [[ "${OS_ID}" == "alpine" ]] || [[ " ${OS_ID_LIKE} " == *" alpine "* ]]; then PACKAGE_MANAGER="apk" INIT_SYSTEM="openrc" SERVICE_NAME="${OPENRC_SERVICE_NAME}" if ! command -v rc-service >/dev/null 2>&1 || ! command -v rc-update >/dev/null 2>&1; then red "检测到 Alpine/apk,但未发现 OpenRC(rc-service/rc-update),无法继续。" exit 1 fi return 0 fi if command -v apt >/dev/null 2>&1; then PACKAGE_MANAGER="apt" INIT_SYSTEM="systemd" SERVICE_NAME="${SYSTEMD_SERVICE_NAME}" if ! command -v systemctl >/dev/null 2>&1; then red "检测到 APT 环境,但未发现 systemctl,当前脚本暂不支持该初始化系统。" exit 1 fi return 0 fi red "当前仅支持 Debian/Ubuntu(apt + systemd)和 Alpine(apk + OpenRC)。" exit 1 } detect_hysteria_arch() { case "$(uname -m)" in x86_64|amd64) echo "amd64" ;; i386|i686) echo "386" ;; armv5*|arm5*) echo "armv5" ;; armv6*|armv7*|arm) echo "arm" ;; aarch64|arm64) echo "arm64" ;; s390x) echo "s390x" ;; mips64le|mipsle|mips) echo "mipsle" ;; riscv64) echo "riscv64" ;; *) return 1 ;; esac } random_subdomain_prefix() { openssl rand -hex 8 | cut -c1-8 } prompt_nonempty() { local prompt="$1" local value="" while true; do read -r -p "$prompt" value < /dev/tty if [[ -n "${value// }" ]]; then printf '%s' "$value" return 0 fi yellow "输入不能为空,请重新输入。" >&2 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 } 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 green "APT 锁已释放。" return 0 } generate_password() { local length="${1:-36}" local password="" while ((${#password} < length)); do password+="$( openssl rand -base64 48 | tr -dc 'A-Za-z0-9' )" done printf '%s\n' "${password:0:length}" } mask_secret() { local secret="$1" local len="${#secret}" if (( len <= 8 )); then printf '********' else printf '%s****%s' "${secret:0:4}" "${secret: -4}" fi } yaml_quote() { local value="$1" value="${value//\\/\\\\}" value="${value//\"/\\\"}" printf '"%s"' "$value" } validate_domain() { local domain="$1" [[ -n "$domain" ]] || return 1 [[ "$domain" != *$'\n'* ]] || return 1 [[ "$domain" != *$'\r'* ]] || return 1 [[ "$domain" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] } get_server_ip() { local ipv4 ipv6 ipv4="$(curl -4 -fsSL https://api.ipify.org || true)" ipv6="$(curl -6 -fsSL https://api64.ipify.org || true)" echo "${ipv4}|${ipv6}" } apt_update_and_install_base() { if ! confirm_yn "即将执行 apt update 并安装基础依赖 curl sed ufw openssl python3 ca-certificates,是否继续?"; 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 curl sed ufw openssl python3 ca-certificates } apk_update_and_install_base() { if ! confirm_yn "即将执行 apk update 并安装基础依赖 bash curl sed ufw ufw-openrc openssl python3 iptables ca-certificates,是否继续?"; then red "已取消。" exit 1 fi blue "==> 更新 APK 软件索引" apk update blue "==> 安装基础依赖" apk add --no-cache bash curl sed ufw ufw-openrc openssl python3 iptables ca-certificates } install_base_dependencies() { case "${PACKAGE_MANAGER}" in apt) apt_update_and_install_base ;; apk) apk_update_and_install_base ;; *) red "未知包管理器:${PACKAGE_MANAGER}" exit 1 ;; esac } service_exists() { local service="$1" if is_systemd; then systemctl list-unit-files 2>/dev/null | grep -Eq "^${service}(\\.service)?" return $? fi if is_openrc; then [[ -x "/etc/init.d/${service}" ]] return $? fi return 1 } stop_and_disable_service() { local service="$1" if is_systemd; then systemctl stop "${service}" || true systemctl disable "${service}" || true return 0 fi if is_openrc; then rc-service "${service}" stop || true rc-update del "${service}" default || true return 0 fi return 1 } 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 if is_openrc && service_exists ufw; then rc-service ufw stop || true fi fi if service_exists firewalld; then yellow "检测到 firewalld,正在停止并禁用" stop_and_disable_service firewalld || true if is_systemd; then systemctl mask firewalld || true fi fi if service_exists nftables; then yellow "检测到 nftables,正在停止并禁用" stop_and_disable_service 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 if is_openrc && service_exists ufw; then rc-update add ufw default || true rc-service ufw restart || rc-service ufw start || true fi green "UFW 配置完成。" } write_openrc_service_file() { mkdir -p "${HYSTERIA_WORK_DIR}" cat > "${OPENRC_SERVICE_FILE}" < 下载 Hysteria 2 二进制 (${arch})" curl -fsSL "${download_url}" -o "${tmp_bin}" blue "==> 安装 Hysteria 2 二进制" install -m 755 "${tmp_bin}" "${HYSTERIA_BIN}" rm -f "${tmp_bin}" write_openrc_service_file } install_hysteria2() { if ! confirm_yn "即将安装 Hysteria 2,是否继续?"; then red "已取消 Hysteria 2 安装。" exit 1 fi blue "==> 安装 Hysteria 2" if is_openrc; then install_hysteria2_alpine else bash <(curl -fsSL https://get.hy2.sh/) fi green "Hysteria 2 安装完成。" } run_domain_selector() { if confirm_yn "是否执行外部域名筛选脚本?"; then blue "==> 执行域名筛选脚本" bash <(curl -sL https://raw.githubusercontent.com/ccxkai233/Domain_Selector/main/domain_check.sh) || true else yellow "已跳过域名筛选脚本。" fi } cf_api_request() { local method="$1" local url="$2" local data="${3:-}" if [[ -n "$data" ]]; then curl -fsSL -X "$method" "$url" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "$data" else curl -fsSL -X "$method" "$url" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" fi } cf_extract_zone_id() { python3 -c ' import json, sys data = json.load(sys.stdin) result = data.get("result") or [] if result and isinstance(result[0], dict): print(result[0].get("id", ""), end="") ' } cf_extract_dns_record_id() { python3 -c ' import json, sys data = json.load(sys.stdin) result = data.get("result") or [] if result and isinstance(result[0], dict): print(result[0].get("id", ""), end="") ' } get_cf_zone_id() { local zone="$1" cf_api_request GET "https://api.cloudflare.com/client/v4/zones?name=${zone}" \ | cf_extract_zone_id } delete_existing_cf_dns_record() { local zone_id="$1" local full_domain="$2" local type="$3" local record_id record_id="$( cf_api_request GET "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?name=${full_domain}&type=${type}" \ | cf_extract_dns_record_id )" if [[ -n "$record_id" ]]; then cf_api_request DELETE "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" >/dev/null fi } create_cf_dns_record_type() { local zone_id="$1" local full_domain="$2" local type="$3" local content="$4" [[ -z "$content" ]] && return 0 local payload payload=$( cat </dev/null; then red "创建 Cloudflare ${type} 记录失败,请检查 Token 权限和 Zone 配置。" >&2 return 1 fi } create_cloudflare_dns_record() { local zone="$1" local subdomain="$2" local ipv4="$3" local ipv6="$4" local full_domain="${subdomain}.${zone}" local zone_id echo "==> 在 Cloudflare 中创建 DNS 记录" >&2 echo "Zone: ${zone}" >&2 echo "完整域名: ${full_domain}" >&2 if ! zone_id="$(get_cf_zone_id "$zone")"; then red "调用 Cloudflare API 获取 Zone ID 失败,请检查网络和 Token 权限。" >&2 return 1 fi if [[ -z "$zone_id" ]]; then red "无法获取 Cloudflare Zone ID,请检查 Token 权限或 Zone 名称。" >&2 return 1 fi delete_existing_cf_dns_record "$zone_id" "$full_domain" "A" || true delete_existing_cf_dns_record "$zone_id" "$full_domain" "AAAA" || true if [[ -n "$ipv4" ]]; then create_cf_dns_record_type "$zone_id" "$full_domain" "A" "$ipv4" || return 1 echo "已创建 A 记录 -> ${ipv4}" >&2 fi if [[ -n "$ipv6" ]]; then create_cf_dns_record_type "$zone_id" "$full_domain" "AAAA" "$ipv6" || return 1 echo "已创建 AAAA 记录 -> ${ipv6}" >&2 fi if ! validate_domain "$full_domain"; then red "生成的域名格式无效:${full_domain}" >&2 return 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 } set_config_permissions() { local service_user service_group if is_openrc; then chown root:root "${CONFIG_FILE}" chmod 600 "${CONFIG_FILE}" green "已按 OpenRC 服务配置将配置文件权限设置为 root:root 600" return 0 fi service_user="$(systemctl show -p User --value "${SERVICE_NAME}" 2>/dev/null || true)" service_user="${service_user//$'\n'/}" if [[ -n "${service_user}" && "${service_user}" != "root" ]]; then service_group="$(id -gn "${service_user}" 2>/dev/null || true)" if [[ -n "${service_group}" ]]; then chown root:"${service_group}" "${CONFIG_FILE}" chmod 640 "${CONFIG_FILE}" green "已按服务账户 ${service_user}:${service_group} 设置配置文件权限为 640" return 0 fi fi chown root:root "${CONFIG_FILE}" chmod 644 "${CONFIG_FILE}" yellow "未识别到可用的服务账户组,已回退为 root:root 且权限 644" } render_userpass_block() { local indent="${1:-4}" local prefix="" entry username password prefix="$(printf '%*s' "${indent}" '')" for entry in "${USERPASS_ENTRIES[@]}"; do username="${entry%%:*}" password="${entry#*:}" printf '%s%s: %s\n' "${prefix}" "${username}" "$(yaml_quote "$password")" done } render_config() { local domain="$1" local email="$2" local cf_token="$3" local proxy_url="$4" cat < 即将写入如下配置到 ${CONFIG_FILE}" render_preview_config "$domain" "$email" "$cf_token" "$proxy_url" echo if ! confirm_yn "是否确认写入配置文件?"; then red "已取消写入配置。" exit 1 fi mkdir -p /etc/hysteria backup_existing_config render_config "$domain" "$email" "$cf_token" "$proxy_url" > "${CONFIG_FILE}" set_config_permissions green "配置已写入 ${CONFIG_FILE}" } start_service() { if ! confirm_yn "是否启动并设置 Hysteria 服务开机自启?"; then red "已取消启动服务。" exit 1 fi blue "==> 启动并设置开机自启" if is_systemd; then systemctl daemon-reload || true systemctl enable --now "${SERVICE_NAME}" systemctl restart "${SERVICE_NAME}" else rc-update add "${SERVICE_NAME}" default || true rc-service "${SERVICE_NAME}" restart || rc-service "${SERVICE_NAME}" start fi green "服务已启动。" } show_service_status() { if is_systemd; then systemctl --no-pager --full status "${SERVICE_NAME}" || true else rc-service "${SERVICE_NAME}" status || true pgrep -af "${HYSTERIA_BIN##*/}" || true fi } show_recent_logs() { if is_systemd; then journalctl --no-pager -n 30 -u "${SERVICE_NAME}" || true else if [[ -f "${OPENRC_LOG_FILE}" ]]; then tail -n 30 "${OPENRC_LOG_FILE}" || true else yellow "暂未找到 OpenRC 日志文件:${OPENRC_LOG_FILE}" fi fi } show_result() { local domain="$1" local proxy_url="$2" local ip_info="$3" local ipv4="${ip_info%%|*}" local ipv6="${ip_info##*|}" local entry username password echo green "================= HY2 节点信息 =================" echo "域名: ${domain}" echo "端口: 443" echo "认证方式: userpass" echo "服务管理器: ${INIT_SYSTEM}" echo "伪装站点: ${proxy_url}" echo "流量统计监听: 127.0.0.1:9999" echo "流量统计密钥: ${TRAFFIC_STATS_SECRET}" [[ -n "${ipv4}" ]] && echo "IPv4: ${ipv4}" [[ -n "${ipv6}" ]] && echo "IPv6: ${ipv6}" echo echo "客户端连接参数:" echo " server: ${domain}:443" echo " sni: ${domain}" echo " auth: userpass" echo echo "设备账号:" for entry in "${USERPASS_ENTRIES[@]}"; do username="${entry%%:*}" password="${entry#*:}" echo " ${username}: ${password}" done echo echo "hysteria 状态:" show_service_status echo echo "最近日志:" show_recent_logs echo "================================================" } main() { require_root detect_platform if ! confirm_yn "本脚本将更新软件源、关闭现有防火墙、配置 UFW、安装并配置 Hysteria 2(检测到: $(platform_label)),是否继续?"; then red "用户取消执行。" exit 1 fi install_base_dependencies require_cmd curl require_cmd sed require_cmd ufw require_cmd openssl require_cmd python3 if is_systemd; then require_cmd systemctl else require_cmd rc-service require_cmd rc-update fi require_python3_json || { red "python3 缺少 json 模块,无法解析 Cloudflare API 返回值。"; exit 1; } local email zone subdomain default_subdomain proxy_url ip_info ipv4 ipv6 domain username email="$(prompt_nonempty '请输入 ACME 邮箱: ')" zone="$(prompt_nonempty '请输入 Cloudflare Zone(例如 example.com): ')" default_subdomain="$(random_subdomain_prefix)" read -r -p "请输入要创建的子域名前缀(例如 hy2,直接回车使用默认值 ${default_subdomain}): " subdomain subdomain="${subdomain:-$default_subdomain}" subdomain="$(printf '%s' "$subdomain" | tr 'A-Z' 'a-z' | tr -cd 'a-z0-9-')" CF_API_TOKEN="$(prompt_nonempty '请输入 Cloudflare API Token: ')" export CF_API_TOKEN USERPASS_ENTRIES=() for username in "${DEVICE_USERS[@]}"; do USERPASS_ENTRIES+=("${username}:$(generate_password 36)") done TRAFFIC_STATS_SECRET="$(generate_password 36)" ip_info="$(get_server_ip)" ipv4="${ip_info%%|*}" ipv6="${ip_info##*|}" if [[ -z "$ipv4" && -z "$ipv6" ]]; then red "无法获取服务器公网 IP。" exit 1 fi disable_existing_firewalls configure_ufw install_hysteria2 run_domain_selector proxy_url="$(prompt_nonempty '请输入最终用于 masquerade 的完整 URL(例如 https://example.com/): ')" if confirm_yn "是否在 Cloudflare 中自动创建 DNS 记录 ${subdomain}.${zone}?"; then if ! domain="$(create_cloudflare_dns_record "$zone" "$subdomain" "$ipv4" "$ipv6")"; then red "自动创建 Cloudflare DNS 记录失败,脚本已中止,未写入 Hysteria 配置。" exit 1 fi else domain="$(prompt_nonempty '请输入已存在并已解析到本机的完整域名: ')" fi write_config "${domain}" "${email}" "${CF_API_TOKEN}" "${proxy_url}" start_service show_result "${domain}" "${proxy_url}" "${ip_info}" } main "$@"