diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28099d5 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/vps-xray/.env.example b/vps-xray/.env.example index 90dc38a..64546fb 100644 --- a/vps-xray/.env.example +++ b/vps-xray/.env.example @@ -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 diff --git a/vps-xray/README.md b/vps-xray/README.md index 935c899..8006b71 100644 --- a/vps-xray/README.md +++ b/vps-xray/README.md @@ -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 ` 从私钥反推。 ### 第三步:客户端配置 @@ -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 ``` ### 速度慢 diff --git a/vps-xray/backup.sh b/vps-xray/backup.sh new file mode 100644 index 0000000..76638ca --- /dev/null +++ b/vps-xray/backup.sh @@ -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 diff --git a/vps-xray/client-config/gen-clash.sh b/vps-xray/client-config/gen-clash.sh new file mode 100644 index 0000000..d11998a --- /dev/null +++ b/vps-xray/client-config/gen-clash.sh @@ -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 排除,请勿提交或外传" diff --git a/vps-xray/client-config/lisa-vps-xray-optimized-info-4.md b/vps-xray/client-config/lisa-vps-xray-optimized-info-4.md index cbaded1..f237eb1 100644 --- a/vps-xray/client-config/lisa-vps-xray-optimized-info-4.md +++ b/vps-xray/client-config/lisa-vps-xray-optimized-info-4.md @@ -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 ========================================================== \ No newline at end of file diff --git a/vps-xray/client-config/vpn-vps-xray-optimized.yaml b/vps-xray/client-config/vpn-vps-xray-optimized.yaml index 3690a03..526faa9 100644 --- a/vps-xray/client-config/vpn-vps-xray-optimized.yaml +++ b/vps-xray/client-config/vpn-vps-xray-optimized.yaml @@ -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 - # 其余走代理 + - GEOIP,CN,DIRECT + # 兜底。MATCH 是终结规则,其后任何规则都不可达 - MATCH,Proxy - - MATCH,Proxy \ No newline at end of file diff --git a/vps-xray/client-config/vps-xray-optimized-info-3.md b/vps-xray/client-config/vps-xray-optimized-info-3.md deleted file mode 100644 index cafbbc8..0000000 --- a/vps-xray/client-config/vps-xray-optimized-info-3.md +++ /dev/null @@ -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 -========================================================== \ No newline at end of file diff --git a/vps-xray/deploy.sh b/vps-xray/deploy.sh index 5071b7b..03fd97f 100644 --- a/vps-xray/deploy.sh +++ b/vps-xray/deploy.sh @@ -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 </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)" diff --git a/vps-xray/uninstall.sh b/vps-xray/uninstall.sh index e4b519f..10bdc2a 100644 --- a/vps-xray/uninstall.sh +++ b/vps-xray/uninstall.sh @@ -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 diff --git a/vps-xray/upload.sh b/vps-xray/upload.sh index fac7dfe..f38c8b9 100644 --- a/vps-xray/upload.sh +++ b/vps-xray/upload.sh @@ -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}/"