Files
sh/hy2.sh
2026-03-19 15:04:56 +08:00

452 lines
11 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"' 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"
}
apt_update_and_install_base() {
if ! confirm_yn "即将执行 apt update 并安装基础依赖 curl sed ufw jq openssl是否继续"; then
red "已取消。"
exit 1
fi
blue "==> 更新 APT 软件库"
export DEBIAN_FRONTEND=noninteractive
apt update -y
blue "==> 安装基础依赖"
apt install -y curl sed ufw jq 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 <<EOF
listen: :443
acme:
domains:
- ${full_domain}
email: ${email}
type: dns
dns:
name: cloudflare
config:
cloudflare_api_token: ${cf_token}
auth:
type: password
password: ${password}
masquerade:
type: proxy
proxy:
url: ${proxy_url}
rewriteHost: true
EOF
echo
if ! confirm_yn "是否确认写入配置文件?"; then
red "已取消写入配置。"
exit 1
fi
mkdir -p /etc/hysteria
backup_existing_config
cat > "${CONFIG_FILE}" <<EOF
listen: :443
acme:
domains:
- ${full_domain}
email: ${email}
type: dns
dns:
name: cloudflare
config:
cloudflare_api_token: ${cf_token}
auth:
type: password
password: ${password}
masquerade:
type: proxy
proxy:
url: ${proxy_url}
rewriteHost: true
EOF
chown root:root "${CONFIG_FILE}"
chmod 644 "${CONFIG_FILE}"
green "配置已写入 ${CONFIG_FILE}"
}
start_service() {
if ! confirm_yn "是否启动并设置 Hysteria 服务开机自启?"; then
red "已取消启动服务。"
exit 1
fi
blue "==> 启动并设置开机自启"
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 "$@"