Files
sh/hy2.sh
2026-03-28 17:36:46 +08:00

875 lines
22 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.
#!/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但未发现 OpenRCrc-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/Ubuntuapt + systemd和 Alpineapk + 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}" <<EOF
#!/sbin/openrc-run
description="Hysteria 2 server"
command="${HYSTERIA_BIN}"
command_args="server -c ${CONFIG_FILE}"
command_background="yes"
pidfile="/run/\${RC_SVCNAME}.pid"
start_stop_daemon_args="--chdir ${HYSTERIA_WORK_DIR} --stdout ${OPENRC_LOG_FILE} --stderr ${OPENRC_LOG_FILE}"
depend() {
need net
use dns logger firewall
}
start_pre() {
checkpath --directory --owner root:root --mode 0755 ${HYSTERIA_WORK_DIR}
checkpath --file --owner root:root --mode 0644 ${OPENRC_LOG_FILE}
}
EOF
chmod 755 "${OPENRC_SERVICE_FILE}"
}
install_hysteria2_alpine() {
local arch download_url tmp_bin
arch="$(detect_hysteria_arch)" || {
red "当前架构 $(uname -m) 暂无对应的 Alpine 安装分支。"
exit 1
}
download_url="https://github.com/apernet/hysteria/releases/latest/download/hysteria-linux-${arch}"
tmp_bin="$(mktemp)"
blue "==> 下载 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 <<EOF
{"type":"${type}","name":"${full_domain}","content":"${content}","ttl":120,"proxied":false}
EOF
)
if ! cf_api_request POST "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records" "$payload" >/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 <<EOF
listen: :443
acme:
domains:
- $(yaml_quote "$domain")
email: $(yaml_quote "$email")
type: dns
dns:
name: cloudflare
config:
cloudflare_api_token: $(yaml_quote "$cf_token")
auth:
type: userpass
userpass:
EOF
render_userpass_block 4
cat <<EOF
trafficStats:
listen: 127.0.0.1:9999
secret: $(yaml_quote "$TRAFFIC_STATS_SECRET")
disableUDP: false
udpIdleTimeout: 90s
resolver:
type: https
https:
addr: 1.1.1.1:443
timeout: 10s
sni: cloudflare-dns.com
insecure: false
speedTest: true
masquerade:
type: proxy
proxy:
url: $(yaml_quote "$proxy_url")
rewriteHost: true
insecure: false
EOF
}
render_preview_config() {
local domain="$1"
local email="$2"
local cf_token="$3"
local proxy_url="$4"
local masked_token
if ! validate_domain "$domain"; then
red "域名无效,拒绝写入配置:${domain@Q}"
exit 1
fi
masked_token="$(mask_secret "$cf_token")"
render_config "$domain" "$email" "$masked_token" "$proxy_url"
}
write_config() {
local domain="$1"
local email="$2"
local cf_token="$3"
local proxy_url="$4"
if ! validate_domain "$domain"; then
red "域名无效,拒绝写入配置:${domain@Q}"
exit 1
fi
echo
blue "==> 即将写入如下配置到 ${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 "$@"