Files
server-deploy/vps-xray/deploy.sh
T
2026-08-05 17:12:18 +08:00

865 lines
27 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 -euo pipefail
# ============================================
# Xray VPS 一键部署脚本
# 支持模式:reality (VLESS-Reality) / fast (VLESS TCP 无 TLS)
# 适用于 Debian 11+ / Ubuntu 20.04+
# ============================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "\n${CYAN}========== $* ==========${NC}"; }
# ===== 使用说明 =====
usage() {
echo "用法: bash deploy.sh [选项]"
echo ""
echo "选项:"
echo " --mode reality VLESS-Reality 模式(默认,推荐,抗封锁)"
echo " --mode fast VLESS TCP 模式(无 TLS,极速但易被检测)"
echo " --show 只读取 .env 打印连接信息和分享链接,不改动任何服务"
echo " --redetect 强制重新挑选 Reality 伪装目标(会改变 SNI"
echo " 所有客户端链接都必须同步更新)"
echo " -h, --help 显示帮助"
exit 0
}
# ===== 解析参数 =====
MODE=""
MODE_EXPLICIT=0
SHOW_ONLY=0
REDETECT=0
while [[ $# -gt 0 ]]; do
case $1 in
--mode) MODE="${2:-}"; MODE_EXPLICIT=1; shift 2 ;;
--show) SHOW_ONLY=1; shift ;;
--redetect) REDETECT=1; shift ;;
-h|--help) usage ;;
*) error "未知参数: $1"; usage ;;
esac
done
# ===== 检查 root =====
# .env 权限为 600,读取它同样需要 root
if [ "$(id -u)" -ne 0 ]; then
error "请使用 root 用户运行: sudo bash deploy.sh"
exit 1
fi
# ===== 加载现有配置(如存在)=====
if [ -f .env ]; then
sed -i 's/\r$//' .env
set -a; source .env; set +a
fi
# 未显式指定 --mode 时沿用 .env 里记录的模式,
# 避免在 fast 模式的机器上重跑脚本被静默切换成 reality
if [ "$MODE_EXPLICIT" -eq 0 ]; then
MODE="${XRAY_MODE:-reality}"
fi
if [[ "$MODE" != "reality" && "$MODE" != "fast" ]]; then
error "无效模式: $MODE(支持 reality / fast"
exit 1
fi
# 布尔开关归一化:容忍 true/TRUE/True/yes/on/1 等写法,
# 避免用户按直觉写了 TRUE 却被静默当成关闭
case "$(printf '%s' "${XRAY_DAILY_RESTART:-false}" | tr '[:upper:]' '[:lower:]')" in
true|yes|on|1) DAILY_RESTART=1 ;;
*) DAILY_RESTART=0 ;;
esac
XRAY_PORT="${XRAY_PORT:-443}"
# ===== 0. 检查并安装依赖 =====
# 精简版系统镜像(如部分 VPS 商的 Debian/Ubuntu minimal)常缺 curl / unzip
ensure_deps() {
step "1/8 检查依赖"
local missing=()
local cmd
for cmd in curl unzip openssl; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ ! -f /etc/ssl/certs/ca-certificates.crt ]; then
missing+=("ca-certificates")
fi
if [ "${#missing[@]}" -eq 0 ]; then
log "依赖齐全: curl unzip openssl ca-certificates"
return
fi
warn "缺少依赖: ${missing[*]},正在自动安装..."
if command -v apt-get &>/dev/null; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq "${missing[@]}"
elif command -v dnf &>/dev/null; then
dnf install -y "${missing[@]}"
elif command -v yum &>/dev/null; then
yum install -y "${missing[@]}"
elif command -v apk &>/dev/null; then
apk add --no-cache "${missing[@]}"
else
error "未识别的包管理器,请手动安装: ${missing[*]}"
exit 1
fi
for cmd in curl unzip openssl; do
if ! command -v "$cmd" &>/dev/null; then
error "依赖安装失败: ${cmd},请手动安装后重试"
exit 1
fi
done
log "依赖安装完成"
}
# ===== 1. 安装 Xray =====
install_xray() {
step "2/8 安装 Xray"
if [ -x /usr/local/bin/xray ]; then
log "Xray 已安装: $(/usr/local/bin/xray version | head -1)"
log "将更新到最新版本..."
fi
# 不用 bash <(curl ...)curl 失败时进程替换只产生空脚本,
# 外层 bash 仍返回 0,会掩盖下载失败
local installer
installer="$(mktemp)"
local url ok=0
for url in \
"https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh" \
"https://github.com/XTLS/Xray-install/raw/main/install-release.sh"
do
if curl -fsSL --max-time 60 "$url" -o "$installer" && [ -s "$installer" ]; then
ok=1
break
fi
warn "下载失败,尝试备用地址: $url"
done
if [ "$ok" -ne 1 ]; then
rm -f "$installer"
error "无法下载 Xray 安装脚本,请检查服务器网络 / DNS"
exit 1
fi
bash "$installer"
rm -f "$installer"
if [ ! -x /usr/local/bin/xray ]; then
error "Xray 安装失败:/usr/local/bin/xray 不存在"
exit 1
fi
log "Xray 安装完成: $(/usr/local/bin/xray version | head -1)"
}
# ===== 2. 生成密钥 =====
# ===== 凭据回收 =====
# 部署目录被删除、或 .env 丢失后重跑脚本,若直接生成新密钥,
# 所有客户端的旧链接会立刻失效——而失效表现与「握手被中间人干扰」
# 完全相同(REALITY: received real certificate),极难排查。
# 因此只要 config.json 还在,就以它为准恢复凭据。
recover_credentials() {
[ -f "$XRAY_CONFIG" ] || return 0
[ -n "${XRAY_UUID:-}" ] && return 0 # .env 已提供,无需回收
local flat uuid priv sid sni dest port
flat=$(tr -d '\n\r\t ' < "$XRAY_CONFIG")
uuid=$(echo "$flat" | grep -oP '"id":"\K[^"]+' | head -1 || true)
[ -z "$uuid" ] && return 0 # 多半是官方安装脚本留下的空壳配置
priv=$(echo "$flat" | grep -oP '"privateKey":"\K[^"]+' | head -1 || true)
sid=$(echo "$flat" | grep -oP '"shortIds":\["\K[^"]+' | head -1 || true)
sni=$(echo "$flat" | grep -oP '"serverNames":\["\K[^"]+' | head -1 || true)
dest=$(echo "$flat" | grep -oP '"dest":"\K[^"]+' | head -1 || true)
port=$(echo "$flat" | grep -oP '"port":\K[0-9]+' | head -1 || true)
warn ".env 缺失,正在从现有的 ${XRAY_CONFIG} 恢复凭据"
warn "(这样客户端已有的分享链接依然可用,不会因重装而失效)"
XRAY_UUID="$uuid"
[ -n "$port" ] && XRAY_PORT="$port"
if [ -n "$priv" ]; then
XRAY_PRIVATE_KEY="$priv"
XRAY_PUBLIC_KEY=$(/usr/local/bin/xray x25519 -i "$priv" 2>/dev/null \
| grep -iE 'public|password' | awk -F': ' '{print $2}' | tr -d '[:space:]' || true)
[ -n "$sid" ] && XRAY_SHORT_ID="$sid"
[ -n "$sni" ] && REALITY_SNI="$sni"
[ -n "$dest" ] && REALITY_DEST="${dest%%:*}"
fi
log "已恢复 UUID: ${XRAY_UUID}"
}
generate_keys() {
step "3/8 生成密钥"
recover_credentials
XRAY_UUID="${XRAY_UUID:-$(cat /proc/sys/kernel/random/uuid)}"
log "UUID: ${XRAY_UUID}"
if [ "$MODE" = "reality" ]; then
if [ -z "${XRAY_PRIVATE_KEY:-}" ] || [ -z "${XRAY_PUBLIC_KEY:-}" ]; then
local keys
keys=$(/usr/local/bin/xray x25519 2>&1)
XRAY_PRIVATE_KEY=$(echo "$keys" | grep -i 'private' | awk -F': ' '{print $2}' | tr -d '[:space:]')
XRAY_PUBLIC_KEY=$(echo "$keys" | grep -i 'public' | awk -F': ' '{print $2}' | tr -d '[:space:]')
fi
XRAY_SHORT_ID="${XRAY_SHORT_ID:-$(openssl rand -hex 8)}"
if [ -z "$XRAY_PRIVATE_KEY" ] || [ -z "$XRAY_PUBLIC_KEY" ]; then
error "密钥生成失败!xray x25519 输出:"
echo "$keys"
exit 1
fi
log "x25519 密钥对已生成或已加载"
log "ShortId: ${XRAY_SHORT_ID}"
fi
}
# ===== 3. 选择 Reality 伪装目标 =====
select_reality_dest() {
if [ "$MODE" != "reality" ]; then
return
fi
step "4/8 选择 Reality 伪装目标"
local candidates=("www.microsoft.com" "dl.google.com" "www.apple.com" "www.amazon.com")
REALITY_DEST="${REALITY_DEST:-}"
REALITY_SNI="${REALITY_SNI:-}"
if [ -n "$REALITY_DEST" ] && [ -n "$REALITY_SNI" ] && [ "$REDETECT" -eq 0 ]; then
log "使用 .env 中已固定的伪装目标: ${REALITY_DEST}"
# 固定下来的伪装站可能哪天失效。这里只做可用性检查并告警,
# 绝不自动更换——更换会改变 SNI,导致所有客户端链接静默失效。
local ms
ms=$(curl -so /dev/null -w '%{time_connect}' --max-time 5 "https://${REALITY_DEST}" 2>/dev/null \
| awk '{printf "%d", $1*1000}') || true
if [ -z "$ms" ] || ! [[ "$ms" =~ ^[0-9]+$ ]] || [ "$ms" -le 0 ]; then
warn "伪装目标 ${REALITY_DEST} 当前不可达!Reality 回落会失败,客户端可能连不上"
warn "如需更换,执行: bash deploy.sh --redetect"
warn "注意:更换会改变 SNI,届时所有客户端链接都必须同步更新"
else
log "伪装目标连通性正常 (${ms}ms)"
fi
return
fi
if [ "$REDETECT" -eq 1 ] && [ -n "$REALITY_DEST" ]; then
warn "--redetect:将重新挑选伪装目标,原目标为 ${REALITY_DEST}"
fi
local best_dest=""
local best_ms=9999
for site in "${candidates[@]}"; do
local ms
# curl 失败时 %{time_connect} 会输出 0.000000,必须要求 ms > 0
# 否则连不通的站点会被当成「延迟最低」选中
ms=$(curl -so /dev/null -w '%{time_connect}' --max-time 3 "https://${site}" 2>/dev/null \
| awk '{printf "%d", $1*1000}') || true
if [ -z "$ms" ] || ! [[ "$ms" =~ ^[0-9]+$ ]] || [ "$ms" -le 0 ]; then
warn " ${site} 不可达,跳过"
continue
fi
log " ${site}${ms}ms"
if [ "$ms" -lt "$best_ms" ]; then
best_ms=$ms
best_dest=$site
fi
done
if [ -z "$best_dest" ]; then
error "所有候选伪装目标都无法连接,请检查服务器网络 / DNS"
error "候选列表: ${candidates[*]}"
exit 1
fi
local old_dest="$REALITY_DEST"
REALITY_DEST="$best_dest"
REALITY_SNI="$best_dest"
log "伪装目标: ${REALITY_DEST} (延迟 ${best_ms}ms)"
if [ -n "$old_dest" ] && [ "$old_dest" != "$best_dest" ]; then
echo ""
warn "════════════════════════════════════════════════════════"
warn "伪装目标已从 ${old_dest} 更换为 ${best_dest}"
warn "SNI 随之改变,所有客户端的旧链接将立即失效!"
warn "部署完成后请用新的分享链接重新配置全部客户端。"
warn "════════════════════════════════════════════════════════"
echo ""
fi
}
# ===== 4. 写入 Xray 配置 =====
XRAY_CONFIG="/usr/local/etc/xray/config.json"
# 对外 IP:.env 指定优先,否则探测出口 IP
SERVER_IP_EFFECTIVE=""
resolve_server_ip() {
if [ -n "${SERVER_IP:-}" ]; then
SERVER_IP_EFFECTIVE="$SERVER_IP"
return
fi
SERVER_IP_EFFECTIVE=$(curl -s --max-time 5 ipv4.ip.sb 2>/dev/null \
|| curl -s --max-time 5 ifconfig.me 2>/dev/null || true)
}
# 阻断规则:
# 1) 本机公网 IP 的「代理端口」—— 客户端若因路由回环把「连接本代理」这件事
# 也丢进隧道,服务器就成了自我中继与流量放大器。只掐这一个端口,
# 不影响经隧道 SSH 回本机等正常用法(那是很常见的运维操作)。
# 2) geoip:private —— 私网与回环。代理用户没有任何理由经本机访问服务器内网,
# 放开等于把内网暴露给所有客户端。
build_block_rules() {
SELF_LOOP_RULE=""
if [[ "$SERVER_IP_EFFECTIVE" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then
SELF_LOOP_RULE=$(cat <<EOF
{
"type": "field",
"outboundTag": "block",
"ip": ["${SERVER_IP_EFFECTIVE}/32"],
"port": "${XRAY_PORT}"
},
EOF
)
fi
}
write_config() {
resolve_server_ip
build_block_rules
mkdir -p "$(dirname "$XRAY_CONFIG")"
# 先写临时文件并校验,通过后再原子替换正式配置,
# 保证校验失败时磁盘上的配置和正在运行的服务都不受影响
# xray 靠文件扩展名判断配置格式,临时文件必须叫 *.json,
# 否则会报 "Failed to get format"。所以用临时目录而非 mktemp 裸文件。
local tmpdir tmp
tmpdir="$(mktemp -d)"
tmp="${tmpdir}/config.json"
if [ "$MODE" = "reality" ]; then
write_config_reality "$tmp"
else
write_config_fast "$tmp"
fi
if ! /usr/local/bin/xray run -test -config "$tmp" >/dev/null 2>&1; then
error "配置校验失败,磁盘配置与运行中的服务均未改动。详细输出:"
/usr/local/bin/xray run -test -config "$tmp" || true
rm -rf "$tmpdir"
exit 1
fi
log "配置语法校验通过"
# 覆盖前备份旧配置,重跑脚本不至于丢掉手工改动(例如新增的 clients)
if [ -f "$XRAY_CONFIG" ]; then
local backup="${XRAY_CONFIG}.bak.$(date '+%Y%m%d%H%M%S')"
cp "$XRAY_CONFIG" "$backup"
log "旧配置已备份: ${backup}"
# 只保留最近 5 份,避免反复部署后无限堆积
ls -1t "${XRAY_CONFIG}".bak.* 2>/dev/null | tail -n +6 | xargs -r rm -f
fi
mv "$tmp" "$XRAY_CONFIG"
rm -rf "$tmpdir"
# xray 服务以 nobody 运行,配置必须对其可读
chmod 644 "$XRAY_CONFIG"
log "配置已写入: ${XRAY_CONFIG}"
}
write_config_reality() {
step "5/8 写入 Xray 配置 (Reality)"
local out="$1"
cat > "$out" << EOF
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"listen": "0.0.0.0",
"port": ${XRAY_PORT},
"protocol": "vless",
"settings": {
"clients": [
{
"id": "${XRAY_UUID}",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "${REALITY_DEST}:443",
"xver": 0,
"serverNames": [
"${REALITY_SNI}"
],
"privateKey": "${XRAY_PRIVATE_KEY}",
"shortIds": [
"${XRAY_SHORT_ID}"
]
}
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"routeOnly": true
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct"
},
{
"protocol": "blackhole",
"tag": "block"
}
],
"routing": {
"rules": [
${SELF_LOOP_RULE}
{
"type": "field",
"outboundTag": "block",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "block",
"protocol": ["bittorrent"]
}
]
}
}
EOF
}
write_config_fast() {
step "5/8 写入 Xray 配置 (Fast TCP)"
local out="$1"
cat > "$out" << EOF
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"listen": "0.0.0.0",
"port": ${XRAY_PORT},
"protocol": "vless",
"settings": {
"clients": [
{
"id": "${XRAY_UUID}"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "none"
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"routeOnly": true
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct"
},
{
"protocol": "blackhole",
"tag": "block"
}
],
"routing": {
"rules": [
${SELF_LOOP_RULE}
{
"type": "field",
"outboundTag": "block",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "block",
"protocol": ["bittorrent"]
}
]
}
}
EOF
}
# ===== 检测当前生效的 SSH 端口 =====
# 防火墙一旦 enable,若没放行真实的 SSH 端口就会把自己锁在门外
detect_ssh_ports() {
local ports=""
if command -v sshd &>/dev/null; then
ports=$(sshd -T 2>/dev/null | awk '/^port /{print $2}' || true)
fi
if [ -z "$ports" ]; then
ports=$(ss -tlnH 2>/dev/null | awk '{print $4}' | sed 's/.*://' | sort -u | grep -x 22 || true)
fi
if [ -z "$ports" ]; then
ports=$(awk '/^[[:space:]]*Port[[:space:]]+[0-9]+/{print $2}' /etc/ssh/sshd_config 2>/dev/null || true)
fi
# 兜底:至少放行 22,避免完全无法登录
[ -z "$ports" ] && ports="22"
echo "$ports"
}
# ===== 5. 防火墙 =====
configure_firewall() {
step "6/8 配置防火墙"
local ssh_ports
ssh_ports=$(detect_ssh_ports)
log "检测到 SSH 端口: $(echo "$ssh_ports" | tr '\n' ' ')"
if command -v ufw &>/dev/null; then
ufw allow "${XRAY_PORT}/tcp"
local p
for p in $ssh_ports; do
ufw allow "${p}/tcp"
done
ufw --force enable
log "ufw 已放行端口 ${XRAY_PORT}/tcp 及 SSH 端口"
elif command -v firewall-cmd &>/dev/null; then
firewall-cmd --permanent --add-port="${XRAY_PORT}/tcp"
local p
for p in $ssh_ports; do
firewall-cmd --permanent --add-port="${p}/tcp"
done
firewall-cmd --reload
log "firewalld 已放行端口 ${XRAY_PORT}/tcp 及 SSH 端口"
else
warn "未检测到防火墙工具,请手动放行端口 ${XRAY_PORT}/tcp"
fi
}
# ===== 6. BBR + 网络调优 =====
tune_network() {
step "7/8 BBR + 网络调优"
cat > /etc/sysctl.d/99-xray-turbo.conf << 'SYSEOF'
# BBR 拥塞控制
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
# TCP 缓冲区(提升大文件 / 视频流吞吐量)
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
# 连接优化
net.ipv4.tcp_fastopen=3
net.ipv4.tcp_slow_start_after_idle=0
net.ipv4.tcp_mtu_probing=1
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_keepalive_time=300
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_max_tw_buckets=262144
net.ipv4.tcp_max_syn_backlog=8192
net.core.somaxconn=8192
net.core.netdev_max_backlog=8192
SYSEOF
sysctl --system > /dev/null 2>&1
log "BBR 已启用,网络参数已优化"
}
# ===== 新增:系统与服务加固 =====
harden_system() {
step "8/8 加固系统与服务"
# 文件描述符上限
if ! grep -q '99-xray-nofile' /etc/security/limits.conf 2>/dev/null; then
cat >> /etc/security/limits.conf << 'EOF'
# 99-xray-nofile
* soft nofile 1000000
* hard nofile 1000000
root soft nofile 1000000
root hard nofile 1000000
EOF
fi
# systemd 服务加固
mkdir -p /etc/systemd/system/xray.service.d
cat > /etc/systemd/system/xray.service.d/override.conf << 'EOF'
[Service]
LimitNOFILE=1000000
Restart=always
RestartSec=3
EOF
# ===== 每日重启 timer:默认关闭 =====
# 崩溃恢复已由上面的 Restart=always + RestartSec=3 覆盖(秒级响应),
# 而每日硬重启会切断全部活动连接(跨夜下载、备份、CI 都会中断),
# 属于为假想问题付出真实代价。仅在确实观察到长期运行后性能退化时才开启。
if [ "$DAILY_RESTART" -ne 1 ]; then
if [ -f /etc/systemd/system/xray-restart.timer ]; then
systemctl disable --now xray-restart.timer 2>/dev/null || true
rm -f /etc/systemd/system/xray-restart.timer \
/etc/systemd/system/xray-restart.service
warn "已移除旧部署遗留的每日重启 timer"
warn "如需保留,请在 .env 中设置 XRAY_DAILY_RESTART=true 后重新部署"
fi
log "每日自动重启: 关闭(崩溃由 Restart=always 秒级恢复)"
return
fi
# 定时重启 timer(每日凌晨 4 点)
cat > /etc/systemd/system/xray-restart.service << 'EOF'
[Unit]
Description=Daily restart of Xray
After=network.target
[Service]
Type=oneshot
ExecStart=/bin/systemctl restart xray
EOF
cat > /etc/systemd/system/xray-restart.timer << 'EOF'
[Unit]
Description=Daily restart Xray at 04:00 CST
[Timer]
OnCalendar=*-*-* 04:00:00
TimeZone=Asia/Shanghai
Persistent=true
[Install]
WantedBy=timers.target
EOF
warn "每日自动重启: 已启用(04:00 Asia/Shanghai"
warn "该操作会切断所有活动连接,如无必要建议设 XRAY_DAILY_RESTART=false"
}
# ===== 7. 启动服务 =====
start_service() {
systemctl daemon-reload
systemctl restart xray
systemctl enable xray
if [ "$DAILY_RESTART" -eq 1 ]; then
systemctl enable --now xray-restart.timer
fi
sleep 2
if systemctl is-active --quiet xray; then
log "Xray 服务已启动 ✅"
else
error "Xray 启动失败,请检查: journalctl -u xray -f"
exit 1
fi
}
# ===== 8. 保存配置到 .env =====
save_env() {
cat > "$SCRIPT_DIR/.env" << EOF
# Xray 部署配置(由 deploy.sh 自动生成)
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')
XRAY_MODE=${MODE}
XRAY_PORT=${XRAY_PORT}
XRAY_UUID=${XRAY_UUID}
# 分享链接中使用的 IP。留空则自动探测出口 IP;
# NAT / 端口转发的机器请填写服务商给你的实际入站 IP
SERVER_IP=${SERVER_IP:-}
# 每日 04:00 自动重启 Xray。默认 false。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而每日硬重启会
# 切断全部活动连接。仅在确实观察到长期运行后性能退化时才改为 true。
XRAY_DAILY_RESTART=$([ "$DAILY_RESTART" -eq 1 ] && echo true || echo false)
EOF
if [ "$MODE" = "reality" ]; then
cat >> "$SCRIPT_DIR/.env" << EOF
XRAY_PRIVATE_KEY=${XRAY_PRIVATE_KEY}
XRAY_PUBLIC_KEY=${XRAY_PUBLIC_KEY}
XRAY_SHORT_ID=${XRAY_SHORT_ID}
REALITY_DEST=${REALITY_DEST}
REALITY_SNI=${REALITY_SNI}
EOF
fi
chmod 600 "$SCRIPT_DIR/.env"
log "配置已保存到 .env(权限 600"
}
# ===== 9. 输出连接信息 =====
print_result() {
local server_ip
[ -z "$SERVER_IP_EFFECTIVE" ] && resolve_server_ip
if [ -n "${SERVER_IP:-}" ]; then
server_ip="$SERVER_IP"
log "使用 .env 中指定的 SERVER_IP: ${server_ip}"
else
server_ip="${SERVER_IP_EFFECTIVE:-获取失败}"
# 注意:这里拿到的是「出口」IP。NAT / 端口转发的机器上,
# 它与外部实际可连接的「入站」IP 可能不是同一个
local nic_ip
nic_ip=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
if [ -n "$nic_ip" ] && [ "$nic_ip" != "$server_ip" ]; then
warn "网卡地址 ${nic_ip} 与出口 IP ${server_ip} 不一致,本机可能位于 NAT / 端口转发之后"
warn "若客户端连不上,请向服务商确认实际映射的入站 IP 与端口,"
warn "然后在 .env 中设置 SERVER_IP= 和 XRAY_PORT= 后重新运行本脚本"
fi
fi
local title
if [ "${SHOW_ONLY:-0}" -eq 1 ]; then
title="连接信息"
else
title="部署完成 ✅"
fi
echo ""
echo "=========================================================="
if [ "$MODE" = "reality" ]; then
echo " Xray VLESS-Reality ${title}"
echo "=========================================================="
echo ""
echo " IP : ${server_ip}"
echo " 端口 : ${XRAY_PORT}"
echo " 协议 : VLESS"
echo " UUID : ${XRAY_UUID}"
echo " 流控 : xtls-rprx-vision"
echo " 传输 : tcp"
echo " 安全 : reality"
echo " SNI : ${REALITY_SNI}"
echo " Fingerprint : chrome"
echo " PublicKey : ${XRAY_PUBLIC_KEY}"
echo " ShortId : ${XRAY_SHORT_ID}"
echo ""
echo "=========================================================="
echo ""
echo ">>> VLESS 分享链接(可直接导入客户端):"
echo ""
echo "vless://${XRAY_UUID}@${server_ip}:${XRAY_PORT}?encryption=none&flow=xtls-rprx-vision&security=reality&sni=${REALITY_SNI}&fp=chrome&pbk=${XRAY_PUBLIC_KEY}&sid=${XRAY_SHORT_ID}&type=tcp#vps-reality"
else
echo " Xray VLESS TCP ${title}"
echo "=========================================================="
echo ""
echo " IP : ${server_ip}"
echo " 端口 : ${XRAY_PORT}"
echo " 协议 : VLESS"
echo " UUID : ${XRAY_UUID}"
echo " 传输 : tcp"
echo " 安全 : none"
echo ""
echo "=========================================================="
echo ""
echo ">>> VLESS 分享链接(可直接导入客户端):"
echo ""
echo "vless://${XRAY_UUID}@${server_ip}:${XRAY_PORT}?encryption=none&security=none&type=tcp&headerType=none#vps-fast"
fi
echo ""
echo "=========================================================="
echo ""
echo "⚠️ 请妥善保存以上信息!"
echo " 配置文件: /usr/local/etc/xray/config.json"
echo " 凭据备份: ${SCRIPT_DIR}/.env"
echo " 再次查看: bash deploy.sh --show"
echo " 查看日志: journalctl -u xray -f"
echo " 重启服务: systemctl restart xray"
echo "=========================================================="
}
# ===== --show:只读打印连接信息,不改动任何服务 =====
show_info() {
if [ ! -f "$SCRIPT_DIR/.env" ]; then
error "未找到 ${SCRIPT_DIR}/.env,无法读取部署信息"
echo ""
echo "若 Xray 配置仍在,可从 /usr/local/etc/xray/config.json 手工恢复:"
echo " priv=\$(grep -oP '\"privateKey\"\\s*:\\s*\"\\K[^\"]+' /usr/local/etc/xray/config.json)"
echo " /usr/local/bin/xray x25519 -i \"\$priv\" # 输出的 Password (PublicKey) 即客户端 pbk"
exit 1
fi
local missing=()
[ -z "${XRAY_UUID:-}" ] && missing+=("XRAY_UUID")
if [ "$MODE" = "reality" ]; then
[ -z "${XRAY_PUBLIC_KEY:-}" ] && missing+=("XRAY_PUBLIC_KEY")
[ -z "${XRAY_SHORT_ID:-}" ] && missing+=("XRAY_SHORT_ID")
[ -z "${REALITY_SNI:-}" ] && missing+=("REALITY_SNI")
fi
if [ "${#missing[@]}" -gt 0 ]; then
error ".env 缺少必要字段: ${missing[*]}"
error "请重新运行 bash deploy.sh 完成部署"
exit 1
fi
if ! systemctl is-active --quiet xray 2>/dev/null; then
warn "Xray 服务当前未运行,以下信息来自 .env,不代表服务可用"
warn "检查: systemctl status xray"
fi
print_result
}
# ===== 主流程 =====
main() {
if [ "$SHOW_ONLY" -eq 1 ]; then
show_info
exit 0
fi
echo ""
log "部署模式: ${MODE}"
[ "$MODE" = "reality" ] && log "VLESS-Reality(推荐,抗封锁)" || log "VLESS TCP(极速,无 TLS"
echo ""
ensure_deps
install_xray
generate_keys
select_reality_dest
write_config
configure_firewall
tune_network
harden_system
start_service
save_env
print_result
}
main