一键部署Reality和Hysteria2
最近让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

