#!/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 </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