vps部署优化

This commit is contained in:
2026-08-05 17:12:18 +08:00
parent 2afc2cade6
commit 5195986fc4
11 changed files with 989 additions and 96 deletions
+20
View File
@@ -0,0 +1,20 @@
# 环境变量 / 凭据(.env.example 模板除外)
.env
*.env
!.env.example
!*.env.example
# vps-xray:部署脚本生成的真实节点配置,含 UUID / 私钥 / 服务器 IP
# vps-xray/client-config/*-info*.md
# vps-xray/client-config/*.yaml
# vps-xray/client-config/*.yml
# 备份产物
*.bak
*.bak.*
backups/
# 系统 / 编辑器
.DS_Store
Thumbs.db
*.swp
+11
View File
@@ -6,11 +6,22 @@
# XRAY_MODE=reality
# 监听端口(默认 443,建议不改)
# NAT / 端口转发的机器请填服务商实际映射给你的端口
# XRAY_PORT=443
# 分享链接中使用的 IP,留空则自动探测出口 IP
# NAT / 端口转发的机器上出口 IP 与入站 IP 可能不同,此时必须手动指定
# SERVER_IP=1.2.3.4
# Reality 伪装目标(留空则自动选择延迟最低的)
# REALITY_DEST=www.microsoft.com
# REALITY_SNI=www.microsoft.com
# 每日 04:00 自动重启 Xray(默认 false
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级响应),
# 而每日硬重启会切断全部活动连接(跨夜下载、备份、CI 都会中断)。
# 仅在确实观察到长期运行后性能退化时才改为 true。
# XRAY_DAILY_RESTART=false
# 备份目录
# BACKUP_DIR=/var/backups/xray
+68 -4
View File
@@ -53,14 +53,19 @@
vps-xray/
├── deploy.sh # 一键部署脚本
├── uninstall.sh # 完全卸载脚本
├── backup.sh # 备份脚本
├── upload.sh # 上传脚本(仅上传服务端必要文件)
├── .env.example # 配置模板
├── README.md # 本文件
├── vps-xray-optimized.md # Reality 方案详细文档
├── vps-xray-fast.md # Fast TCP 方案详细文档
└── client-config/ # 客户端配置示例(本地使用,不上传服务器)
└── client-config/ # 客户端配置(本地使用,不上传服务器)
├── gen-clash.sh # 由节点信息生成 Clash.Meta 配置
└── client-macos.md # macOS 客户端指南(模板,可提交)
```
> `client-config/` 下含真实 UUID / 服务器 IP 的节点配置(`*-info*.md`、`*.yaml`)已在根 `.gitignore` 中排除,不要提交到版本库。
服务器上的文件位置:
```
@@ -98,7 +103,15 @@ bash deploy.sh --mode fast
- 连接参数(IP、端口、UUID、密钥等)
- VLESS 分享链接(可直接导入客户端)
> **⚠️ 请妥善保存输出的连接信息!密钥仅显示一次。**凭据同时保存在 `/opt/vps-xray/.env` 中。
凭据保存在 `/opt/vps-xray/.env`(权限 600)。**信息不会丢** —— 随时可以重新打印:
```bash
cd /opt/vps-xray && bash deploy.sh --show
```
`--show` 只读取 `.env` 输出连接信息和分享链接,不安装、不改配置、不重启服务。
> 即使 `.env` 被误删,只要 `/usr/local/etc/xray/config.json` 还在,UUID、shortId、SNI 都能从中读出,公钥可用 `xray x25519 -i <privateKey>` 从私钥反推。
### 第三步:客户端配置
@@ -111,16 +124,51 @@ bash deploy.sh --mode fast
| iOS | Shadowrocket / Streisand |
| Android | v2rayNG |
#### 生成 Clash 配置
把每台 VPS 的 `bash deploy.sh --show` 输出保存成 `client-config/xxx-info.md`,然后:
```bash
cd client-config
bash gen-clash.sh # 交互式勾选要写入的节点
bash gen-clash.sh --all # 全部节点
```
脚本解析文件里的 `vless://` 链接,生成 `vpn-vps-xray-optimized.yaml`(多节点自动组成 url-test 自动选择组,原文件会先备份为 `.bak`)。
> 生成的配置需要 **Clash.Meta / mihomo** 内核(Clash Verge Rev、ClashX Meta、FlClash 等)。原版 Clash / Clash Premium 不支持 VLESS + Reality。
详细的 Clash Meta / Sing-Box 配置参见 [vps-xray-optimized.md](vps-xray-optimized.md)。
## 日常运维
### 查看连接信息 / 分享链接
```bash
cd /opt/vps-xray
bash deploy.sh --show
```
只读操作,不会改动任何服务。
### 更换 Reality 伪装目标
伪装目标在首次部署时按握手延迟自动挑选,之后固定写入 `.env` 不再变动。后续每次部署会检查它是否仍然可达,不可达时告警但**不会自动更换**——因为更换会改变 SNI,导致所有客户端链接静默失效。
确认要换时显式执行:
```bash
bash deploy.sh --redetect
```
> ⚠️ 换完后 SNI 变了,**所有客户端都必须用新的分享链接重新配置**。脚本会在更换时打印醒目提示。
### 查看状态 / 日志
```bash
systemctl status xray
journalctl -u xray -f
journalctl -u xray --tail 100
journalctl -u xray -n 100
```
### 备份
@@ -167,6 +215,22 @@ bash <(curl -fsSL https://raw.githubusercontent.com/XTLS/Xray-install/main/insta
systemctl restart xray
```
### 每日自动重启(默认关闭)
早期版本会安装一个每天 04:00 重启 Xray 的 systemd timer,现已**默认关闭**
- 崩溃恢复已由 `Restart=always` + `RestartSec=3` 覆盖,响应是秒级,不必等到凌晨
- Xray 无已知需要定期重启的缺陷(内存、句柄上限均已配置)
- 而每日硬重启会**切断全部活动连接**,跨夜下载、备份、CI 都会中断
即"为假想问题付出真实代价"。部署时会自动移除旧版本遗留的 timer。
确有需要(例如实测到长期运行后性能退化)时在 `.env` 中开启:
```bash
XRAY_DAILY_RESTART=true
```
### 重启 / 停止
```bash
@@ -239,7 +303,7 @@ systemctl status xray
ss -tlnp | grep 443
# 查看错误日志
journalctl -u xray --tail 50
journalctl -u xray -n 50
```
### 速度慢
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
# ============================================
# Xray 备份脚本
# 备份 Xray 配置 / 部署配置 / 网络调优参数
# 输出目录默认 /var/backups/xray,自动清理 30 天前的旧备份
# ============================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
if [ "$(id -u)" -ne 0 ]; then
error "请使用 root 用户运行: sudo bash backup.sh"
exit 1
fi
if [ -f .env ]; then
sed -i 's/\r$//' .env
set -a; source .env; set +a
fi
BACKUP_DIR="${BACKUP_DIR:-/var/backups/xray}"
KEEP_DAYS="${BACKUP_KEEP_DAYS:-30}"
STAMP="$(date '+%Y%m%d%H%M%S')"
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
backed_up=0
# ===== 1. Xray 配置 =====
# 打包 /usr/local/etc/xray 整个目录,恢复时 tar xzf ... -C /usr/local/etc/
if [ -d /usr/local/etc/xray ]; then
tar czf "${BACKUP_DIR}/xray_config_${STAMP}.tar.gz" -C /usr/local/etc xray
log "Xray 配置已备份: ${BACKUP_DIR}/xray_config_${STAMP}.tar.gz"
backed_up=$((backed_up + 1))
else
warn "/usr/local/etc/xray 不存在,跳过"
fi
# ===== 2. 部署配置(.env + 脚本)=====
# .env 含私钥,打包后权限收紧到 600
deploy_files=()
for f in .env deploy.sh uninstall.sh backup.sh upload.sh .env.example; do
[ -f "$f" ] && deploy_files+=("$f")
done
if [ "${#deploy_files[@]}" -gt 0 ]; then
tar czf "${BACKUP_DIR}/deploy_${STAMP}.tar.gz" -C "$SCRIPT_DIR" "${deploy_files[@]}"
chmod 600 "${BACKUP_DIR}/deploy_${STAMP}.tar.gz"
log "部署配置已备份: ${BACKUP_DIR}/deploy_${STAMP}.tar.gz(权限 600,含私钥)"
backed_up=$((backed_up + 1))
else
warn "未找到可备份的部署文件,跳过"
fi
# ===== 3. 网络调优参数 =====
if [ -f /etc/sysctl.d/99-xray-turbo.conf ]; then
cp /etc/sysctl.d/99-xray-turbo.conf "${BACKUP_DIR}/sysctl_${STAMP}.conf"
log "网络调优参数已备份: ${BACKUP_DIR}/sysctl_${STAMP}.conf"
backed_up=$((backed_up + 1))
else
warn "/etc/sysctl.d/99-xray-turbo.conf 不存在,跳过"
fi
if [ "$backed_up" -eq 0 ]; then
error "没有任何内容被备份,请确认 Xray 是否已部署"
exit 1
fi
# ===== 4. 清理过期备份 =====
old_count=$(find "$BACKUP_DIR" -maxdepth 1 -type f \
\( -name 'xray_config_*.tar.gz' -o -name 'deploy_*.tar.gz' -o -name 'sysctl_*.conf' \) \
-mtime "+${KEEP_DAYS}" | wc -l)
if [ "$old_count" -gt 0 ]; then
find "$BACKUP_DIR" -maxdepth 1 -type f \
\( -name 'xray_config_*.tar.gz' -o -name 'deploy_*.tar.gz' -o -name 'sysctl_*.conf' \) \
-mtime "+${KEEP_DAYS}" -delete
log "已清理 ${old_count} 个超过 ${KEEP_DAYS} 天的旧备份"
fi
echo ""
log "备份完成,目录: ${BACKUP_DIR}"
ls -lh "$BACKUP_DIR" | tail -n +2 | tail -10
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env bash
set -euo pipefail
# ============================================
# Clash.Meta 配置生成器
#
# 从 deploy.sh 输出的节点信息文件(*info*.md,内含 vless:// 链接)
# 生成 Clash.Meta / mihomo 可用的 YAML 配置。
#
# 用法:
# bash gen-clash.sh # 交互式选择节点文件
# bash gen-clash.sh a.md b.md # 直接指定
# bash gen-clash.sh -o my.yaml a.md # 指定输出文件
# bash gen-clash.sh --all # 使用全部 *info*.md
#
# 本脚本仅在本地运行,不会上传到 VPS。
# ============================================
# 不切换工作目录:候选文件在脚本所在目录里找,
# 但命令行显式传入的路径仍相对调用者的当前目录解析
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; 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; }
OUTPUT="${SCRIPT_DIR}/vpn-vps-xray-optimized.yaml"
USE_ALL=0
FILES=()
usage() {
sed -n '4,17p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) OUTPUT="${2:?-o 需要一个文件名}"; shift 2 ;;
--all) USE_ALL=1; shift ;;
-h|--help) usage ;;
-*) error "未知参数: $1"; usage ;;
*) FILES+=("$1"); shift ;;
esac
done
# ===== 收集候选文件 =====
mapfile -t CANDIDATES < <(ls -1 "${SCRIPT_DIR}"/*info*.md 2>/dev/null || true)
if [ "${#FILES[@]}" -eq 0 ]; then
if [ "${#CANDIDATES[@]}" -eq 0 ]; then
error "当前目录下找不到任何 *info*.md 节点信息文件"
error "请先在 VPS 上执行 bash deploy.sh --show,把输出保存为 xxx-info.md 放到这里"
exit 1
fi
if [ "$USE_ALL" -eq 1 ]; then
FILES=("${CANDIDATES[@]}")
else
echo ""
echo -e "${CYAN}可用的节点信息文件:${NC}"
for i in "${!CANDIDATES[@]}"; do
f="${CANDIDATES[$i]}"
# 顺带把链接里的备注名显示出来,方便辨认
tag=$(grep -m1 -o 'vless://[^[:space:]]*' "$f" 2>/dev/null | sed 's/.*#//' || true)
printf " %2d) %-45s %s\n" "$((i + 1))" "${f#"${SCRIPT_DIR}"/}" "${tag:+[${tag}]}"
done
echo ""
read -r -p "选择要写入配置的节点(空格分隔序号,回车=全选): " picks
if [ -z "${picks// /}" ]; then
FILES=("${CANDIDATES[@]}")
else
for n in $picks; do
if ! [[ "$n" =~ ^[0-9]+$ ]] || [ "$n" -lt 1 ] || [ "$n" -gt "${#CANDIDATES[@]}" ]; then
error "无效序号: $n"
exit 1
fi
FILES+=("${CANDIDATES[$((n - 1))]}")
done
fi
fi
fi
# ===== 工具函数 =====
urldecode() {
local s="${1//+/ }"
printf '%b' "${s//%/\\x}"
}
# 从 query string 中取出指定参数
qs_get() {
local query="$1" key="$2" kv
local IFS='&'
for kv in $query; do
if [ "${kv%%=*}" = "$key" ]; then
urldecode "${kv#*=}"
return
fi
done
}
# ===== 解析所有节点 =====
NAMES=(); SERVERS=(); PORTS=(); UUIDS=(); SNIS=(); PBKS=(); SIDS=(); FPS=(); FLOWS=(); SECS=()
declare -A name_seen=()
for f in "${FILES[@]}"; do
[ -f "$f" ] || { error "文件不存在: $f"; exit 1; }
link=$(grep -m1 -o 'vless://[^[:space:]]*' "$f" 2>/dev/null || true)
if [ -z "$link" ]; then
warn "跳过 ${f#"${SCRIPT_DIR}"/}:未找到 vless:// 链接"
continue
fi
rest="${link#vless://}"
# 备注名(# 之后)
if [[ "$rest" == *"#"* ]]; then
name=$(urldecode "${rest##*#}")
rest="${rest%%#*}"
else
name=""
fi
uuid="${rest%%@*}"
hpq="${rest#*@}"
hostport="${hpq%%\?*}"
query=""
[[ "$hpq" == *"?"* ]] && query="${hpq#*\?}"
# 兼容 IPv6 字面量 [::1]:443
if [[ "$hostport" == \[* ]]; then
server="${hostport%%]*}]"
port="${hostport##*]:}"
else
server="${hostport%%:*}"
port="${hostport##*:}"
fi
sec=$(qs_get "$query" security); sec="${sec:-none}"
sni=$(qs_get "$query" sni)
pbk=$(qs_get "$query" pbk)
sid=$(qs_get "$query" sid)
fp=$(qs_get "$query" fp); fp="${fp:-chrome}"
flow=$(qs_get "$query" flow)
net=$(qs_get "$query" type); net="${net:-tcp}"
if [ -z "$uuid" ] || [ -z "$server" ] || [ -z "$port" ]; then
error "${f#"${SCRIPT_DIR}"/} 的链接缺少 uuid/server/port,无法解析"
exit 1
fi
if [ "$sec" = "reality" ] && { [ -z "$pbk" ] || [ -z "$sni" ]; }; then
error "${f#"${SCRIPT_DIR}"/} 是 reality 节点但缺少 pbk 或 sni"
exit 1
fi
if [ "$net" != "tcp" ]; then
warn "${f#"${SCRIPT_DIR}"/}: transport=${net},本生成器目前只支持 tcp,已跳过"
continue
fi
[ -z "$name" ] && name="${server}"
# 名称去重,Clash 要求 proxy name 唯一
base="$name"; n=2
while [ -n "${name_seen[$name]:-}" ]; do
name="${base}-${n}"; n=$((n + 1))
done
name_seen["$name"]=1
NAMES+=("$name"); SERVERS+=("$server"); PORTS+=("$port"); UUIDS+=("$uuid")
SNIS+=("$sni"); PBKS+=("$pbk"); SIDS+=("$sid")
FPS+=("$fp"); FLOWS+=("$flow"); SECS+=("$sec")
log "解析成功: ${name} (${server}:${port}, ${sec})"
done
if [ "${#NAMES[@]}" -eq 0 ]; then
error "没有解析出任何可用节点"
exit 1
fi
# ===== 备份既有输出 =====
if [ -f "$OUTPUT" ]; then
cp "$OUTPUT" "${OUTPUT}.bak"
log "已备份原配置: ${OUTPUT}.bak"
fi
# ===== 生成 YAML =====
{
cat <<'HEADER'
# ============================================================
# 由 gen-clash.sh 自动生成,请勿手工编辑(重新生成会覆盖)
#
# ⚠ 需要 Clash.Meta / mihomo 内核(Clash Verge Rev、ClashX Meta、
# FlClash 等)。原版 Clash / Clash Premium 不支持 VLESS + Reality。
#
# ⚠ 本文件含真实 UUID 与服务器地址,等同凭据,切勿提交到版本库或外传。
# ============================================================
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
# fake-ip 下开启嗅探,才能从流量中还原域名,让域名类规则对直连 IP 也生效
sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
skip-domain:
- "+.push.apple.com"
dns:
enable: true
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
# 这些域名必须拿到真实 IP,否则局域网发现、时间同步、
# 网络连通性检测等会出问题
fake-ip-filter:
- "*.lan"
- "*.local"
- "*.localdomain"
- "*.home.arpa"
- "+.msftconnecttest.com"
- "+.msftncsi.com"
- "time.*.com"
- "ntp.*.com"
- "+.pool.ntp.org"
- "localhost.ptlogin2.qq.com"
# 用于解析下面 fallback 里 DoH 服务器自身的域名
default-nameserver:
- 223.5.5.5
- 119.29.29.29
nameserver:
- 223.5.5.5
- 119.29.29.29
# fallback 的意义是绕过投毒,必须用加密 DNS——
# 明文 UDP 的 8.8.8.8 在国内同样会被污染,等于没有 fallback
fallback:
- https://1.1.1.1/dns-query
- https://dns.google/dns-query
fallback-filter:
geoip: true
geoip-code: CN
ipcidr:
- 240.0.0.0/4
- 0.0.0.0/32
proxies:
HEADER
for i in "${!NAMES[@]}"; do
echo " - name: \"${NAMES[$i]}\""
echo " type: vless"
echo " server: ${SERVERS[$i]}"
echo " port: ${PORTS[$i]}"
echo " uuid: ${UUIDS[$i]}"
echo " network: tcp"
echo " udp: true"
if [ "${SECS[$i]}" = "reality" ]; then
echo " tls: true"
[ -n "${FLOWS[$i]}" ] && echo " flow: ${FLOWS[$i]}"
echo " servername: ${SNIS[$i]}"
echo " client-fingerprint: ${FPS[$i]}"
echo " reality-opts:"
echo " public-key: ${PBKS[$i]}"
[ -n "${SIDS[$i]}" ] && echo " short-id: ${SIDS[$i]}"
else
echo " tls: false"
fi
echo ""
done
cat <<'GROUPS_HEAD'
proxy-groups:
- name: "Proxy"
type: select
proxies:
- Auto-Select
GROUPS_HEAD
for n in "${NAMES[@]}"; do echo " - \"${n}\""; done
echo " - DIRECT"
echo ""
cat <<'AUTO_HEAD'
- name: "Auto-Select"
type: url-test
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
proxies:
AUTO_HEAD
for n in "${NAMES[@]}"; do echo " - \"${n}\""; done
echo ""
echo "rules:"
echo " # 代理服务器自身必须直连。否则 TUN / 系统代理会把「连接代理服务器」"
echo " # 这件事本身也丢进代理,形成回环,表现为 REALITY 握手拿到真证书。"
declare -A srv_seen=()
for s in "${SERVERS[@]}"; do
[ -n "${srv_seen[$s]:-}" ] && continue
srv_seen["$s"]=1
if [[ "$s" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo " - IP-CIDR,${s}/32,DIRECT,no-resolve"
elif [[ "$s" == \[*\] ]]; then
echo " - IP-CIDR6,${s//[\[\]]/}/128,DIRECT,no-resolve"
else
echo " - DOMAIN,${s},DIRECT"
fi
done
cat <<'RULES'
# 本机与局域网直连(fake-ip 下不加这条,路由器/NAS 会被丢进代理)
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,169.254.0.0/16,DIRECT,no-resolve
# 域名规则放在 IP 规则之前:GEOSITE 直接按域名命中,
# 不必先做一次真实 DNS 解析(fake-ip 下 GEOIP 前置会触发额外解析)
- GEOSITE,cn,DIRECT
- GEOIP,CN,DIRECT
# 兜底。MATCH 是终结规则,其后任何规则都不可达
- MATCH,Proxy
RULES
} > "$OUTPUT"
echo ""
log "已生成: ${OUTPUT}${#NAMES[@]} 个节点)"
for n in "${NAMES[@]}"; do echo " - ${n}"; done
echo ""
warn "该文件含真实凭据,已被根 .gitignore 排除,请勿提交或外传"
@@ -5,26 +5,27 @@
IP : 64.81.25.247
端口 : 443
协议 : VLESS
UUID : 5455974f-a22a-4039-82b4-5dc0a30f06ad
UUID : b2f1198d-9475-40af-a2c6-2738a69b4e06
流控 : xtls-rprx-vision
传输 : tcp
安全 : reality
SNI : dl.google.com
SNI : www.apple.com
Fingerprint : chrome
PublicKey : cu-tvQ0PmpJDF5WtkgXgMmyhas2ZYdW5rpUiaAuekmo
ShortId : 88992d098ccc5622
PublicKey : fKRVRMVlmmedi2rFk-cetAT397V-IzRifStKQcMTilM
ShortId : fca381b1ac6f4d02
==========================================================
>>> VLESS 分享链接(可直接导入客户端):
vless://5455974f-a22a-4039-82b4-5dc0a30f06ad@64.81.25.247:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=dl.google.com&fp=chrome&pbk=cu-tvQ0PmpJDF5WtkgXgMmyhas2ZYdW5rpUiaAuekmo&sid=88992d098ccc5622&type=tcp#vps-reality-lisa-1
vless://b2f1198d-9475-40af-a2c6-2738a69b4e06@64.81.25.247:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.apple.com&fp=chrome&pbk=fKRVRMVlmmedi2rFk-cetAT397V-IzRifStKQcMTilM&sid=fca381b1ac6f4d02&type=tcp#vps-reality-lisa-1
==========================================================
⚠️ 请妥善保存以上信息!
配置文件: /usr/local/etc/xray/config.json
凭据备份: /opt/vps-xray/.env
再次查看: bash deploy.sh --show
查看日志: journalctl -u xray -f
重启服务: systemctl restart xray
==========================================================
@@ -1,21 +1,67 @@
# ============================================================
# 由 gen-clash.sh 自动生成,请勿手工编辑(重新生成会覆盖)
#
# ⚠ 需要 Clash.Meta / mihomo 内核(Clash Verge Rev、ClashX Meta、
# FlClash 等)。原版 Clash / Clash Premium 不支持 VLESS + Reality。
#
# ⚠ 本文件含真实 UUID 与服务器地址,等同凭据,切勿提交到版本库或外传。
# ============================================================
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
# fake-ip 下开启嗅探,才能从流量中还原域名,让域名类规则对直连 IP 也生效
sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
skip-domain:
- "+.push.apple.com"
dns:
enable: true
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
# 这些域名必须拿到真实 IP,否则局域网发现、时间同步、
# 网络连通性检测等会出问题
fake-ip-filter:
- "*.lan"
- "*.local"
- "*.localdomain"
- "*.home.arpa"
- "+.msftconnecttest.com"
- "+.msftncsi.com"
- "time.*.com"
- "ntp.*.com"
- "+.pool.ntp.org"
- "localhost.ptlogin2.qq.com"
# 用于解析下面 fallback 里 DoH 服务器自身的域名
default-nameserver:
- 223.5.5.5
- 119.29.29.29
nameserver:
- 223.5.5.5
- 119.29.29.29
# fallback 的意义是绕过投毒,必须用加密 DNS——
# 明文 UDP 的 8.8.8.8 在国内同样会被污染,等于没有 fallback
fallback:
- 8.8.8.8
- 1.1.1.1
- https://1.1.1.1/dns-query
- https://dns.google/dns-query
fallback-filter:
geoip: true
geoip-code: CN
ipcidr:
- 240.0.0.0/4
- 0.0.0.0/32
proxies:
- name: "vps-reality-1"
@@ -24,8 +70,8 @@ proxies:
port: 443
uuid: 5b05151b-a681-46db-a223-95dd45b13589
network: tcp
tls: true
udp: true
tls: true
flow: xtls-rprx-vision
servername: www.apple.com
client-fingerprint: chrome
@@ -39,8 +85,8 @@ proxies:
port: 443
uuid: cbf3836d-517d-4dfb-86d4-a636bca27713
network: tcp
tls: true
udp: true
tls: true
flow: xtls-rprx-vision
servername: www.apple.com
client-fingerprint: chrome
@@ -53,22 +99,33 @@ proxy-groups:
type: select
proxies:
- Auto-Select
- vps-reality-1
- vps-reality-2
- "vps-reality-1"
- "vps-reality-2"
- DIRECT
- name: "Auto-Select"
type: url-test
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
proxies:
- vps-reality-1
- vps-reality-1
- "vps-reality-1"
- "vps-reality-2"
rules:
# 国内直连
- GEOIP,CN,DIRECT
# 代理服务器自身必须直连。否则 TUN / 系统代理会把「连接代理服务器」
# 这件事本身也丢进代理,形成回环,表现为 REALITY 握手拿到真证书。
- IP-CIDR,45.32.69.133/32,DIRECT,no-resolve
- IP-CIDR,45.32.71.199/32,DIRECT,no-resolve
# 本机与局域网直连(fake-ip 下不加这条,路由器/NAS 会被丢进代理)
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,169.254.0.0/16,DIRECT,no-resolve
# 域名规则放在 IP 规则之前:GEOSITE 直接按域名命中,
# 不必先做一次真实 DNS 解析(fake-ip 下 GEOIP 前置会触发额外解析)
- GEOSITE,cn,DIRECT
# 其余走代理
- MATCH,Proxy
- GEOIP,CN,DIRECT
# 兜底。MATCH 是终结规则,其后任何规则都不可达
- MATCH,Proxy
@@ -1,30 +0,0 @@
==========================================================
Xray VLESS-Reality 部署完成 ✅
==========================================================
IP : 140.82.23.27
端口 : 443
协议 : VLESS
UUID : 07b3bbf7-2a86-41dd-9c61-4e84767e68de
流控 : xtls-rprx-vision
传输 : tcp
安全 : reality
SNI : dl.google.com
Fingerprint : chrome
PublicKey : xLsb2ZHk5C1r7kGUTyUwgx5nzDd1MrRUttdo15fbplM
ShortId : ac5038148e675b48
==========================================================
>>> VLESS 分享链接(可直接导入客户端):
vless://07b3bbf7-2a86-41dd-9c61-4e84767e68de@140.82.23.27:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=dl.google.com&fp=chrome&pbk=xLsb2ZHk5C1r7kGUTyUwgx5nzDd1MrRUttdo15fbplM&sid=ac5038148e675b48&type=tcp#vps-reality-3
==========================================================
⚠️ 请妥善保存以上信息!
配置文件: /usr/local/etc/xray/config.json
凭据备份: /opt/vps-xray/.env
查看日志: journalctl -u xray -f
重启服务: systemctl restart xray
==========================================================
+345 -40
View File
@@ -28,26 +28,30 @@ usage() {
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="reality"
MODE=""
MODE_EXPLICIT=0
SHOW_ONLY=0
REDETECT=0
while [[ $# -gt 0 ]]; do
case $1 in
--mode) MODE="$2"; shift 2 ;;
--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
if [[ "$MODE" != "reality" && "$MODE" != "fast" ]]; then
error "无效模式: $MODE(支持 reality / fast"
exit 1
fi
# ===== 检查 root =====
# .env 权限为 600,读取它同样需要 root
if [ "$(id -u)" -ne 0 ]; then
error "请使用 root 用户运行: sudo bash deploy.sh"
exit 1
@@ -59,12 +63,30 @@ if [ -f .env ]; then
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 "0/5 检查依赖"
step "1/8 检查依赖"
local missing=()
local cmd
@@ -107,7 +129,7 @@ ensure_deps() {
# ===== 1. 安装 Xray =====
install_xray() {
step "1/5 安装 Xray"
step "2/8 安装 Xray"
if [ -x /usr/local/bin/xray ]; then
log "Xray 已安装: $(/usr/local/bin/xray version | head -1)"
@@ -147,8 +169,48 @@ install_xray() {
}
# ===== 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 "2/5 生成密钥"
step "3/8 生成密钥"
recover_credentials
XRAY_UUID="${XRAY_UUID:-$(cat /proc/sys/kernel/random/uuid)}"
log "UUID: ${XRAY_UUID}"
@@ -178,48 +240,158 @@ select_reality_dest() {
return
fi
step "3/5 选择 Reality 伪装目标"
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" ]; then
log "使用 .env 中定的伪装目标: ${REALITY_DEST}"
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
local best_dest="www.microsoft.com"
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 [ -n "$ms" ] && [ "$ms" -lt "$best_ms" ] 2>/dev/null; then
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)"
}
# ===== 4. 写入 Xray 配置 =====
write_config() {
if [ "$MODE" = "reality" ]; then
write_config_reality
else
write_config_fast
if [ -n "$old_dest" ] && [ "$old_dest" != "$best_dest" ]; then
echo ""
warn "════════════════════════════════════════════════════════"
warn "伪装目标已从 ${old_dest} 更换为 ${best_dest}"
warn "SNI 随之改变,所有客户端的旧链接将立即失效!"
warn "部署完成后请用新的分享链接重新配置全部客户端。"
warn "════════════════════════════════════════════════════════"
echo ""
fi
}
write_config_reality() {
step "3/5 写入 Xray 配置 (Reality)"
# ===== 4. 写入 Xray 配置 =====
XRAY_CONFIG="/usr/local/etc/xray/config.json"
cat > /usr/local/etc/xray/config.json << EOF
# 对外 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"
@@ -273,6 +445,12 @@ write_config_reality() {
],
"routing": {
"rules": [
${SELF_LOOP_RULE}
{
"type": "field",
"outboundTag": "block",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "block",
@@ -282,14 +460,13 @@ write_config_reality() {
}
}
EOF
log "配置已写入: /usr/local/etc/xray/config.json"
}
write_config_fast() {
step "3/5 写入 Xray 配置 (Fast TCP)"
step "5/8 写入 Xray 配置 (Fast TCP)"
local out="$1"
cat > /usr/local/etc/xray/config.json << EOF
cat > "$out" << EOF
{
"log": {
"loglevel": "warning"
@@ -330,6 +507,12 @@ write_config_fast() {
],
"routing": {
"rules": [
${SELF_LOOP_RULE}
{
"type": "field",
"outboundTag": "block",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "block",
@@ -339,23 +522,50 @@ write_config_fast() {
}
}
EOF
}
log "配置已写入: /usr/local/etc/xray/config.json"
# ===== 检测当前生效的 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 "4/5 配置防火墙"
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"
ufw allow 22/tcp
local p
for p in $ssh_ports; do
ufw allow "${p}/tcp"
done
ufw --force enable
log "ufw 已放行端口 ${XRAY_PORT}/tcp"
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"
log "firewalld 已放行端口 ${XRAY_PORT}/tcp 及 SSH 端口"
else
warn "未检测到防火墙工具,请手动放行端口 ${XRAY_PORT}/tcp"
fi
@@ -363,7 +573,7 @@ configure_firewall() {
# ===== 6. BBR + 网络调优 =====
tune_network() {
step "5/5 BBR + 网络调优"
step "7/8 BBR + 网络调优"
cat > /etc/sysctl.d/99-xray-turbo.conf << 'SYSEOF'
# BBR 拥塞控制
@@ -396,7 +606,7 @@ SYSEOF
# ===== 新增:系统与服务加固 =====
harden_system() {
step "加固系统与服务"
step "8/8 加固系统与服务"
# 文件描述符上限
if ! grep -q '99-xray-nofile' /etc/security/limits.conf 2>/dev/null; then
@@ -418,6 +628,22 @@ 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]
@@ -441,6 +667,8 @@ Persistent=true
[Install]
WantedBy=timers.target
EOF
warn "每日自动重启: 已启用(04:00 Asia/Shanghai"
warn "该操作会切断所有活动连接,如无必要建议设 XRAY_DAILY_RESTART=false"
}
# ===== 7. 启动服务 =====
@@ -448,7 +676,9 @@ start_service() {
systemctl daemon-reload
systemctl restart xray
systemctl enable xray
systemctl enable --now xray-restart.timer
if [ "$DAILY_RESTART" -eq 1 ]; then
systemctl enable --now xray-restart.timer
fi
sleep 2
if systemctl is-active --quiet xray; then
@@ -468,6 +698,15 @@ save_env() {
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
@@ -487,13 +726,35 @@ EOF
# ===== 9. 输出连接信息 =====
print_result() {
local server_ip
server_ip=$(curl -s --max-time 5 ipv4.ip.sb || curl -s --max-time 5 ifconfig.me || echo "获取失败")
[ -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 部署完成 ✅"
echo " Xray VLESS-Reality ${title}"
echo "=========================================================="
echo ""
echo " IP : ${server_ip}"
@@ -514,7 +775,7 @@ print_result() {
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 部署完成 ✅"
echo " Xray VLESS TCP ${title}"
echo "=========================================================="
echo ""
echo " IP : ${server_ip}"
@@ -523,6 +784,12 @@ print_result() {
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 ""
@@ -531,13 +798,51 @@ print_result() {
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"
+35 -5
View File
@@ -83,11 +83,33 @@ else
fi
systemctl disable xray 2>/dev/null || true
# 每日重启 timer(旧版部署可能残留)
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
log "已移除每日重启 timer"
fi
# ===== 2. 卸载 Xray =====
log "正在卸载 Xray..."
if [ -f /usr/local/bin/xray ]; then
# 使用官方卸载方式
bash <(curl -fsSL https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh) --remove 2>/dev/null || true
# 官方卸载:下载到临时文件再执行。
# 不能用 bash <(curl ...)——curl 缺失时进程替换只产生空脚本,
# 外层 bash 仍返回 0,卸载会被静默跳过
if command -v curl &>/dev/null; then
installer="$(mktemp)"
if curl -fsSL --max-time 60 \
https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh \
-o "$installer" && [ -s "$installer" ]; then
bash "$installer" remove --purge || warn "官方卸载脚本执行失败,改为手动清理"
else
warn "无法下载官方卸载脚本,改为手动清理"
fi
rm -f "$installer"
else
warn "未安装 curl,跳过官方卸载脚本,改为手动清理"
fi
# 手动清理残留
rm -f /usr/local/bin/xray
rm -rf /usr/local/etc/xray
@@ -128,9 +150,17 @@ fi
echo ""
read -r -p "是否删除部署目录 ${SCRIPT_DIR}(y/N): " del_deploy
if [[ "$del_deploy" =~ ^[Yy]$ ]]; then
cd /opt
rm -rf "$SCRIPT_DIR"
log "已删除部署目录"
# 删除前做路径断言,避免 SCRIPT_DIR 异常时误删根目录或系统目录
case "$SCRIPT_DIR" in
/|/root|/etc|/usr|/var|/home|/opt|"")
error "拒绝删除高危路径: '${SCRIPT_DIR}',请手动处理"
;;
*)
cd /
rm -rf "$SCRIPT_DIR"
log "已删除部署目录: ${SCRIPT_DIR}"
;;
esac
else
warn "保留部署目录: ${SCRIPT_DIR}"
fi
+1
View File
@@ -27,6 +27,7 @@ echo ">>> 上传部署文件"
scp -P "${SSH_PORT}" \
"${SCRIPT_DIR}/deploy.sh" \
"${SCRIPT_DIR}/uninstall.sh" \
"${SCRIPT_DIR}/backup.sh" \
"${SCRIPT_DIR}/.env.example" \
"root@${VPS_IP}:${REMOTE_DIR}/"