最近让chatgpt写了一个在vps上一键部署xray+Reality和xray+Hysteria2的脚本。

如果要配置Hysteria2,首先需要申请一个域名,然后在dns配置的地方将域名指向vps的ipv4节点,并开启vps防火墙的tcp 80(用于let’sencrypt证书申请),tcp 443(默认给reality)、udp 8443(默认给hy2)等端口。

这个脚本支持功能如下:

1.一键部署xray+Reality

2.一键部署xray+Hysteria2(可选),支持let’sencrypt证书自动申请和续期。

3.自动生成客户端参数信息。

4.支持脚本执行中断后续跑

5.自动配置ufw和bbr

脚本内容为:

#!/usr/bin/env bash
# Xray VLESS Reality + Hysteria2 dual-instance installer
# Supported: Debian / Ubuntu with systemd
# Resumable: reuses /root/xray-dual-state.env on subsequent runs.
set -Eeuo pipefail
umask 077

XRAY_VERSION="v26.10.21"
XRAY_BIN="/usr/local/bin/xray"
XRAY_SHARE="/usr/local/share/xray"
BASE_DIR="/usr/local/etc/xray"
REALITY_DIR="$BASE_DIR/reality"
HY2_DIR="$BASE_DIR/hy2"
HY2_CERT_DIR="$HY2_DIR/certs"
REALITY_CONFIG="$REALITY_DIR/config.json"
HY2_CONFIG="$HY2_DIR/config.json"
REALITY_SERVICE="xray-reality"
HY2_SERVICE="xray-hy2"
REALITY_SERVICE_FILE="/etc/systemd/system/${REALITY_SERVICE}.service"
HY2_SERVICE_FILE="/etc/systemd/system/${HY2_SERVICE}.service"
CERTBOT_HOOK="/etc/letsencrypt/renewal-hooks/deploy/50-xray-hy2.sh"
BACKUP_DIR="/root/xray-dual-backup"
STATE_FILE="/root/xray-dual-state.env"
INFO_FILE="/root/xray-dual-client.txt"
PHASE="初始化"
TMP_DIR=""
REALITY_TMP=""
HY2_TMP=""
HOOK_TMP=""
INFO_TMP=""
CERT_TMP=""
KEY_TMP=""

log()  { printf '\033[32m[%s]\033[0m %s\n' "$(date '+%F %T')" "$*"; }
warn() { printf '\033[33m[WARN]\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[31m[ERROR]\033[0m %s\n' "$*" >&2; exit 1; }

cleanup() {
  [[ -z "$TMP_DIR" ]] || rm -rf -- "$TMP_DIR"
  [[ -z "$REALITY_TMP" ]] || rm -f -- "$REALITY_TMP"
  [[ -z "$HY2_TMP" ]] || rm -f -- "$HY2_TMP"
  [[ -z "$HOOK_TMP" ]] || rm -f -- "$HOOK_TMP"
  [[ -z "$INFO_TMP" ]] || rm -f -- "$INFO_TMP"
  [[ -z "$CERT_TMP" ]] || rm -f -- "$CERT_TMP"
  [[ -z "$KEY_TMP" ]] || rm -f -- "$KEY_TMP"
}

on_error() {
  local rc=$?
  trap - ERR
  printf '\n\033[31m[ERROR]\033[0m 阶段「%s」失败,退出码=%s,脚本位置=%s\n' \
    "$PHASE" "$rc" "${BASH_LINENO[0]:-unknown}" >&2
  if command -v systemctl >/dev/null 2>&1; then
    systemctl --no-pager -l status "$REALITY_SERVICE" >&2 || true
    journalctl -u "$REALITY_SERVICE" -n 50 --no-pager >&2 || true
    if [[ "${CONFIGURE_HY2:-1}" == "1" ]]; then
      systemctl --no-pager -l status "$HY2_SERVICE" >&2 || true
      journalctl -u "$HY2_SERVICE" -n 50 --no-pager >&2 || true
    fi
  fi
  printf '状态文件(如已生成):%s\n' "$STATE_FILE" >&2
  exit "$rc"
}
trap on_error ERR
trap cleanup EXIT

require_root() { [[ ${EUID} -eq 0 ]] || die "请使用 root 用户运行"; }

ask() {
  local var="$1" prompt="$2" default="${3:-}" value
  if [[ -n "$default" ]]; then
    read -r -p "$prompt [$default]: " value
    value=${value:-$default}
  else
    read -r -p "$prompt: " value
  fi
  printf -v "$var" '%s' "$value"
}

save_state() {
  local tmp
  tmp=$(mktemp /root/xray-dual-state.XXXXXX)
  {
    printf 'CONFIGURE_HY2=%q\n' "${CONFIGURE_HY2:-1}"
    printf 'REALITY_PORT=%q\n' "${REALITY_PORT:-}"
    printf 'HY2_PORT=%q\n' "${HY2_PORT:-}"
    printf 'REALITY_SNI=%q\n' "${REALITY_SNI:-}"
    printf 'HY2_DOMAIN=%q\n' "${HY2_DOMAIN:-}"
    printf 'LE_EMAIL=%q\n' "${LE_EMAIL:-}"
    printf 'SERVER_IP=%q\n' "${SERVER_IP:-}"
    printf 'REALITY_UUID=%q\n' "${REALITY_UUID:-}"
    printf 'REALITY_PRIVATE_KEY=%q\n' "${REALITY_PRIVATE_KEY:-}"
    printf 'REALITY_PUBLIC_KEY=%q\n' "${REALITY_PUBLIC_KEY:-}"
    printf 'REALITY_SHORT_ID=%q\n' "${REALITY_SHORT_ID:-}"
    printf 'HY2_PASSWORD=%q\n' "${HY2_PASSWORD:-}"
  } > "$tmp"
  chmod 600 "$tmp"
  chown root:root "$tmp"
  mv -f "$tmp" "$STATE_FILE"
}

load_state() {
  if [[ -e "$STATE_FILE" ]]; then
    [[ -f "$STATE_FILE" ]] || die "$STATE_FILE 不是普通文件"
    [[ $(stat -c '%u' "$STATE_FILE") == 0 ]] || die "$STATE_FILE 必须由 root 拥有"
    chmod 600 "$STATE_FILE"
    # This file is generated by this script and root-owned.
    # shellcheck disable=SC1090
    source "$STATE_FILE"
    log "已读取上次状态;已有凭据将被复用。"
  fi
}

port_listening() {
  local port="$1" proto="$2"
  if [[ "$proto" == tcp ]]; then
    ss -H -lnt "( sport = :$port )" 2>/dev/null | grep -q .
  else
    ss -H -lnu "( sport = :$port )" 2>/dev/null | grep -q .
  fi
}

check_port_available_or_ours() {
  local port="$1" proto="$2" service="$3"
  if port_listening "$port" "$proto"; then
    if systemctl is-active --quiet "$service"; then
      log "$proto/$port 已由 $service 监听,按续跑处理。"
    else
      die "$proto/$port 已被占用,但 $service 未运行。请用 ss -lntup 查明占用进程。"
    fi
  fi
}

create_user() {
  local username="$1"
  getent group "$username" >/dev/null || groupadd --system "$username"
  if id "$username" >/dev/null 2>&1; then
    usermod --gid "$username" --shell /usr/sbin/nologin "$username"
  else
    useradd --system --gid "$username" --no-create-home --shell /usr/sbin/nologin "$username"
  fi
}

atomic_install_text() {
  local target="$1" owner="$2" group="$3" mode="$4" tmp
  tmp=$(mktemp "${target}.tmp.XXXXXX")
  cat > "$tmp"
  chown "$owner:$group" "$tmp"
  chmod "$mode" "$tmp"
  mv -f "$tmp" "$target"
}

install_renewal_hook() {
  install -d -o root -g root -m 755 "$(dirname "$CERTBOT_HOOK")"
  HOOK_TMP=$(mktemp "${CERTBOT_HOOK}.XXXXXX")
  cat > "$HOOK_TMP" <<'HOOK'
#!/usr/bin/env bash
set -Eeuo pipefail

# Certbot supplies the exact lineage directory for the certificate renewed.
CERT_SOURCE="${RENEWED_LINEAGE:?Certbot did not provide RENEWED_LINEAGE}"
CERT_DIR="/usr/local/etc/xray/hy2/certs"
SERVICE="xray-hy2.service"

[[ -s "$CERT_SOURCE/fullchain.pem" ]] || { echo "Missing fullchain.pem in $CERT_SOURCE" >&2; exit 1; }
[[ -s "$CERT_SOURCE/privkey.pem" ]] || { echo "Missing privkey.pem in $CERT_SOURCE" >&2; exit 1; }

install -d -o root -g xray-hy2 -m 750 "$CERT_DIR"
FULLCHAIN_TMP=$(mktemp "$CERT_DIR/fullchain.pem.XXXXXX")
PRIVKEY_TMP=$(mktemp "$CERT_DIR/privkey.pem.XXXXXX")
cleanup() {
  rm -f -- "${FULLCHAIN_TMP:-}" "${PRIVKEY_TMP:-}"
}
trap cleanup EXIT

install -o root -g xray-hy2 -m 640 "$CERT_SOURCE/fullchain.pem" "$FULLCHAIN_TMP"
install -o root -g xray-hy2 -m 640 "$CERT_SOURCE/privkey.pem" "$PRIVKEY_TMP"

# Validate the certificate/key pair before replacing the active files.
CERT_PUB=$(openssl x509 -in "$FULLCHAIN_TMP" -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')
KEY_PUB=$(openssl pkey -in "$PRIVKEY_TMP" -pubout -outform DER | sha256sum | awk '{print $1}')
[[ "$CERT_PUB" == "$KEY_PUB" ]] || { echo "Certificate and private key do not match" >&2; exit 1; }

mv -f "$FULLCHAIN_TMP" "$CERT_DIR/fullchain.pem"
FULLCHAIN_TMP=""
mv -f "$PRIVKEY_TMP" "$CERT_DIR/privkey.pem"
PRIVKEY_TMP=""

# During first issuance the service may not exist yet; the installer will
# copy the certificate and start the service after certbot returns.
if systemctl cat "$SERVICE" >/dev/null 2>&1; then
  systemctl restart "$SERVICE"
  systemctl is-active --quiet "$SERVICE"
  echo "Updated HY2 certificate and restarted $SERVICE"
else
  echo "Certificate copied; $SERVICE is not installed yet, skipping restart."
fi
HOOK
  chmod 700 "$HOOK_TMP"
  chown root:root "$HOOK_TMP"
  mv -f "$HOOK_TMP" "$CERTBOT_HOOK"
  HOOK_TMP=""
  [[ -x "$CERTBOT_HOOK" ]] || die "续期钩子安装失败或不可执行"
  bash -n "$CERTBOT_HOOK" || die "续期钩子语法检查失败"
}

require_root
command -v systemctl >/dev/null || die "系统不支持 systemd"
command -v apt-get >/dev/null || die "仅支持 Debian / Ubuntu"

ARCH=$(uname -m)
case "$ARCH" in
  x86_64) XRAY_ARCH="64" ;;
  aarch64|arm64) XRAY_ARCH="arm64-v8a" ;;
  *) die "不支持的 CPU 架构:$ARCH" ;;
esac

printf '\n==============================================\n Xray Reality / 可选 Hysteria2 安装器\n Xray: %s\n==============================================\n' "$XRAY_VERSION"

PHASE="读取参数和恢复状态"
load_state

# Default to the previous dual-mode behavior, but let the operator opt out.
HY2_DEFAULT="y"
[[ "${CONFIGURE_HY2:-1}" == "0" ]] && HY2_DEFAULT="n"
if [[ "$HY2_DEFAULT" == "y" ]]; then
  read -r -p "是否配置 Hysteria2 (HY2)?[Y/n,默认 y]: " HY2_CHOICE
else
  read -r -p "是否配置 Hysteria2 (HY2)?[y/N,默认 n]: " HY2_CHOICE
fi
HY2_CHOICE=${HY2_CHOICE:-$HY2_DEFAULT}
case "$HY2_CHOICE" in
  [Yy]|[Yy][Ee][Ss]) CONFIGURE_HY2=1 ;;
  [Nn]|[Nn][Oo]) CONFIGURE_HY2=0 ;;
  *) die "请输入 y 或 n,表示是否配置 HY2" ;;
esac

ask REALITY_PORT "Reality TCP 端口" "${REALITY_PORT:-443}"
ask REALITY_SNI "Reality SNI" "${REALITY_SNI:-www.microsoft.com}"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  ask HY2_PORT "HY2 UDP 端口" "${HY2_PORT:-8443}"
  ask HY2_DOMAIN "HY2 域名" "${HY2_DOMAIN:-}"
  ask LE_EMAIL "Let's Encrypt 邮箱" "${LE_EMAIL:-}"
fi
ask SERVER_IP "VPS 公网 IPv4(留空自动检测)" "${SERVER_IP:-}"

[[ "$REALITY_PORT" =~ ^[0-9]+$ ]] && (( 10#$REALITY_PORT >= 1 && 10#$REALITY_PORT <= 65535 )) || die "Reality 端口无效"
REALITY_PORT=$((10#$REALITY_PORT))
[[ "$REALITY_SNI" =~ ^[A-Za-z0-9.-]+$ ]] || die "Reality SNI 格式错误"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  [[ "$HY2_PORT" =~ ^[0-9]+$ ]] && (( 10#$HY2_PORT >= 1 && 10#$HY2_PORT <= 65535 )) || die "HY2 端口无效"
  HY2_PORT=$((10#$HY2_PORT))
  [[ "$REALITY_PORT" != "$HY2_PORT" ]] || die "Reality 与 HY2 端口不能相同"
  [[ "$HY2_DOMAIN" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || die "HY2 域名格式错误"
  [[ "$LE_EMAIL" =~ ^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$ ]] || die "邮箱格式错误"
fi
if [[ -z "$SERVER_IP" ]]; then
  SERVER_IP=$(curl -4 -fsS --max-time 10 https://api.ipify.org) || die "无法获取公网 IPv4,请手动输入"
fi
python3 - "$SERVER_IP" <<'PY'
import ipaddress, sys
try:
    ipaddress.IPv4Address(sys.argv[1])
except Exception:
    raise SystemExit("SERVER_IP 不是有效的 IPv4 地址")
PY
save_state

printf '\nReality TCP: %s\nReality SNI: %s\n' "$REALITY_PORT" "$REALITY_SNI"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  printf 'HY2 UDP: %s\nHY2 Domain: %s\nLE Email: %s\n' "$HY2_PORT" "$HY2_DOMAIN" "$LE_EMAIL"
else
  printf 'HY2: disabled (仅部署 Reality)\n'
fi
printf 'Server IPv4: %s\n\n' "$SERVER_IP"
read -r -p "确认部署/续跑?[y/N]: " CONFIRM
[[ "$CONFIRM" =~ ^[Yy]$ ]] || die "用户取消;参数已保存到 $STATE_FILE"

PHASE="检查端口占用"
check_port_available_or_ours "$REALITY_PORT" tcp "$REALITY_SERVICE"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  check_port_available_or_ours "$HY2_PORT" udp "$HY2_SERVICE"
  if port_listening 80 tcp; then
    CERT_SOURCE_CHECK="/etc/letsencrypt/live/${HY2_DOMAIN}"
    if [[ ! -s "$CERT_SOURCE_CHECK/fullchain.pem" || ! -s "$CERT_SOURCE_CHECK/privkey.pem" ]] || \
       ! openssl x509 -in "$CERT_SOURCE_CHECK/fullchain.pem" -noout -checkend 86400 >/dev/null 2>&1; then
      die "TCP 80 已被占用,且没有可复用的有效证书;standalone 签发需要释放 TCP 80。"
    else
      log "TCP 80 已被占用,但已有有效证书,本次无需使用 standalone 验证。"
    fi
  fi
fi

PHASE="安装系统依赖"
export DEBIAN_FRONTEND=noninteractive
apt-get update
APT_PACKAGES=(curl unzip openssl ca-certificates iproute2 jq python3)
if [[ "$CONFIGURE_HY2" == "1" ]]; then APT_PACKAGES+=(certbot); fi
apt-get install -y "${APT_PACKAGES[@]}"

PHASE="下载和安装 Xray"
TMP_DIR=$(mktemp -d)
DOWNLOAD_URL="https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/Xray-linux-${XRAY_ARCH}.zip"
curl -fL --retry 3 --connect-timeout 15 "$DOWNLOAD_URL" -o "$TMP_DIR/xray.zip"
mkdir -p "$TMP_DIR/release"
unzip -oq "$TMP_DIR/xray.zip" -d "$TMP_DIR/release"
for f in xray geoip.dat geosite.dat; do [[ -s "$TMP_DIR/release/$f" ]] || die "下载包缺少文件:$f"; done
install -d -o root -g root -m 755 /usr/local/bin "$XRAY_SHARE"
install -m 755 "$TMP_DIR/release/xray" "$XRAY_BIN"
install -m 644 "$TMP_DIR/release/geoip.dat" "$TMP_DIR/release/geosite.dat" "$XRAY_SHARE/"
"$XRAY_BIN" version

PHASE="创建服务用户和目录"
create_user xray-reality
install -d -o root -g root -m 711 "$BASE_DIR"
install -d -o root -g xray-reality -m 750 "$REALITY_DIR"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  create_user xray-hy2
  install -d -o root -g xray-hy2 -m 750 "$HY2_DIR" "$HY2_CERT_DIR"
fi

PHASE="生成或复用服务凭据"
if [[ -z "${REALITY_UUID:-}" ]]; then REALITY_UUID=$("$XRAY_BIN" uuid); fi
if [[ -z "${REALITY_PRIVATE_KEY:-}" || -z "${REALITY_PUBLIC_KEY:-}" ]]; then
  KEY_OUTPUT=$("$XRAY_BIN" x25519)
  REALITY_PRIVATE_KEY=$(sed -n 's/^PrivateKey: *//p' <<< "$KEY_OUTPUT" | head -n1)
  REALITY_PUBLIC_KEY=$(sed -n 's/^Password (PublicKey): *//p' <<< "$KEY_OUTPUT" | head -n1)
fi
[[ -n "$REALITY_UUID" && -n "$REALITY_PRIVATE_KEY" && -n "$REALITY_PUBLIC_KEY" ]] || die "Reality 凭据生成失败"
if [[ -z "${REALITY_SHORT_ID:-}" ]]; then REALITY_SHORT_ID=$(openssl rand -hex 8); fi
if [[ "$CONFIGURE_HY2" == "1" && -z "${HY2_PASSWORD:-}" ]]; then HY2_PASSWORD=$(openssl rand -hex 16); fi
save_state

PHASE="备份现有配置"
install -d -o root -g root -m 700 "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILES=("$REALITY_CONFIG" "$REALITY_SERVICE_FILE" "$HY2_CONFIG" "$HY2_SERVICE_FILE" "$CERTBOT_HOOK")
for f in "${BACKUP_FILES[@]}"; do
  if [[ -f "$f" ]]; then cp -a "$f" "$BACKUP_DIR/$(basename "$f").${TIMESTAMP}"; fi
done

if [[ "$CONFIGURE_HY2" == "1" ]]; then
  PHASE="安装 Certbot 续期钩子"
  install_renewal_hook
  systemctl enable --now certbot.timer

  PHASE="申请或复用 Let's Encrypt 证书"
  CERT_SOURCE="/etc/letsencrypt/live/${HY2_DOMAIN}"
  if command -v ufw >/dev/null 2>&1 && ufw status | grep -q 'Status: active'; then
    ufw allow 80/tcp
  fi
  if [[ -s "$CERT_SOURCE/fullchain.pem" && -s "$CERT_SOURCE/privkey.pem" ]] && \
     openssl x509 -in "$CERT_SOURCE/fullchain.pem" -noout -checkend 86400 >/dev/null 2>&1; then
    log "发现有效证书,跳过重新申请。"
  else
    if port_listening 80 tcp; then die "TCP 80 被占用,无法使用 certbot standalone 验证。"; fi
    certbot certonly --standalone --preferred-challenges http --non-interactive --agree-tos \
      --keep-until-expiring --email "$LE_EMAIL" -d "$HY2_DOMAIN"
  fi
  [[ -s "$CERT_SOURCE/fullchain.pem" ]] || die "证书文件不存在:$CERT_SOURCE/fullchain.pem"
  [[ -s "$CERT_SOURCE/privkey.pem" ]] || die "证书私钥不存在:$CERT_SOURCE/privkey.pem"
  openssl x509 -in "$CERT_SOURCE/fullchain.pem" -noout -checkhost "$HY2_DOMAIN" >/dev/null || die "证书域名与 $HY2_DOMAIN 不匹配"

  PHASE="复制并验证 HY2 证书"
  install -o root -g xray-hy2 -m 750 -d "$HY2_CERT_DIR"
  CERT_TMP=$(mktemp "$HY2_CERT_DIR/fullchain.pem.XXXXXX")
  KEY_TMP=$(mktemp "$HY2_CERT_DIR/privkey.pem.XXXXXX")
  install -o root -g xray-hy2 -m 640 "$CERT_SOURCE/fullchain.pem" "$CERT_TMP"
  install -o root -g xray-hy2 -m 640 "$CERT_SOURCE/privkey.pem" "$KEY_TMP"
  CERT_PUB=$(openssl x509 -in "$CERT_TMP" -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')
  KEY_PUB=$(openssl pkey -in "$KEY_TMP" -pubout -outform DER | sha256sum | awk '{print $1}')
  [[ "$CERT_PUB" == "$KEY_PUB" ]] || die "HY2 证书与私钥不匹配"
  mv -f "$CERT_TMP" "$HY2_CERT_DIR/fullchain.pem"; CERT_TMP=""
  mv -f "$KEY_TMP" "$HY2_CERT_DIR/privkey.pem"; KEY_TMP=""
else
  PHASE="确保 HY2 处于停用状态"
  if systemctl cat "$HY2_SERVICE" >/dev/null 2>&1; then
    systemctl disable --now "$HY2_SERVICE" || die "无法停止/禁用旧的 HY2 服务"
  fi
  rm -f -- "$CERTBOT_HOOK"
  systemctl daemon-reload
  log "已选择仅部署 Reality;跳过 HY2 用户、配置、证书与续期任务。旧 HY2 服务(如有)已停用。"
fi

PHASE="生成并验证 Reality 配置"
REALITY_TMP=$(mktemp --suffix=.json "$REALITY_DIR/config.json.tmp.XXXXXX")
jq -n \
  --arg uuid "$REALITY_UUID" --arg flow "xtls-rprx-vision" \
  --arg sni "$REALITY_SNI" --arg privateKey "$REALITY_PRIVATE_KEY" \
  --arg shortId "$REALITY_SHORT_ID" --argjson port "$REALITY_PORT" \
  '{log:{loglevel:"warning"},inbounds:[{listen:"0.0.0.0",port:$port,protocol:"vless",settings:{clients:[{id:$uuid,flow:$flow}],decryption:"none"},streamSettings:{network:"raw",security:"reality",realitySettings:{show:false,target:($sni+":443"),xver:0,serverNames:[$sni],privateKey:$privateKey,shortIds:[$shortId]}}}],outbounds:[{protocol:"freedom"}]}' > "$REALITY_TMP"
"$XRAY_BIN" run -test -config "$REALITY_TMP"
chown root:xray-reality "$REALITY_TMP"
chmod 640 "$REALITY_TMP"
mv -f "$REALITY_TMP" "$REALITY_CONFIG"
REALITY_TMP=""

if [[ "$CONFIGURE_HY2" == "1" ]]; then
PHASE="生成并验证 HY2 配置"
HY2_TMP=$(mktemp --suffix=.json "$HY2_DIR/config.json.tmp.XXXXXX")
jq -n \
  --arg password "$HY2_PASSWORD" \
  --arg cert "$HY2_CERT_DIR/fullchain.pem" --arg key "$HY2_CERT_DIR/privkey.pem" \
  --argjson port "$HY2_PORT" \
  '{log:{loglevel:"warning"},inbounds:[{tag:"IN-HY2",listen:"0.0.0.0",port:$port,protocol:"hysteria",settings:{version:2},streamSettings:{network:"hysteria",security:"tls",tlsSettings:{alpn:["h3"],minVersion:"1.3",maxVersion:"1.3",certificates:[{certificateFile:$cert,keyFile:$key}]},hysteriaSettings:{version:2,auth:$password,udpIdleTimeout:60}}}],outbounds:[{protocol:"freedom",tag:"direct"}],routing:{rules:[{type:"field",inboundTag:["IN-HY2"],outboundTag:"direct"}]}}' > "$HY2_TMP"
"$XRAY_BIN" run -test -config "$HY2_TMP"
chown root:xray-hy2 "$HY2_TMP"
chmod 640 "$HY2_TMP"
mv -f "$HY2_TMP" "$HY2_CONFIG"
HY2_TMP=""
fi

PHASE="设置目录和文件权限"
chown root:root "$BASE_DIR"; chmod 711 "$BASE_DIR"
chown root:xray-reality "$REALITY_DIR" "$REALITY_CONFIG"; chmod 750 "$REALITY_DIR"; chmod 640 "$REALITY_CONFIG"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  chown root:xray-hy2 "$HY2_DIR" "$HY2_CONFIG" "$HY2_CERT_DIR" "$HY2_CERT_DIR/fullchain.pem" "$HY2_CERT_DIR/privkey.pem"
  chmod 750 "$HY2_DIR" "$HY2_CERT_DIR"
  chmod 640 "$HY2_CONFIG" "$HY2_CERT_DIR/fullchain.pem" "$HY2_CERT_DIR/privkey.pem"
fi

PHASE="写入 systemd 服务"
atomic_install_text "$REALITY_SERVICE_FILE" root root 644 <<EOF2
[Unit]
Description=Xray Reality Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=xray-reality
Group=xray-reality
Environment=XRAY_LOCATION_ASSET=$XRAY_SHARE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ExecStart=$XRAY_BIN run -config $REALITY_CONFIG
Restart=on-failure
RestartSec=5
LimitNOFILE=1000000
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target
EOF2
if [[ "$CONFIGURE_HY2" == "1" ]]; then
atomic_install_text "$HY2_SERVICE_FILE" root root 644 <<EOF2
[Unit]
Description=Xray Hysteria2 Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=xray-hy2
Group=xray-hy2
Environment=XRAY_LOCATION_ASSET=$XRAY_SHARE
NoNewPrivileges=true
ExecStart=$XRAY_BIN run -config $HY2_CONFIG
Restart=on-failure
RestartSec=5
LimitNOFILE=1000000
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target
EOF2

fi

PHASE="启动并检查服务"
systemctl daemon-reload
systemctl enable "$REALITY_SERVICE"
systemctl restart "$REALITY_SERVICE"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  systemctl enable "$HY2_SERVICE"
  systemctl restart "$HY2_SERVICE"
fi
sleep 2
systemctl is-active --quiet "$REALITY_SERVICE" || die "Reality 服务未能保持 active"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  systemctl is-active --quiet "$HY2_SERVICE" || die "HY2 服务未能保持 active"
fi

PHASE="配置防火墙和 BBR"
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q 'Status: active'; then
  ufw allow "${REALITY_PORT}/tcp"
  if [[ "$CONFIGURE_HY2" == "1" ]]; then
    ufw allow 80/tcp
    ufw allow "${HY2_PORT}/udp"
  fi
  log "UFW 规则已添加。"
else
  if [[ "$CONFIGURE_HY2" == "1" ]]; then
    warn "未检测到活动 UFW;请确认云安全组及 nftables/iptables 放行 TCP 80、TCP ${REALITY_PORT}、UDP ${HY2_PORT}。"
  else
    warn "未检测到活动 UFW;请确认云安全组及 nftables/iptables 放行 TCP ${REALITY_PORT}。"
  fi
fi
cat > /etc/sysctl.d/99-bbr.conf <<'SYSCTL'
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
SYSCTL
sysctl --system >/dev/null || warn "应用 sysctl 参数时有警告,请检查 BBR。"

PHASE="生成客户端分享链接"
REALITY_LINK="vless://${REALITY_UUID}@${SERVER_IP}:${REALITY_PORT}?encryption=none&security=reality&sni=${REALITY_SNI}&fp=chrome&pbk=${REALITY_PUBLIC_KEY}&sid=${REALITY_SHORT_ID}&type=tcp&flow=xtls-rprx-vision#Xray-Reality"
INFO_TMP=$(mktemp /root/xray-dual-client.XXXXXX)
{
  cat <<INFO
========================================
Xray Server
Xray Version: ${XRAY_VERSION}
========================================

[REALITY]
Server: ${SERVER_IP}
Port: ${REALITY_PORT}
UUID: ${REALITY_UUID}
SNI: ${REALITY_SNI}
Flow: xtls-rprx-vision
PublicKey: ${REALITY_PUBLIC_KEY}
ShortID: ${REALITY_SHORT_ID}

${REALITY_LINK}
INFO
  if [[ "$CONFIGURE_HY2" == "1" ]]; then
    HY2_LINK="hysteria2://${HY2_PASSWORD}@${HY2_DOMAIN}:${HY2_PORT}/?sni=${HY2_DOMAIN}&alpn=h3#Xray-HY2"
    cat <<INFO

[HYSTERIA2]
Server: ${HY2_DOMAIN}
Port: ${HY2_PORT}
Password: ${HY2_PASSWORD}
SNI: ${HY2_DOMAIN}
ALPN: h3

${HY2_LINK}

[Certificate]
Domain: ${HY2_DOMAIN}
Certificate: ${CERT_SOURCE}/fullchain.pem
PrivateKey: ${CERT_SOURCE}/privkey.pem
INFO
  fi
  cat <<INFO

[Services]
systemctl status ${REALITY_SERVICE}
INFO
  if [[ "$CONFIGURE_HY2" == "1" ]]; then printf 'systemctl status %s\n' "$HY2_SERVICE"; fi
} > "$INFO_TMP"
chown root:root "$INFO_TMP"
chmod 600 "$INFO_TMP"
mv -f "$INFO_TMP" "$INFO_FILE"
INFO_TMP=""

PHASE="最终检查"
[[ $(stat -c '%a' "$BASE_DIR") == 711 ]] || die "父目录权限不是 711"
systemctl is-enabled --quiet "$REALITY_SERVICE" || die "Reality 服务未启用"
systemctl is-active --quiet "$REALITY_SERVICE" || die "Reality 服务未运行"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  [[ -x "$CERTBOT_HOOK" ]] || die "Certbot deploy hook 不存在或不可执行"
  systemctl is-enabled --quiet certbot.timer || die "certbot.timer 未启用"
  systemctl is-active --quiet certbot.timer || die "certbot.timer 未运行"
  systemctl is-enabled --quiet "$HY2_SERVICE" || die "HY2 服务未启用"
  systemctl is-active --quiet "$HY2_SERVICE" || die "HY2 服务未运行"
else
  if systemctl is-active --quiet "$HY2_SERVICE"; then die "已选择不配置 HY2,但 HY2 仍在运行"; fi
  [[ ! -e "$CERTBOT_HOOK" ]] || die "已选择不配置 HY2,但发现旧续期钩子仍存在"
fi
[[ -s "$INFO_FILE" ]] || die "客户端配置文件未生成"

printf '\n==============================================\n'
if [[ "$CONFIGURE_HY2" == "1" ]]; then log "Reality + HY2 部署/续跑完成!"; else log "Reality-only 部署/续跑完成!"; fi
printf 'Reality: %s\n' "$(systemctl is-active "$REALITY_SERVICE")"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  printf 'HY2:     %s\n' "$(systemctl is-active "$HY2_SERVICE")"
  printf 'Certbot timer: %s\n' "$(systemctl is-active certbot.timer)"
  printf '续期钩子:%s\n' "$CERTBOT_HOOK"
fi
printf '客户端配置:%s\n' "$INFO_FILE"
printf '状态文件:%s(含私钥和密码,请勿外传)\n' "$STATE_FILE"
printf '管理命令:systemctl restart %s\n' "$REALITY_SERVICE"
if [[ "$CONFIGURE_HY2" == "1" ]]; then
  printf '          systemctl restart %s\n' "$HY2_SERVICE"
  printf '日志命令:journalctl -u %s -f\n' "$REALITY_SERVICE"
  printf '          journalctl -u %s -f\n' "$HY2_SERVICE"
  printf '请确认云安全组开放 TCP 80、TCP %s、UDP %s。\n' "$REALITY_PORT" "$HY2_PORT"
else
  printf '日志命令:journalctl -u %s -f\n' "$REALITY_SERVICE"
  printf '请确认云安全组开放 TCP %s。\n' "$REALITY_PORT"
fi

注意:两个xray默认只监听ipv4的端口(”listen”: “0.0.0.0”),如果vps支持ipv4和ipv6双栈可以改成 “listen”: “::” 。

是否直接改可以通过在vps上运行如下命令判断:

sysctl net.ipv6.bindv6only

如果是:

  • 0:IPv6 socket 通常也接受 IPv4 连接。那只需改成 “listen”: “::” ,同时接收v4和v6。
  • 1:只接受 IPv6 连接。则需另外建一个inbounds,加上”listen”: “::” ,单独接收v6。

参考:

1.chatgpt