✧ 君と集まって星座になれたら ✦ ROKUTOSEI
16316 字
82 分钟
[弃用] 服务端:单端口五合一部署及完整安全加固
更新于 2026-09-11

已彻底转向 Xray-core,本文不再维护,请自行判断是否过时

TIP

观前提醒

本文包括基础系统安防指导,因此略显冗长

可直接点击目录查看需要的部分,或者查看精简版

本教程基于 Xray 服务端 + Caddy 反代 + mihomo 客户端 构建

本教程几乎全程手敲命令,不借助 3x-ui 此类面板,需要一定耐心

如果想一键部署,很遗憾,本文不适合你

XHTTP 五合一指的是在同一台 VPS 的 443 端口实现了五个出站节点:

点击查看 适用场景

(此处部分引用 @Benjamin1919 大佬的教程原文案)

出站 1:XTLS(Vision) + REALITY 直连

  • 客户端 GUI 不支持 XHTTP
  • 不想开启 TCP 多路复用
  • 想要尽可能低的延迟与尽可能高的上下行速度

出站 2:XHTTP + REALITY 直连

  • VPS 去程和回程的线路都比较好
  • 想要尽可能低的延迟与尽可能高的上下行速度

出站 3:上行 XHTTP + TLS(ECH) + CDN | 下行 XHTTP + REALITY(上下行不同 SNI)

  • 去程(客户端 → VPS)线路较差,回程(VPS → 客户端)线路较好
  • 日常使用更偏重下载,很少上传
  • 隐藏上行 SNI 防探测封锁

出站 4:XHTTP + TLS(ECH) 过 CDN

  • VPS 去程和回程的线路都很差
  • 不希望 VPS 的 IP 暴露在公网
  • VPS 的 IP 已被墙
  • 隐藏 SNI 防探测封锁

出站 5:上行 XHTTP + REALITY | 下行 XHTTP + TLS(ECH) + CDN(上下行不同 SNI)

  • 去程(客户端 → VPS)线路较好,回程(VPS → 客户端)线路较差
  • 日常使用更偏重上传,很少下载
  • 隐藏下行 SNI 防探测封锁

目录

1. 准备工作#

在开始之前,请准备好以下内容:

  1. 一个个人域名
  2. 一个通过支付验证的 Cloudflare 账号
  3. 一台拥有 root 完全访问权限的 VPS
NOTE

为避免纠纷,本文不提供任何 VPS 商家推荐或购买指导,请自行选择。

本方案属于 Xray 前置、REALITY 回落到本机 Caddy,不需要配置 limitFallbackUpload,

本文中的 <VPS_IP>、<YOUR_USER>、<YOUR_DOMAIN> 均为占位符,实际操作时请替换为你的真实 IP、用户名和域名。

SSH 端口以 <SSH_PORT> 表示(示例为 54321),请根据实际部署修改。


2. 系统基础防护#

2.1 系统基础优化#

2.1.1 清除厂家模板#

使用 root 账密进行 SSH 登录:

Terminal window
ssh root@<VPS_IP>

部分服务商的系统模板可能包含首次开机脚本,建议清除,没有则跳过:

Terminal window
sudo crontab -e

进入编辑界面后,寻找类似 @reboot /admin/firstbootkvm yes 的行,将其删除并保存。

2.1.2 开启 BBR 网络加速#

把系统自带的 BBR 拥塞控制开启,拉升机器的公网吞吐量:

Terminal window
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p

2.1.3 增大 Swap(可选)#

对于内存较少的 VPS,适当增大 Swap 可提供缓冲:

Terminal window
# 查看当前 Swap
sudo swapon --show
# 创建 Swap 文件(根据磁盘余量调整大小,如 2G)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 写入 fstab 使其重启后自动挂载
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

降低 swappiness 让系统优先使用物理内存:

Terminal window
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
swappiness行为
0不用 swap(如果 RAM 充足,可以完全不用)
10物理内存接近耗尽时才动用 swap(推荐)
20较保守的折中值
60系统默认,较早换出到 swap

数值越低,系统越倾向保留物理内存,避免磁盘 I/O;但同时 OOM 风险略增。常规场景设为 10 即可。

2.1.4 切换默认 Shell 为 Bash#

Debian 12 Template 默认将 /bin/sh 指向 dash,不是 bash。本文所有脚本均以 #!/bin/bash 声明,不受影响,但个别系统脚本和交互环境可能行为异常。

将默认 Shell 切换为 bash:

方法一(标准交互方式):

Terminal window
# 查看当前指向
ls -l /bin/sh
# 重新配置 dash,在弹出的对话框中选择 <No>
sudo dpkg-reconfigure dash
# 确认已切换
ls -l /bin/sh

如果执行后没有弹出任何交互窗口,请改用方法二:

Terminal window
# 设置 debconf 选项为"不使用 dash"
echo "dash dash/sh boolean false" | sudo debconf-set-selections
# 手动链接到 bash
sudo ln -sf bash /bin/sh
# 确认切换
ls -l /bin/sh

方法二同时更新了 debconf 数据库,因此后续 dash 包升级时不会自动覆盖该链接。


2.2 SSH 密钥登录#

2.2.1 生成密钥对#

在本地电脑(本文以 Windows 为例)打开 PowerShell 7,生成一把新密钥对:

PowerShell 7
cd ~
ssh-keygen -t ed25519 -C "my-vps" -f ~/.ssh/id_ed25519_vps

回车后,会提示你设置密码(passphrase),如果嫌麻烦可以直接连按两次回车跳过。

2.2.2 上传公钥至服务器#

在本地 PowerShell 中运行(将 <VPS_IP> 替换为真实 IP):

PowerShell 7
$pubKey = Get-Content "$HOME\.ssh\id_ed25519_vps.pub"
$pubKey | ssh root@<VPS_IP> "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

2.2.3 加固 SSH 配置#

回到 VPS SSH 窗口,编辑 SSH 配置文件:

Terminal window
nano /etc/ssh/sshd_config

修改或取消注释以下行:

Port <SSH_PORT> # 改为任意高位端口,如 54321
PasswordAuthentication no # 禁用密码登录
PubkeyAuthentication yes # 允许密钥登录

顺便查看是否存在 Include /etc/ssh/sshd_config.d/*.conf。如有,检查该目录下是否存在配置文件,同样需要禁用密码登录:

Terminal window
ls -R /etc/ssh/sshd_config.d/

如有 50-cloud-init.conf,编辑并修改 PasswordAuthentication yes → no

Terminal window
sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf

重启 SSH 服务使变更生效:

Terminal window
sudo systemctl restart sshd
CAUTION

重启服务后,先别关闭当前 SSH 终端。开一个新终端测试新端口和密钥能否连上,确认无误后再关闭旧窗口。

2.2.4 测试连接#

从本地 Windows 测试连接:

PowerShell 7
ssh -i ~/.ssh/id_ed25519_vps -p <SSH_PORT> root@<VPS_IP>

如果私钥设置了 passphrase,在连接时会提示输入;如果没有设置,则直接登录

2.2.5 配置本地快捷登录#

如果希望后续不用每次都输路径和端口,可以在本地 ~/.ssh/config 添加一个配置块

在 Windows Powershell 7 输入:

PowerShell 7
notepad $HOME/.ssh/config

回车后会打开一个记事本编辑器,输入以下内容:

Host my-vps
HostName <VPS_IP>
User root
Port <SSH_PORT>
IdentityFile ~/.ssh/id_ed25519_vps
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3

保存后即可在 pwsh7 使用 ssh my-vps 登录。


2.3 创建新用户#

为贯彻最小权限原则,创建一个专用用户来运行服务和管理文件。

TIP

以下步骤仍在当前的 root SSH 会话中执行,完成后将切换到新用户

2.3.1 创建用户并设置密码#

Terminal window
# 创建用户 <YOUR_USER>(UID 1000,带 home 目录)
sudo useradd -m -u 1000 <YOUR_USER>
# 设置密码(必须设置,否则无法使用 sudo)
sudo passwd <YOUR_USER>
NOTE

系统会提示你输入两次密码,虽然我们主要用密钥登录,但 sudo 默认需要用户有密码,否则会报错 user is not in the sudoers file 或 no password set

2.3.2 加入 sudo 组#

Terminal window
sudo usermod -aG sudo <YOUR_USER>

2.3.3 配置免密 sudo(可选)#

CAUTION

跳过密码验证会降低安全性,仅建议在个人独占的 VPS 上使用。

Terminal window
sudo visudo

在文件末尾添加:

<YOUR_USER> ALL=(ALL) NOPASSWD: ALL
TIP

比较推荐的方法是保留密码验证,只对特定命令免密

比如 sudo systemctl start xray

本教程为简化流程采用全局免密

2.3.4 将公钥复制给新用户#

在本地 PowerShell 中运行:

PowerShell 7
$pubKey = Get-Content "$HOME\.ssh\id_ed25519_vps.pub"
$pubKey | ssh my-vps "mkdir -p /home/<YOUR_USER>/.ssh && chmod 700 /home/<YOUR_USER>/.ssh && cat >> /home/<YOUR_USER>/.ssh/authorized_keys && chmod 600 /home/<YOUR_USER>/.ssh/authorized_keys && chown -R <YOUR_USER>:<YOUR_USER> /home/<YOUR_USER>/.ssh"

2.3.5 更新本地 SSH config#

追加到本地 ~/.ssh/config:

Host my-vps-user
HostName <VPS_IP>
User <YOUR_USER>
Port <SSH_PORT>
IdentityFile ~/.ssh/id_ed25519_vps
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3

保存后,即可用 ssh my-vps-user 登录,再通过 sudo 执行管理命令。


2.4 部署防火墙#

本文采用以下设计:

防火墙规则 (deploy_firewall.sh) → 执行一次即可
↓
ipset 集合
↓
ASN 黑名单 (update_blacklists.sh) → 每周自动更新

2.4.1 禁用 UFW#

如果你之前启用过 UFW,先关掉:

Terminal window
sudo ufw disable
sudo systemctl stop ufw
sudo systemctl disable ufw

2.4.2 防火墙部署脚本#

TIP

请在本地编辑器复制并替换下方代码块开头的用户配置区,然后粘贴整段代码块到 VPS 终端执行

防火墙部署
sudo tee /usr/local/bin/deploy_firewall.sh >/dev/null <<'EOF'
#!/bin/bash
# ============================================================================
# 部署防火墙规则,本脚本只需执行一次
# 无 P2P 版
# 修改配置后重新执行即可,脚本会自动覆盖旧规则
# ============================================================================
# ===================== 用户配置区 =====================
SSH_PORT=54321
# Web 端口(80 用于 HTTP,443 用于 HTTPS)
# 如需放行其他端口请在此添加
WEB_PORTS=(80 443)
# 是否启用 IPv6(1=启用,0=禁用)
ENABLE_IPV6=1
# 邻居网段全局阻断(设为 "-" 表示不启用)
# 示例: NEIGHBOR_V4="10.0.0.0/8"
NEIGHBOR_V4="<YOUR_NEIGHBOR_V4_CIDR>"
NEIGHBOR_V6="<YOUR_NEIGHBOR_V6_CIDR>"
export PATH="/usr/sbin:/sbin:$PATH"
# ===================== 日志与颜色 =====================
RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
CYAN='\033[1;36m'
NC='\033[0m'
log() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
err() { echo -e "${RED}[ERR]${NC} $1"; }
die() { err "$1"; exit 1; }
# 检查 IPv6
check_v6() {
if [ "$ENABLE_IPV6" != "1" ]; then echo 0; return; fi
if [ -f /proc/net/if_inet6 ] && [ "$(wc -l < /proc/net/if_inet6)" -gt 0 ]; then
command -v ip6tables &>/dev/null && echo 1 || echo 0
else
echo 0
fi
}
# 依赖检查
check_deps() {
local missing=()
for cmd in ipset iptables; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ "${#missing[@]}" -gt 0 ]; then
err "Missing dependencies: ${missing[*]}"
err "Run: sudo apt install -y ipset iptables"
exit 1
fi
ok "Dependencies satisfied"
if [ "$HAS_V6" = "1" ] && ! command -v ip6tables &>/dev/null; then
warn "ip6tables not found, disabling IPv6"
HAS_V6=0
fi
}
# 安装 iptables-persistent 与 ipset-persistent
install_persistent() {
if dpkg -l iptables-persistent 2>/dev/null | grep -q '^ii' && dpkg -l ipset-persistent 2>/dev/null | grep -q '^ii'; then
return 0
fi
log "Installing iptables-persistent & ipset-persistent..."
echo iptables-persistent iptables-persistent/autosave_v4 boolean true \
| debconf-set-selections 2>/dev/null
echo iptables-persistent iptables-persistent/autosave_v6 boolean true \
| debconf-set-selections 2>/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent ipset-persistent &>/dev/null \
|| warn "Could not install persistent packages, will fallback to manual save"
}
# 创建 ipset 集合
create_ipsets() {
ipset create bad_asn_v4 hash:net family inet \
hashsize 4096 maxelem 500000 -exist 2>/dev/null
if [ "$HAS_V6" = "1" ]; then
ipset create bad_asn_v6 hash:net family inet6 \
hashsize 4096 maxelem 500000 -exist 2>/dev/null
fi
ok "ipset collections created"
}
# 部署 iptables 规则
deploy_v4() {
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -F
iptables -X
# 基础放行
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
# 全局阻断邻居网段
if [ "$NEIGHBOR_V4" != "-" ]; then
iptables -A INPUT -s "$NEIGHBOR_V4" -j DROP
ok "Neighbor v4 blocked globally: $NEIGHBOR_V4"
fi
# 全局阻断 ASN 黑名单
iptables -A INPUT -m set --match-set bad_asn_v4 src -j DROP
ok "Global blacklist: DROP(ASN)"
# SSH
iptables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
ok "Allow TCP port $SSH_PORT (SSH)"
# Web (80, 443)
for port in "${WEB_PORTS[@]}"; do
iptables -A INPUT -p tcp --dport "$port" -j ACCEPT
ok "Allow TCP port $port (Web)"
done
}
# 部署 ip6tables 规则
deploy_v6() {
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -F
ip6tables -X
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
# 全局阻断 邻居网段
if [ "$NEIGHBOR_V6" != "-" ]; then
ip6tables -A INPUT -s "$NEIGHBOR_V6" -j DROP
ok "Neighbor v6 blocked globally: $NEIGHBOR_V6"
fi
# 全局阻断 ASN 黑名单
ip6tables -A INPUT -m set --match-set bad_asn_v6 src -j DROP
ok "Global blacklist: DROP(ASN)"
# SSH
ip6tables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
# Web (80, 443)
for port in "${WEB_PORTS[@]}"; do
ip6tables -A INPUT -p tcp --dport "$port" -j ACCEPT
done
}
# 持久化
persist() {
if command -v netfilter-persistent &>/dev/null; then
netfilter-persistent save 2>/dev/null
ok "Rules saved via netfilter-persistent"
else
mkdir -p /etc/iptables
ipset save > /etc/iptables/ipsets 2>/dev/null
iptables-save > /etc/iptables/rules.v4
[ "$HAS_V6" = "1" ] && ip6tables-save > /etc/iptables/rules.v6
ok "Rules saved to /etc/iptables/"
warn "Consider installing iptables-persistent for automatic restore on boot"
fi
}
print_rules() {
echo ""
echo -e "${CYAN}=== IPv4 INPUT Rules ===${NC}"
iptables -L INPUT -v -n --line-numbers 2>/dev/null | head -20
if [ "$HAS_V6" = "1" ]; then
echo ""
echo -e "${CYAN}=== IPv6 INPUT Rules ===${NC}"
ip6tables -L INPUT -v -n --line-numbers 2>/dev/null | head -15
fi
}
# 主流程
main() {
if [ "$(id -u)" != "0" ]; then
die "This script must be run as root"
fi
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${GREEN} Firewall Deployment (One-Time Setup)${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
HAS_V6=$(check_v6)
[ "$HAS_V6" = "1" ] && ok "IPv6 enabled" || warn "IPv6 disabled"
check_deps
create_ipsets
echo ""
log "Deploying IPv4 rules..."
deploy_v4
if [ "$HAS_V6" = "1" ]; then
echo ""
log "Deploying IPv6 rules..."
deploy_v6
fi
echo ""
log "Persisting rules..."
install_persistent
persist
print_rules
echo ""
echo -e "${GREEN}Firewall deployed.${NC}"
echo -e "${YELLOW}Next: run update_blacklists.sh to load ASN blacklists.${NC}"
echo ""
}
main "$@"
EOF

赋予执行权限并运行:

Terminal window
sudo chmod +x /usr/local/bin/deploy_firewall.sh
sudo /usr/local/bin/deploy_firewall.sh

脚本会:

  1. 创建 ipset 集合(bad_asn_v4、bad_asn_v6)
  2. 清空原有 iptables 规则并部署新规则
  3. 安装 iptables-persistent 并保存规则(重启后自动恢复)
NOTE

此脚本只部署防火墙规则,不会下载黑名单数据,此时 ipset 集合为空,所有 443 请求都可以正常访问。

2.4.3 黑名单更新脚本#

TIP

直接复制下方代码块至终端回车,即可一键生成更新黑名单脚本:

一键写入更新黑名单脚本
sudo tee /usr/local/bin/update_blacklists.sh >/dev/null <<'EOF'
#!/bin/bash
# ============================================================================
# 更新 ASN 黑名单
# ============================================================================
# 需要屏蔽的 ASN(格式:ASN|名称)
ASNS=(
"14061|DigitalOcean"
"16509|AWS"
"15169|Google_Cloud"
"8075|Azure"
"20473|Vultr"
"31898|Oracle"
)
# 缓存目录
CACHE_DIR="/var/lib/seedbox-firewall"
export PATH="/usr/sbin:/sbin:$PATH"
set -o pipefail
# ===================== 日志与颜色 =====================
RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
CYAN='\033[1;36m'
NC='\033[0m'
log() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
err() { echo -e "${RED}[ERR]${NC} $1"; }
die() { err "$1"; exit 1; }
ln() { echo "------------------------------"; }
fmt() { printf "%'d" "$1"; }
# 检查 IPv6
check_v6() {
if [ -f /proc/net/if_inet6 ] && [ "$(wc -l < /proc/net/if_inet6)" -gt 0 ]; then
command -v ip6tables &>/dev/null && echo 1 || echo 0
else
echo 0
fi
}
# 依赖检查
check_deps() {
local missing=()
for cmd in curl awk python3 ipset whois sha256sum; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ "${#missing[@]}" -gt 0 ]; then
die "Missing: ${missing[*]}. Run: sudo apt install -y curl gawk python3 ipset iproute2 whois"
fi
# 检查 ASN ipset 是否存在(由 deploy_firewall.sh 创建)
if ! ipset list bad_asn_v4 &>/dev/null; then
die "ipset 'bad_asn_v4' not found. Run deploy_firewall.sh first."
fi
}
# ===================== 缓存函数 =====================
init_cache() {
mkdir -p "$CACHE_DIR" 2>/dev/null || { die "Cannot create $CACHE_DIR"; }
}
get_cached_hash() {
local name="$1"
local hashfile="${CACHE_DIR}/${name}.sha256"
[ -f "$hashfile" ] && cat "$hashfile" || echo ""
}
set_cached_hash() {
local name="$1" hash="$2"
echo "$hash" > "${CACHE_DIR}/${name}.sha256"
}
is_changed() {
local name="$1" cidr_file="$2"
if [ ! -s "$cidr_file" ]; then
return 0 # 空文件视为有变化(或错误)
fi
local new_hash=$(sha256sum "$cidr_file" | awk '{print $1}')
local old_hash=$(get_cached_hash "$name")
if [ "$new_hash" = "$old_hash" ] && [ -n "$old_hash" ]; then
return 1 # 未变化
fi
return 0 # 有变化
}
update_cache() {
local name="$1" cidr_file="$2"
local new_hash=$(sha256sum "$cidr_file" | awk '{print $1}')
set_cached_hash "$name" "$new_hash"
}
# ===================== ASN 更新 =====================
update_asn() {
log "Fetching ASN routes from RADb..."
local work="/tmp/asn-update-$$"
mkdir -p "$work"
local v4_cidr="${CACHE_DIR}/asn_v4.cidr"
local v6_cidr="${CACHE_DIR}/asn_v6.cidr"
local v4_tmp="${work}/asn_v4.cidr"
local v6_tmp="${work}/asn_v6.cidr"
true > "$v4_tmp"
[ "$HAS_V6" = "1" ] && true > "$v6_tmp"
local v4_total=0 v6_total=0
for ITEM in "${ASNS[@]}"; do
ASN="${ITEM%%|*}"
NAME="${ITEM##*|}"
local whois_out="${work}/whois_${ASN}.txt"
if ! timeout 20 whois -h whois.radb.net -- "-i origin AS${ASN}" > "$whois_out" 2>/dev/null; then
warn "AS${ASN} ${NAME}: whois timeout or failed"
rm -f "$whois_out"
continue
fi
# 提取 v4 路由
local v4_count=$(awk '/^route:/ {print $2}' "$whois_out" | grep -v '^$' | tee -a "$v4_tmp" | wc -l)
v4_total=$((v4_total + v4_count))
if [ "$HAS_V6" = "1" ]; then
local v6_count=$(awk '/^route6:/ {print $2}' "$whois_out" | grep -v '^$' | tee -a "$v6_tmp" | wc -l)
v6_total=$((v6_total + v6_count))
fi
log " AS${ASN} ${NAME}: v4=${v4_count} v6=${HAS_V6:+"$v6_count"}"
rm -f "$whois_out"
done
# 更新 v4
local v4_updated=0
if [ "$v4_total" -lt 50 ]; then
warn "ASN v4 too few entries ($v4_total), keeping existing set"
elif is_changed "asn_v4" "$v4_tmp"; then
log "ASN v4 changed, swapping ipset..."
local new_set="bad_asn_v4_new"
ipset create "$new_set" hash:net family inet hashsize 4096 maxelem 500000 -exist 2>/dev/null
{
echo "create $new_set hash:net family inet hashsize 4096 maxelem 500000 -exist"
awk '{print "add '"$new_set"' " $0}' "$v4_tmp"
} | ipset restore -exist 2>/dev/null
ipset swap "$new_set" bad_asn_v4
ipset destroy "$new_set" 2>/dev/null
cp "$v4_tmp" "$v4_cidr"
update_cache "asn_v4" "$v4_tmp"
v4_updated=1
ok "ASN v4 updated: $(fmt $(wc -l < "$v4_cidr")) entries"
else
ok "ASN v4 unchanged ($(fmt $v4_total) entries), skipped"
fi
# 更新 v6
if [ "$HAS_V6" = "1" ]; then
if [ "$v6_total" -lt 50 ]; then
warn "ASN v6 too few entries ($v6_total), keeping existing set"
elif is_changed "asn_v6" "$v6_tmp"; then
log "ASN v6 changed, swapping ipset..."
local new_set="bad_asn_v6_new"
ipset create "$new_set" hash:net family inet6 hashsize 4096 maxelem 500000 -exist 2>/dev/null
{
echo "create $new_set hash:net family inet6 hashsize 4096 maxelem 500000 -exist"
awk '{print "add '"$new_set"' " $0}' "$v6_tmp"
} | ipset restore -exist 2>/dev/null
ipset swap "$new_set" bad_asn_v6
ipset destroy "$new_set" 2>/dev/null
cp "$v6_tmp" "$v6_cidr"
update_cache "asn_v6" "$v6_tmp"
ok "ASN v6 updated: $(fmt $(wc -l < "$v6_cidr")) entries"
else
ok "ASN v6 unchanged ($(fmt $v6_total) entries), skipped"
fi
fi
rm -rf "$work"
}
# ===================== 打印统计 =====================
print_summary() {
echo ""
ln
local asn_v4=$(ipset list bad_asn_v4 2>/dev/null | grep "Number of entries" | awk '{print $NF}')
echo " ASN v4: ${asn_v4:-0} entries"
if [ "$HAS_V6" = "1" ]; then
local asn_v6=$(ipset list bad_asn_v6 2>/dev/null | grep "Number of entries" | awk '{print $NF}')
echo " ASN v6: ${asn_v6:-0} entries"
fi
ln
}
# ===================== 主流程 =====================
main() {
if [ "$(id -u)" != "0" ]; then
die "This script must be run as root"
fi
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${GREEN} ASN Blacklist Update${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
HAS_V6=$(check_v6)
[ "$HAS_V6" = "1" ] && ok "IPv6 enabled" || warn "IPv6 disabled"
check_deps
init_cache
echo ""
log "Updating ASN blacklists..."
update_asn
print_summary
echo -e "${GREEN}ASN blacklist update complete.${NC}"
echo ""
}
main "$@"
EOF

赋予执行权限:

Terminal window
sudo chmod +x /usr/local/bin/update_blacklists.sh

脚本说明:

  • 原子更新:创建新集合 → 导入数据 → ipset swap 切换 → 销毁旧集合,切换耗时毫秒级,更新期间不会出现空集合窗口
  • 缓存:将解析后的 CIDR 列表存入 /var/lib/seedbox-firewall/,下次运行时对比 SHA256,数据未变化则跳过更新
  • 容错:下载失败不销毁现有集合,部分源失败不影响已成功的更新

2.4.4 首次加载黑名单并设置定时任务#

Terminal window
sudo /usr/local/bin/update_blacklists.sh

首次运行会下载并导入所有 ASN 黑名单,耗时取决于网络和 VPS 性能

NOTE

由于脚本采用原子 swap 更新,首次运行时 deploy_firewall.sh 创建的集合可能为空,需要等待导入完成后才会有数据

后续每周运行时,当前集合始终保持有效,不受更新过程影响

设置每周自动更新(cron):

Terminal window
sudo crontab -e
# 末尾添加(每周日凌晨 4 点):
0 4 * * 0 /usr/local/bin/update_blacklists.sh > /var/log/blacklist-update.log 2>&1

2.4.5 验证规则#

Terminal window
sudo iptables -L INPUT -v -n --line-numbers

预期输出类似(bad_asn_v4 为全局 DROP,不限于 443 端口):

Chain INPUT (policy DROP)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT 0 -- * * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
2 0 0 ACCEPT 0 -- lo * 0.0.0.0/0 0.0.0.0/0
3 0 0 DROP 0 -- * * <NEIGHBOR_V4> 0.0.0.0/0
4 0 0 DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set bad_asn_v4 src
5 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:<SSH_PORT>
6 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
7 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443

bad_asn_v4 规则是全局 DROP(prot 0),排在 ACCEPT 规则之前,因此先匹配黑名单丢弃,再放行白名单流量。

验证 ipset 集合数据量:

Terminal window
sudo ipset list bad_asn_v4 | grep "Number of entries"
sudo ipset list bad_asn_v6 | grep "Number of entries"

2.5 部署 Fail2ban(可选)#

虽然已禁用密码登录,但公网上的端口探测依然会消耗系统资源。Fail2ban 可以动态封禁反复试探的 IP。

如果你后续计划部署零信任 SSH(第 6 章),SSH 端口会从公网彻底隐藏,届时 Fail2ban 将不再需要,可参考 6.6 节卸载。

Terminal window
sudo apt update && sudo apt install fail2ban -y

创建自定义配置,注意将 port 改为你的 SSH 端口:

TIP

直接复制下方代码块至终端回车执行:

Terminal window
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
写入:
```ini
[sshd]
enabled = true
port = <SSH_PORT>
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 86400

10 分钟内输错 3 次密码,封禁该 IP 24 小时。

启动并设置开机自启:

Terminal window
sudo systemctl enable --now fail2ban

3. Cloudflare 配置#

3.1 添加 DNS 记录#

将域名托管到 Cloudflare,添加 DNS 解析记录:

记录类型子域名值小黄云(Proxy)用途
Acdn.<YOUR_DOMAIN>VPS IPv4开启CDN 回源
AREALITY.<YOUR_DOMAIN>VPS IPv4关闭REALITY 握手直连

如果需要双栈,可额外添加 AAAA 记录,可复用同一个子域名。

3.2 SSL/TLS 设置#

面板左侧 -> SSL/TLS -> 概述 -> 配置 -> 加密模式和源连接设置 -> 加密模式设置为 完全(严格)

面板左侧 -> SSL/TLS -> 边缘证书 -> 开启 TLS 1.3

面板左侧 -> 网络 -> 开启 gRPC

3.3 缓存规则设置#

为避免 XHTTP 请求被缓存,创建缓存规则:

面板左侧 -> 缓存 -> Cache Rules -> 创建缓存规则:

  • 名称:随意
  • 如果传入请求匹配… -> 选择 自定义筛选表达式 -> 点击 编辑表达式
  • 输入(替换为你的域名和路径):
(http.host eq "cdn.<YOUR_DOMAIN>") or (http.request.uri.path contains "/<YOUR_XHTTP_PATH>")
  • 缓存资格 → 设置为 绕过缓存

4. 安装 Xray-core 与 Caddy#

4.1 安装 Xray-core#

通过官方脚本裸装(Bare-metal)并使用 systemd 管理:

推荐安装 Pre-release 版本,目前 Stable 版本还停留在 v26.3.27,新版本包含 XHTTP 修复和功能修复等

发版策略说明

RPRX(Owner):

忘公开说明一下了,关于 Xray-core 的发版策略, pre-release 有可能是实际意义上的 stable,而 latest 也不等于“没有 bug”

没标 latest 通常是因为我觉得下个 stable 该包含什么但它还没有包含,或者单纯懒得写 release notes 那就算了下次吧,感觉比写代码都麻烦

via https://t.me/projectXray/5113846

mihomo 客户端兼容性说明

Xray-core v26.7.11 增加了 REALITY 最低客户端版本校验(minClientVer),默认要求客户端版本 ≥ 26.3.27 由于 mihomo 底层硬编码了版本号 1.8.2,两者不兼容时将导致 REALITY 握手失败

本教程的服务端配置模板已在 realitySettings 中显式添加 "minClientVer": "1.8.2" 以绕过此限制,安装最新版 Xray-core 即可正常工作

具体情况请见相关 Issue / PR:

点击一键吃瓜

本教程的 Xray 服务端通过 REALITY 协议与 mihomo 客户端通信。在开始安装之前,有必要了解一起直接影响新版 Xray 与 mihomo 的兼容性社区事件,这也是后续配置中 "minClientVer": "1.8.2" 这一行的由来。

  • 2026-05-29

Xray-core 维护者 RPRX 在 uTLS 指纹更新 PR #6181 的讨论中,收到关于俄罗斯 GFW 针对 Chrome TLS 指纹实施封锁的报告。他在分析各大第三方客户端的应对策略时,了解 mihomo 与 sing-box 的做法后,表达了强烈的不认同:

Chrome 浏览器是自动更新的,我认为无论是 mihomo 的选择不带 X25519MLKEM768 的 Chrome 120 指纹还是 sing-box 的在最新版 Chrome 指纹中移除 X25519MLKEM768 都会造成强特征,现在只是因为它们在俄罗斯的用户相对较少所以尚未被 GFW 针对而已,一旦被关注到那么这类强特征将是不可解决的,除非换成最新版且未修改的 Chrome 指纹

Sing-box 和 mihomo 这 Chrome 指纹问题。。。怎么感觉狼人越来越多了

  • 2026-07-05

Xray-core 维护者 RPRX 在 Telegram 发布严正公告,宣布 Project X 即日起完全转向俄罗斯和伊朗市场,不再处理中国 GFW 相关事件:

严正公告:由于中国翻墙娱乐圈傻逼太多带不动,且几乎无国人捐款反而一直喂国字头 Surge,以及为了规避风险,Project X 即日起完全转向俄罗斯和伊朗,严格禁止在中国大陆内使用(https://t.me/projectXtls/1513),不再处理中国 GFW 相关事件,比如不会为了对抗中国 GFW 的最新封锁而迭代,且放弃兼容中国代理软件、机场等生态,比如下个版本开始 REALITY 服务端将默认限制客户端最小版本,这也同时阻止了有问题的客户端 TLS 指纹(其它两个内核的 Chrome 指纹均有问题),此外不建议使用其它两个内核搭建 REALITY 服务端,它们的实现均过时已久、存在严重特征,不确定此前一些封锁报告的服务端内核是什么,有可能根本就不是 Xray 服务端

简单来说接触中国翻墙娱乐圈也有几年了,终于发现某些文化“培育”出的人简直烂透了没救了令人失望透顶,从开发者到 end user 再到机场主什么的全烂完了,本来还想列一下结果发现各种破事根本都列不完,就这样吧我也不想管了,绝大多数人本就根本不值得,谨此共勉

Project X 群友1:疑惑,这个事件导火索到底是什么

Project X 群友2:最开始是vless的流行压力surge支持vless协议,刘某不干,认为主流机场用户都是ss协议,然后群内气氛开始向用vless reality协议的直连用户低人一等,都是穷鬼用不起高端机场(因为高端机场用专线可以ss不走gfw),然后surge用户去攻击rprx支持者,然后今年上面对机场大打击,加上ss特征明显导致机场迎来毁灭性打击,r派系开始狂欢继续压力surge,随后双方矛盾更近一步,本来surge就有黑历史-刘某被公安喝茶🍵+闭源软件+国内公司(北京随行)开发报税,如此黑历史+surge最近只收钱不更新下刘某还挣的盆满钵满买房装修,反观rprx免费开发收获的捐赠几乎全来自于伊朗和俄罗斯,rprx觉得不值已有不满,加上目前所有主流机场秉持的原则是只要不被封,不管安不安全就用,典型的就是跳过证书验证,rprx非常反感此类行为,认为这是给gfw递刀子,机场贪图便利不为用户安全考虑,用户也不在意,加上surge用户维护surge情愿被割韭菜,几乎所有普通用户不管自身网络安全,秉持能用就行的原则,至此代理软件+机场+用户三位一体全部烂泥扶不上墙,rprx彻底爆发放弃对抗gfw

  • 2026-07-07

RPRX 更进一步,说下个版本还将禁止公网未加密代理协议出站:

昨天发的有些人误解了,本来想解释一下,发现已经有人详细阐述了,差不多就是这个意思,完全就是烂泥扶不上墙

压死骆驼的最后一根稻草就是 大型机场为了赚 Surge 用户那点钱甚至敢全部换成能被 MITM 的自签证书,至此我才认识到不光是 Surge 那些人有问题,中国翻墙娱乐圈整个生态都烂透了没救了,真的没有多少人在意用户的安全性,而是能翻出去就行,这与俄罗斯支持者真的是天壤之别,甚至用户自身也不在意,就如当初我们点出共用 SS 密码的机场 Surge 那边也是无所谓的态度,算了吧累了,我觉得“能翻出去就行、无所谓安全”的需求并不需要我的协议,全是蜜罐你能翻不出去吗,况且既然用户自身都不在意了,我天天掀 GFW 的蜜罐纯粹是吃力不讨好而已,还容易被上门,真的不会再干这种傻事了,还是明哲保身吧

下个版本还会禁止公网未加密代理协议出站(除了 Socks、HTTP,这俩协议地球人都知道是明文),大概是继清零明文 HTTP 面板、清退共用 SS 密码、删掉 allowInsecure 后,推动最基本的加密安全性的最后一块拼图,加密后的特征是后话,但至少得有个加密吧

前天发个 严正公告 没想到引起了广泛讨论,还是存在一些误解,比如下个版本开始 Xray REALITY 服务端只是会给“客户端最小版本”参数设默认值,若无所谓其它两个内核的 Chrome 指纹特征风险的话可以调低(若它们消除了相关特征才应改内置版本),另外俄伊那边的 GFW 比中国更严,咳言尽于此,除了受够了中国翻墙娱乐圈的现状、选择与机场等生态切割、明哲保身外,我更多地是希望 Xray 完全自成一派,比如只要使用 Xray 相关生态就意味着有最基本的加密安全性,而不会被自己或别人可能因为缺乏相关知识而做出的离谱操作所坑

各方反应也比较有意思,有说 Surge 用户少所以机场转向 AnyTLS 与它关系不大的,然而薯条转向 VLESS 后为了兼容 Surge 还专门开了线路,以及 AnyTLS 机场有多少是自签的问题,连大型机场的都是自签且不 pin 证书,Xray 移除 allowInsecure 也是阻力重重,还不多吗,甚至有些真证书都开它,以及世某人说翻墙是伪命题什么的,言论真的是一年比一年魔幻,这言论都出来了还不得赶紧切割吗,我觉得吧特征问题还可以逐步处理,但至少别能被解密吧,还有以 mieq 为首的 SURGE PRO 群直接弹冠相庆,唉弱智 endest user 的代表,这次懒得评价了

我的态度很明确,有些人想瞎搞就瞎搞吧,历史会反省并给出公正评价,反正任何责任或后果都与我无关,且我现在只想要境内的翻墙行为和我没任何关系,我可没那协议即使有人想上门也无法选中

  • 2026-07-11

RPRX 提交了 Commit af7eb68: REALITY server: Set default "minClientVer": "26.3.27" (change it at your own risk)

RPRX再次评论:

实现了 REALITY 的客户端不应支持该功能,如果 TLS 指纹符合规范,修改内置版本号至 26.3.27 即可,该内置版本号应随 TLS 指纹更新,不然 Xray 就得在 REALITY 服务端内置 JA4 指纹白名单了


对我们用户的问题在于:

mihomo 在构造 REALITY ClientHello 时,其 TLS Session ID 中的版本号被硬编码为 [1, 8, 2](component/tls/REALITY.go:74-76)。1.8.2 远低于 26.3.27,因此所有 mihomo 客户端的 REALITY 连接均被新版 Xray 服务端拒绝。

  • Now

部分 mihomo 用户请求 mihomo 官方调整版本号以适配 Xray 的新要求。

mihomo CTO wwqgtxx 回应并将 Issue 标记为 wontfix 关闭:

我们对 xray 的决定不做评论,但是也不会对我们的当前实现做出任何修改

事已至此,双方均无意妥协

目前对我们普通用户而言,解决路径仅存于服务端一侧,在 Xray 的 realitySettings 中显式配置 "minClientVer": "1.8.2",手动向服务端声明允许该版本的客户端接入

本教程的配置模板已预置此行配置,按下文安装最新版 Xray-core 后即可正常工作,无需额外干预。

相关链接:

【吃瓜完毕】

Terminal window
sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install --beta

如果你依然偏向 Stable,把命令的 --beta 去掉即为下载“正式版”

安装完成时会默认启动服务,需要先暂停:

Terminal window
sudo systemctl stop xray

4.2 生成凭据#

生成 UUID(需要两组,分别用于 Vision 和 XHTTP):

Terminal window
# 执行两次,得到两个不同的 UUID
xray uuid
xray uuid

分别记为 UUID_01 和 UUID_02,保存待用。

生成 REALITY 密钥对:

Terminal window
xray x25519

保存输出的 Private key 和 Public key,私钥填在服务端的 config.json,公钥填在本地客户端里。

生成 shortID:

Terminal window
openssl rand -hex 8

4.3 写入 Xray 配置文件#

TIP

请在本地的文本编辑器,将以下代码块中的占位符替换为你自己的真实信息。替换完成后,粘贴到 VPS 终端中并回车

NOTE

占位符说明:

占位符替换为
UUID_01第一组 UUID(Vision 用户)
UUID_02第二组 UUID(XHTTP 用户)
YOUR_REALITY_PRIVATE_KEY刚才生成的 Private key
YOUR_SHORT_ID刚才生成的 shortID
reality.example.com你的 REALITY 子域名(小黄云关)
cdn.example.com你的 CDN 子域名(小黄云开)
/your-xhttp-path自定义路径
"minClientVer": "1.8.2"保持默认,不要改
Terminal window
sudo tee /usr/local/etc/xray/config.json >/dev/null <<'EOF'
{
"log": {
"loglevel": "info" // 调试完成后可改为 "warning"
},
"inbounds": [
{
"listen": "0.0.0.0", // 如果需要 IPv6,改为监听 "::"
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "UUID_01", // 对应客户端出站 1
"level": 0,
"email": "vision-user",
"flow": "xtls-rprx-vision"
},
{
"id": "UUID_02", // 对应客户端出站 2/3/4/5
"level": 0,
"email": "xhttp-user"
}
],
"decryption": "none",
"fallbacks": [
{
"dest": "/run/xray/xhttp_in.sock",
// 处理“直连”的 XHTTP 请求 (对应客户端出站 2/3/5 的直连部分)
// 当 Reality 识别出 VLESS 协议但没有 flow 时,直接丢给内部 XHTTP Socket
"xver": 0
}
]
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"show": false,
"target": "/run/caddy/tls_gate.sock",
// 处理“CDN 转发”或“普通 HTTPS”请求 (对应客户端出站 3/4/5 的 CDN 部分)
// 任何 Reality 不识别的流量都丢给 Caddy 处理
"xver": 0,
"serverNames": [
"reality.example.com",
"cdn.example.com"
],
"privateKey": "YOUR_REALITY_PRIVATE_KEY",
"shortIds": ["YOUR_SHORT_ID"],
"minClientVer": "1.8.2"
},
"sockopt": {
"tcpFastOpen": true,
"tcpcongestion": "bbr",
"tcpMptcp": true,
"tcpNoDelay": true
}
},
"tag": "VISION+REALITY",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"metadataOnly": false,
"routeOnly": true
}
},
{
"listen": "/run/xray/xhttp_in.sock",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "UUID_02",
"level": 0,
"email": "xhttp-user"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"xhttpSettings": {
"host": "",
"path": "/your-xhttp-path",
"mode": "auto",
"extra": {
"noSSEHeader": false,
"scMaxEachPostBytes": 1000000,
"scMaxBufferedPosts": 30,
"xPaddingBytes": "100-1000",
"xPaddingObfsMode": true,
"xPaddingMethod": "tokenish", // 生成一个随机的 Base62 标记
"xPaddingPlacement": "queryInHeader", // 填充嵌入 Referer URL query(CF 兼容)
"xPaddingKey": "t", // URL query 参数名
"xPaddingHeader": "Referer",
"sessionIDPlacement": "path",
"sessionIDKey": "SID",
"sessionIDTable": "Base62",
"sessionIDLength": "12-20"
}
},
"sockopt": {
"tcpFastOpen": true,
"acceptProxyProtocol": false,
"tcpcongestion": "bbr",
"tcpMptcp": true,
"tcpNoDelay": true
}
},
"tag": "XHTTP_IN",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"metadataOnly": false,
"routeOnly": true
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct",
"settings": {
"finalRules": [
{
"action": "allow",
"network": "tcp",
"port": "22-65535",
"ip": ["127.0.0.1/32", "::1/128"]
}
]
}
},
{
"protocol": "blackhole",
"tag": "blocked",
"settings": {}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"ip": ["127.0.0.1", "::1"],
"port": "22-65535",
"outboundTag": "direct"
},
{
"type": "field",
"ip": [
"geoip:private"
],
"outboundTag": "blocked"
}
]
}
}
EOF

Xray 配置中的路由已预置 "port": "22-65535" 的白名单规则,用于后续零信任 SSH 通道(第 6 章)。

因为不同 VPS 厂商交付的 SSH 端口不同,故用范围端口而非固定端口,

4.4 安装 Caddy#

使用 Caddy 官方针对 Debian 的软件源安装:

Terminal window
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# 导入 GPG 密钥
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# 添加官方软件源
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
# 更新并安装
sudo apt update
sudo apt install caddy -y

4.5 编写 Caddyfile#

清空再编辑 Caddyfile:

TIP

同理,请在本地编辑器中替换其中的占位符,替换后粘贴整段代码块至终端回车执行

Terminal window
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
# 1. REALITY 域名区块(专供 REALITY 握手与伪装网页)
# 记得换成你自己的 REALITY 子域名!!!
reality.example.com {
# 监听 Xray REALITY 回落的 Unix Socket
bind unix//run/caddy/tls_gate.sock
# 引用 acme.sh 申请的 Let's Encrypt 证书与私钥
tls /etc/caddy/certs/reality.crt /etc/caddy/certs/reality.key
log {
output file /var/log/caddy/reality_access.log {
roll_size 10mib
roll_keep 7
roll_keep_for 168h
}
}
# 处理所有其他访问
# 外部扫描器或浏览器访问,直接给出伪装网页
handle {
# 伪装网页路径:/var/www/html/index.html
root * /var/www/html
file_server
}
}
# 2. CDN 域名区块(处理从 Xray 回落的 CDN 代理流量)
# 记得换成你自己的 CDN 子域名!!!
cdn.example.com {
bind unix//run/caddy/tls_gate.sock
# 申请的是多域名合并证书,引用相同的证书文件即可
tls /etc/caddy/certs/reality.crt /etc/caddy/certs/reality.key
log {
output file /var/log/caddy/cdn_access.log {
roll_size 10mib
roll_keep 7
roll_keep_for 168h
}
}
# 捕获套 CDN 的出站(出站 3/4/5)传来的合法的 XHTTP 流量
# 记得换成你自己的 XHTTP 路径!!!
# 记得换成你自己的 XHTTP 路径!!!
# 记得换成你自己的 XHTTP 路径!!!
handle /your-xhttp-path/* {
reverse_proxy unix//run/xray/xhttp_in.sock {
transport http {
versions 2
}
flush_interval -1
}
}
# 可选:探针探活路径
handle /status {
respond "ok" 200
}
handle /ping {
respond "ok" 200
}
# 对于扫描器,直接重定向到伪装网页防扫描
# 记得换成你自己的 REALITY 子域名!!!
handle {
redir https://reality.example.com{uri}
}
}
EOF

请务必仔细检查你的 handle 块配置,路径末尾的斜杠和星号 /* 不能省略

XHTTP 流量请求的 URL 是动态长随机混淆参数的

如果前缀不通配,Caddy 就会将其视为未知的外部扫描探测,返回 302 伪装页

附:简单伪装导航页

如果你没有现成的网站,可以使用下面这段单文件 HTML 代码

但是后期建议让 GenAI 丰富一下,换成个人博客、OpenList 什么的

将以下代码块整体复制到终端回车执行:

Terminal window
sudo mkdir -p /var/www/html
sudo tee /var/www/html/index.html >/dev/null <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tech Hub</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(-45deg, #e0e7ff, #f3e8ff, #dbeafe, #ede9fe);
background-size: 400% 400%;
animation: gradientBG 18s ease infinite;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 2rem;
width: 100%;
max-width: 720px;
}
.title {
font-size: clamp(2.4rem, 6vw, 3.6rem);
font-weight: 600;
color: #1e293b;
letter-spacing: -0.02em;
text-align: center;
}
.sub {
font-size: clamp(0.8rem, 1.5vw, 1rem);
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.2em;
border-bottom: 1px solid #cbd5e1;
padding-bottom: 1rem;
width: 100%;
text-align: center;
}
.nav {
display: flex;
gap: 1.5rem;
padding: 1.5rem 2.5rem;
border-radius: 80px;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.06);
flex-wrap: wrap;
justify-content: center;
}
.nav-item {
display: flex;
justify-content: center;
align-items: center;
width: 3.4rem;
height: 3.4rem;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transition: all 0.25s ease;
text-decoration: none;
border: 1px solid rgba(255, 255, 255, 0.6);
}
.nav-item img {
width: 1.6rem;
height: 1.6rem;
transition: transform 0.2s ease;
}
.nav-item:hover {
transform: translateY(-4px);
background: rgba(255, 255, 255, 0.9);
border-color: #94a3b8;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
.nav-item:hover img {
transform: scale(1.1);
}
@media (max-width: 600px) {
.wrapper { gap: 1.5rem; padding: 1rem; }
.nav { padding: 1rem 1.5rem; gap: 1rem; border-radius: 2rem; }
.nav-item { width: 2.8rem; height: 2.8rem; }
.nav-item img { width: 1.3rem; height: 1.3rem; }
}
</style>
</head>
<body>
<div class="wrapper">
<h1 class="title">Dev Center</h1>
<div class="sub">Let there be light</div>
<div class="nav">
<a href="https://www.google.com" class="nav-item" title="Google"><img src="https://cdn.simpleicons.org/google/1e293b" alt="Google"></a>
<a href="https://github.com" class="nav-item" title="GitHub"><img src="https://cdn.simpleicons.org/github/1e293b" alt="GitHub"></a>
<a href="https://dash.cloudflare.com" class="nav-item" title="Cloudflare"><img src="https://cdn.simpleicons.org/cloudflare/1e293b" alt="Cloudflare"></a>
<a href="https://hub.docker.com" class="nav-item" title="Docker Hub"><img src="https://cdn.simpleicons.org/docker/1e293b" alt="Docker"></a>
<a href="https://stackoverflow.com" class="nav-item" title="Stack Overflow"><img src="https://cdn.simpleicons.org/stackoverflow/1e293b" alt="Stack Overflow"></a>
<a href="https://www.npmjs.com" class="nav-item" title="npm"><img src="https://cdn.simpleicons.org/npm/1e293b" alt="npm"></a>
<a href="https://gitlab.com" class="nav-item" title="GitLab"><img src="https://cdn.simpleicons.org/gitlab/1e293b" alt="GitLab"></a>
</div>
</div>
</body>
</html>
EOF

4.6 用户组与运行目录设置#

启动 Xray 和 Caddy 前需统一运行用户为 caddy,否则会出现 socket 权限问题:

[Info] transport/internet/tcp: REALITY: failed to dial dest: dial unix /run/caddy/tls_gate.sock: connect: permission denied

进一步,为避免两个服务重启时因 RuntimeDirectory 被 systemd 清理而导致 UDS 套接字文件丢失,我们将双方的套接字放入各自所属服务的运行时目录(Xray 使用 /run/xray/,Caddy 使用 /run/caddy/):

Terminal window
# 统一运行用户为 caddy
sudo mkdir -p /etc/systemd/system/xray.service.d
sudo tee /etc/systemd/system/xray.service.d/override.conf <<EOF
[Service]
User=caddy
EOF
# 为 Caddy 启用独立的运行目录以放套接字
sudo mkdir -p /etc/systemd/system/caddy.service.d
sudo tee /etc/systemd/system/caddy.service.d/override.conf <<EOF
[Service]
User=caddy
RuntimeDirectory=caddy
EOF
# 重新加载 Systemd 配置
sudo systemctl daemon-reload

4.7 申请公有受信任证书#

点击查看:为什么不能用 Caddy 自带的自动申请证书功能
NOTE

Caddy 默认依赖 HTTP-01 或 TLS-ALPN-01 进行 ACME 自动化证书验证,此过程的硬性前置条件是 Web 服务器必须能够直接在操作系统层面接管并监听公网的 80 或 443 端口。

在当前架构中,公网 443 端口由 Xray 独占监听,Caddy 仅绑定于内网的 Unix Socket 充当回落目标。当 Let’s Encrypt 验证集群发起 TLS-ALPN-01 请求时,流量首先到达 Xray 的 REALITY 服务端。由于该请求不具备有效的 XTLS 身份特征,REALITY 服务端会将其视为未知流量,执行透明的双向 TCP 转发(直接回落至 Caddy 的 Unix Socket)。

尽管验证流量最终送达了 Caddy,但证书申请依然会失败。因为 Caddy 的 ACME 模块在处理 TLS-ALPN-01 挑战时,强依赖于标准 443 端口监听器来挂载专用的验证证书。面对来自 Unix Socket 的回落流量,Caddy 的验证上下文无法正确匹配并下发该证书。即便 Caddy 异常响应了临时的 ACME 验证证书,由于 REALITY 服务端需持续抓取目标后端返回的 ServerHello 以缓存真实证书,这种非标准的临时证书将引发 REALITY 内部证书缓存逻辑链异常,导致整个 TLS 握手在链路中被异常阻断。

所以转而使用 acme.sh 脚本动态下发 DNS TXT 记录(即基于 API 交互的 DNS-01 验证),以实现 TLS 证书合规签发。

另外,REALITY 节点需使用真实 IP 直接承载直连流量(CDN Proxy OFF),若部署未受公共根证书信任的 Cloudflare Origin CA 证书,浏览器直接访问伪装站时会触发 TLS 错误。

第一步:获取 Cloudflare API Token

  1. 登录你的 Cloudflare 网页后台;
  2. 展开左边栏 -> 选择 管理账户 -> 账户 API 令牌;
  3. 点击 创建令牌;
  4. 令牌名称自定义;
  5. 权限策略 -> 编辑策略:选择 指定域名,然后下拉框选你的主域名;
  6. 在下方的权限组列表中,点击展开 DNS & Zones 类别
    • 区域 (Zone) - 区域 (Zone) - 勾选读取 (Read)
    • 区域 (Zone) - DNS - 勾选 编辑 (Edit)
  7. 令牌过期时间建议直接选择 “无过期时间”;
  8. 客户端 IP 地址过滤,可以什么都不填,只要你不把 Token 泄露给别人,就是安全的;或者填上运行 acme.sh 脚本的服务器的公网 IP 地址;
  9. 审核令牌 -> 创建令牌;
  10. 接着会给出 账户ID 和 CF_Token,复制并保存

第二步:在 VPS 切换至 root 并安装 acme.sh

为了后续能够全自动在后台无感续签,这一步全程在 root 用户下执行,以免权限不足导致续签后重载 Caddy 失败。

Terminal window
# SSH 登录 VPS 并切换到 root
sudo -i
# 设置你的真实邮箱,并安装 acme.sh
EMAIL="your_email@example.com"
curl https://get.acme.sh | sh -s email="$EMAIL"
# 切换默认颁发机构为 Let's Encrypt
ACME=/root/.acme.sh/acme.sh
$ACME --set-default-ca --server letsencrypt

第三步:隐式导入 Token 并申请证书

为防止 Token 泄露到 .bash_history 中,我们使用静默输入:

Terminal window
# 执行后,直接粘贴你的 Token 并回车(屏幕上不会显示)
read -rsp 'Cloudflare API Token: ' CF_Token
echo
export CF_Token
export CF_Account_ID="你的Account_ID"
# 申请证书 (必须同时包含 Reality 直连域名和 CDN 回源域名,否则 Cloudflare 在“完全(严格)”模式下回源会报 526 证书错误)
$ACME --issue --dns dns_cf -d "reality.example.com" -d "cdn.example.com"

第四步:提取证书并配置自动授权重载

我们需要把证书提取到 Caddy 的配置目录,每次续签后,私钥文件的属主会被重置为 root,这将导致低权限运行的 Caddy 无法读取私钥而崩溃。因此必须在 --reloadcmd 中加入 chown 权限交接命令:

Terminal window
# 创建目录
mkdir -p /etc/caddy/certs
# 提取证书并设定重载命令
# 安装时只需要指定主域名(第一个 -d 域名),acme.sh 会自动关联
$ACME --install-cert -d "reality.example.com" \
--key-file /etc/caddy/certs/reality.key \
--fullchain-file /etc/caddy/certs/reality.crt \
--reloadcmd "chown caddy:caddy /etc/caddy/certs/reality.* && systemctl reload caddy"
NOTE

证书安装后,私钥文件默认权限为 -rw-------(600),仅属主(root)可读写。
若 Caddy 以 caddy 用户运行,需确保其能读取。上述 reloadcmd 中的 chown 已解决属主问题。
若需进一步限制权限,可额外执行 chmod 640 /etc/caddy/certs/reality.key。

TIP

此时报错 Reload error for: reality.example.com / caddy.service is not active 无伤大雅,因为我们在下一步才正式启动应用。

完成后,输入 exit 退出 root 环境,返回普通用户。

4.8 启动服务#

启动前检查
为避免 permission denied 错误,请再次确认以下路径的属主正确:

Terminal window
# Caddy 日志目录
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
sudo chmod 755 /var/log/caddy
# 证书文件
ls -la /etc/caddy/certs/
# 应显示 -rw-r--r-- 1 caddy caddy reality.crt
# 应显示 -rw------- 1 caddy caddy reality.key
# 若属主不是 caddy,请执行修正:
sudo chown -R caddy:caddy /var/log/caddy
sudo chown caddy:caddy /etc/caddy/certs/reality.*
sudo chmod 640 /etc/caddy/certs/reality.key

正式启动:

Terminal window
sudo systemctl enable --now xray caddy

验证状态:

Terminal window
systemctl show xray --property=User,Group
systemctl show caddy --property=User,Group
ls -l /run/xray/ /run/caddy/

理想输出:

Terminal window
User=caddy
Group=
---
User=caddy
Group=caddy
# /run/xray/
srwxr-xr-x 1 caddy caddy 0 ... xhttp_in.sock
-rw-r--r-- 1 caddy caddy 0 ... xhttp_in.sock.lock
# /run/caddy/
s-w------- 1 caddy caddy 0 ... tls_gate.sock

4.9 限制 Systemd Journal 日志大小#

Caddy 已配置了原生内置的日志滚动机制,但 Systemd 的日志默认仅按系统空闲空间比例限制大小,需要手动限制最大占用空间。

TIP

如果不需要保留访问日志,或者磁盘空间十分有限,可以直接将 Caddyfile 中的整个 log { ... } 代码块移除。

Systemd 的日志(journald)默认只按体积轮转但不设硬性上限,需要手动限制最大占用空间。

Terminal window
sudo nano /etc/systemd/journald.conf

找到并取消注释、修改以下两行(若不存在则手动添加):

/etc/systemd/journald.conf
SystemMaxUse=100M
MaxFileSec=1week

保存后重启服务,使新配置生效:

Terminal window
sudo systemctl restart systemd-journald

4.10 后续更新#

更新 Xray-core

官方安装脚本支持直接升级,重新运行安装命令即可:

Terminal window
# 检查是否有新版本
sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ check
# 升级到最新 Pre-release(推荐)
sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install --beta

脚本会自动检测版本号差异、下载新二进制、替换并重启服务。你的 config.json、override.conf、日志等配置文件全部保留,不会被覆盖。

升级后 systemd 主 service 文件会被重写为默认的 User=nobody,但 override.conf(User=caddy)优先级更高,运行时用户不受影响。 若要消除 Special user nobody configured 警告,可手动修复:

Terminal window
sudo sed -i 's/User=nobody/User=caddy/' /etc/systemd/system/xray.service
sudo systemctl daemon-reload

更新 Caddy

通过 apt 更新:

Terminal window
sudo apt update
sudo apt upgrade caddy -y

5. 客户端配置#

5.1 mihomo 客户端#

由于 mihomo GUI 繁多,此处仅给出裸核 config:

mihomo-client-template.yaml
proxies:
# 1. XTLS(Vision) + REALITY 直连
- name: "出站1-XTLS+REALITY"
type: vless
server: "YOUR_VPS_IP"
port: 443
uuid: "YOUR_UUID_01"
encryption: "none"
flow: xtls-rprx-vision
network: tcp
# 以下两项取决于你的实际 UDP 使用场景,不需要请注释或删除
# udp: true # 需要走代理的境外游戏/QUIC/WebRTC 则开启
# packet-encoding: xudp # UDP 编码方式,开启后支持 FullCone NAT(默认即此值)
tls: true
alpn: [h2]
servername: "reality.example.com" # Reality 伪装域名
client-fingerprint: chrome
reality-opts:
public-key: "YOUR_PUB_KEY"
short-id: "YOUR_SHORT_ID"
# 2. XHTTP + REALITY 直连
- name: "出站2-XHTTP+REALITY"
type: vless
server: "YOUR_VPS_IP"
port: 443
uuid: "YOUR_UUID_02"
encryption: "none"
flow: ""
network: xhttp
# udp: true
# packet-encoding: xudp
tls: true
alpn: [h2]
servername: "reality.example.com" # Reality 伪装域名
client-fingerprint: chrome
reality-opts:
public-key: "YOUR_PUB_KEY"
short-id: "YOUR_SHORT_ID"
xhttp-opts:
path: /your-xhttp-path # 必须与服务端配置的 path 一致
mode: auto # Reality 模式下 auto 会自动选择 stream-one
# host: "reality.example.com" # 直连时可省略,默认继承 servername
session-table: "Alphabet" # 自定义 session ID 字符集,降低 CDN 特征识别
session-length: "12-20" # session ID 长度范围(起始值不可为 0,总 ID 空间须 > 21 亿)
session-placement: path # Session ID 放在 URL 路径中(path 模式下 session-key 无效)
x-padding-obfs-mode: true # 启用填充混淆
x-padding-method: tokenish # 随机 Base62 标记,HPACK 编码后精确匹配目标长度
x-padding-placement: queryInHeader # 填充嵌入 Referer URL query 中(CF 兼容)
x-padding-key: "t" # URL query 参数名,伪装成 analytics token
x-padding-header: "Referer" # 填充存放的 HTTP Header 名,默认 Referer(CF 兼容)
sc-stream-up-server-secs: "30-60" # 单连接流控保活心跳间隔,减少 CDN 超时断开
reuse-settings: # 链接复用设置(即XMUX)
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
# 3. 上行 XHTTP + TLS(ECH) + CDN | 下行 XHTTP + REALITY(上下行不同 SNI)
- name: "出站3-CDN上行+XHTTP下行"
type: vless
server: "cdn.example.com" # CDN 域名或优选 IP (上行)
port: 443
uuid: "YOUR_UUID_02"
encryption: "none"
flow: ""
network: xhttp
ech-opts: {enable: true} # 隐藏 SNI
# udp: true
# packet-encoding: xudp
tls: true
alpn: [h3]
servername: "cdn.example.com" # 写你套 CDN 对应的子域名,CDN 通过它回源服务器
client-fingerprint: chrome
skip-cert-verify: false # 强制校验服务端证书,防止中间人攻击
xhttp-opts: # 上行设置
host: "cdn.example.com" # CDN 转发所需的 HTTP Host,与 servername 保持相同
path: /your-xhttp-path
mode: auto # CDN 模式下 auto 会自动选择 stream-up (H2)
session-table: "Alphabet" # 自定义 session ID 字符集,降低 CDN 特征识别
session-length: "12-20" # session ID 长度范围
session-placement: path # Session ID 放在 URL 路径中(path 模式下 session-key 无效)
x-padding-obfs-mode: true # 启用填充混淆
x-padding-method: tokenish # 随机 Base62 标记,HPACK 编码后精确匹配目标长度
x-padding-placement: queryInHeader # 填充嵌入 Referer URL query 中(CF 兼容)
x-padding-key: "t" # URL query 参数名,伪装成 analytics token
x-padding-header: "Referer" # 填充存放的 HTTP Header 名,默认 Referer(CF 兼容)
sc-stream-up-server-secs: "30-60" # 单连接流控保活心跳间隔,减少 CDN 超时断开
reuse-settings: # 上行链接复用设置(即XMUX)
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
download-settings: # 下行设置(Reality 不支持 h3,须显式覆盖为 h2)
alpn: [h2] # 显式指定 h2,覆盖主代理继承来的 h3
server: "<YOUR_VPS_IP>" # 下行地址 (VPS 直连)
port: 443
servername: "reality.example.com" # Reality 伪装域名
reality-opts:
public-key: "YOUR_PUB_KEY"
short-id: "YOUR_SHORT_ID"
# host 继承自外层或省略;mode 强制继承外层
reuse-settings: # 下行链接复用设置(即XMUX)
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
# 4. XHTTP + TLS(ECH) 过 CDN(上下行不分离)
- name: "出站4-CDN上下行"
type: vless
server: "cdn.example.com" # CDN 域名或优选 IP
port: 443
uuid: "YOUR_UUID_02"
encryption: "none"
flow: ""
network: xhttp
ech-opts: {enable: true} # 隐藏 SNI
# udp: true
# packet-encoding: xudp
tls: true
alpn: [h3]
servername: "cdn.example.com" # 写你套 CDN 对应的子域名,CDN 通过它回源服务器
client-fingerprint: chrome
skip-cert-verify: false # 强制校验服务端证书,防止中间人攻击
xhttp-opts:
host: "cdn.example.com" # CDN 转发所需的 HTTP Host,与 servername 保持相同
path: /your-xhttp-path
mode: auto
session-table: "Alphabet" # 自定义 session ID 字符集,降低 CDN 特征识别
session-length: "12-20" # session ID 长度范围
session-placement: path # Session ID 放在 URL 路径中(path 模式下 session-key 无效)
x-padding-obfs-mode: true # 启用填充混淆
x-padding-method: tokenish # 随机 Base62 标记,HPACK 编码后精确匹配目标长度
x-padding-placement: queryInHeader # 填充嵌入 Referer URL query 中(CF 兼容)
x-padding-key: "t" # URL query 参数名,伪装成 analytics token
x-padding-header: "Referer" # 填充存放的 HTTP Header 名,默认 Referer(CF 兼容)
sc-stream-up-server-secs: "30-60" # 单连接流控保活心跳间隔,减少 CDN 超时断开
reuse-settings: # 链接复用设置(即XMUX)
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
# 5. 上行 XHTTP + REALITY | 下行 XHTTP + TLS(ECH) + CDN(上下行不同 SNI)
- name: "出站5-上XHTTP+REALITY下XHTTP+TLS+CDN"
type: vless
server: "YOUR_VPS_IP"
port: 443
uuid: "YOUR_UUID_02"
encryption: "none"
flow: ""
network: xhttp
# udp: true
# packet-encoding: xudp
tls: true
alpn: [h2]
servername: "reality.example.com" # Reality 伪装域名
client-fingerprint: chrome
skip-cert-verify: false # 强制校验服务端证书,防止中间人攻击
reality-opts:
public-key: "YOUR_PUB_KEY"
short-id: "YOUR_SHORT_ID"
xhttp-opts:
host: "cdn.example.com" # CDN 转发所需的 HTTP Host,与 servername 保持相同
path: /your-xhttp-path
mode: auto
session-table: "Alphabet" # 自定义 session ID 字符集,降低 CDN 特征识别
session-length: "12-20" # session ID 长度范围
session-placement: path # Session ID 放在 URL 路径中(path 模式下 session-key 无效)
x-padding-obfs-mode: true # 启用填充混淆
x-padding-method: tokenish # 随机 Base62 标记,HPACK 编码后精确匹配目标长度
x-padding-placement: queryInHeader # 填充嵌入 Referer URL query 中(CF 兼容)
x-padding-key: "t" # URL query 参数名,伪装成 analytics token
x-padding-header: "Referer" # 填充存放的 HTTP Header 名,默认 Referer(CF 兼容)
sc-stream-up-server-secs: "30-60" # 单连接流控保活心跳间隔,减少 CDN 超时断开
reuse-settings: # 上行链接复用设置(即XMUX)
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
download-settings: # 下行设置(CDN 路径,改用 h3)
path: /your-xhttp-path
host: "" # host 继承自外层或省略;mode 强制继承外层
server: "cdn.example.com" # CDN / 优选 IP (下行连接地址)
port: 443
tls: true
alpn: [h3]
servername: "cdn.example.com" # 写你套 CDN 对应的子域名,CDN 通过它回源服务器
client-fingerprint: chrome
skip-cert-verify: false # 强制校验服务端证书,防止中间人攻击
ech-opts: {enable: true} # 隐藏 SNI
reality-opts: { public-key: "" } # 强制清空 Reality 公钥,否则下行连接 CDN 将会携带 Reality 握手
reuse-settings: # 下行链接复用设置(即XMUX)
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
锚点写法
mihomo-client-template-anchors.yaml
# 定义锚点
x-anchors:
# 0. 用户变量 (请在此处统一修改为自己的配置,下方会自动引用)
variables:
vps_ip: &vps_ip "YOUR_VPS_IP"
uuid_01: &uuid_01 "YOUR_UUID_01"
uuid_02: &uuid_02 "YOUR_UUID_02"
reality_domain: &reality_domain "reality.example.com"
cdn_domain: &cdn_domain "cdn.example.com"
xhttp_path: &xhttp_path "/your-xhttp-path"
# 1. Reality 密钥配置
reality-opts: &reality-opts
public-key: "YOUR_PUB_KEY"
short-id: "YOUR_SHORT_ID"
# 2. 链接复用设置参数
reuse-settings: &reuse-settings
max-concurrency: "16-32" # 单连接最大并发请求数(0=不限制)
# max-connections: "3" # 最大连接数(0=不限制)(与 max-concurrency 冲突,二选一)
c-max-reuse-times: "0" # 单连接最大复用次数(0=不限制)
# h-max-request-times: "600-900" # 单连接累计 HTTP 请求数(不严谨,慎用)
h-max-reusable-secs: "1800-3000" # 单连接最长复用时间(秒)
h-keep-alive-period: 0 # H2/H3 保活间隔 (0=默认智能值, -1=关闭, 支持填正数范围但不推荐)
# 3. XHTTP 混淆配置
xhttp-obfs: &xhttp-obfs
path: *xhttp_path
mode: auto
session-table: "Alphabet" # 自定义 session ID 字符集,降低 CDN 特征识别
session-length: "12-20" # session ID 长度范围
session-placement: path # Session ID 放在 URL 路径中
x-padding-obfs-mode: true # 启用填充混淆
x-padding-method: tokenish # 随机 Base62 标记,HPACK 编码后精确匹配目标长度
x-padding-placement: queryInHeader # 填充嵌入 Referer URL query 中
x-padding-key: "t" # URL query 参数名,伪装成 analytics token
x-padding-header: "Referer" # 填充存放的 HTTP Header 名,默认 Referer
sc-stream-up-server-secs: "30-60" # 单连接流控保活心跳间隔,减少 CDN 超时断开
reuse-settings: *reuse-settings
# 4. UDP 配置
udp-opts: &udp-opts
# 以下两项取决于你的实际 UDP 使用场景
udp: false # 境外游戏/QUIC/WebRTC 有需要走代理的则改为 true
# packet-encoding: xudp # UDP 编码方式
# 最终节点配置
proxies:
# 1. XTLS(Vision) + REALITY 直连
- name: "出站1-XTLS+REALITY"
type: vless
server: *vps_ip
port: 443
uuid: *uuid_01
encryption: "none"
flow: xtls-rprx-vision
network: tcp
<<: *udp-opts
tls: true
alpn: [h2]
servername: *reality_domain
client-fingerprint: chrome
reality-opts: *reality-opts
# 2. XHTTP + REALITY 直连
- name: "出站2-XHTTP+REALITY"
type: vless
server: *vps_ip
port: 443
uuid: *uuid_02
encryption: "none"
flow: ""
network: xhttp
<<: *udp-opts
tls: true
alpn: [h2]
servername: *reality_domain
client-fingerprint: chrome
reality-opts: *reality-opts
xhttp-opts: *xhttp-obfs
# 3. 上行 XHTTP + TLS(ECH) + CDN | 下行 XHTTP + REALITY
- name: "出站3-CDN上行+XHTTP下行"
type: vless
server: *cdn_domain
port: 443
uuid: *uuid_02
encryption: "none"
flow: ""
network: xhttp
<<: *udp-opts
tls: true
alpn: [h3]
servername: *cdn_domain
client-fingerprint: chrome
skip-cert-verify: false
ech-opts: {enable: true}
xhttp-opts:
<<: *xhttp-obfs
host: *cdn_domain
download-settings:
server: *vps_ip
port: 443
tls: true
alpn: [h2]
servername: *reality_domain
client-fingerprint: chrome
reality-opts: *reality-opts
reuse-settings: *reuse-settings
# 4. XHTTP + TLS(ECH) 过 CDN(上下行不分离)
- name: "出站4-CDN上下行"
type: vless
server: *cdn_domain
port: 443
uuid: *uuid_02
encryption: "none"
flow: ""
network: xhttp
<<: *udp-opts
tls: true
alpn: [h3]
servername: *cdn_domain
client-fingerprint: chrome
skip-cert-verify: false
ech-opts: {enable: true}
xhttp-opts:
<<: *xhttp-obfs
host: *cdn_domain
# 5. 上行 XHTTP + REALITY | 下行 XHTTP + TLS(ECH) + CDN
- name: "出站5-上XHTTP+REALITY下XHTTP+TLS+CDN"
type: vless
server: *vps_ip
port: 443
uuid: *uuid_02
encryption: "none"
flow: ""
network: xhttp
<<: *udp-opts
tls: true
alpn: [h2]
servername: *reality_domain
client-fingerprint: chrome
skip-cert-verify: false
reality-opts: *reality-opts
xhttp-opts:
<<: *xhttp-obfs
host: *cdn_domain
download-settings:
server: *cdn_domain
port: 443
tls: true
alpn: [h3]
servername: *cdn_domain
client-fingerprint: chrome
skip-cert-verify: false
ech-opts: {enable: true}
path: *xhttp_path
host: ""
reality-opts: { public-key: "" }
reuse-settings: *reuse-settings

5.2 ALPN、ECH 与 XMUX 说明#

ALPN#

  • mihomo v1.19.23:仅支持 h2
  • mihomo v1.19.24+:支持 http/1.1 和 h3,但不支持多协议动态回退,写法如下:
proxies:
- name: xxx
alpn: [h2] # 使用 HTTP/2(默认)
# alpn: [http/1.1] # 强制使用 HTTP/1.1
# alpn: [h3] # 强制使用 HTTP/3。注意:不支持 REALITY,必须 tls: true

ECH#

proxies:
- name: xxx
ech-opts: {enable: true}

Cloudflare 免费版现已默认启用 ECH(Encrypted ClientHello)

针对通过 CDN 回源的出站节点,加上此参数后,mihomo 会自动发起 HTTPS DNS 记录查询,拉取 Cloudflare 下发的 ECH 公钥,并在 TLS 握手时加密 SNI,可以有效防止 GFW 通过嗅探 SNI 来封锁连接,一定程度上提升连接的隐蔽性

NOTE
  1. 只有套了 CDN 的节点才能用
  2. mihomo 需要写支持 DoH 解析的 DNS
  3. 域名需要开启 TLS 1.3

更多详情请查看末尾推荐的文章

XMUX(链接复用)#

IMPORTANT

Xray-core 近期的版本演进中(Issue #6376),对 XMUX 的默认策略经历了多次调整(从最初的 16-32,到 1,再到 3)。

  • 本模板中显式写了 max-concurrency: "16-32"。效果是强制锁定在较稳定的多路复用状态,不会因继承内核默认值而导致并发 TCP 连接数突增(仅为个人理解)
  • 如果为了对抗严格的审查环境(像 issue 提到的俄罗斯抗 TSPU 封锁)的话,可改为 max-connections: 3

6. 可选进阶:部署零信任 SSH#

本方案实现 VPS 不对外暴露 SSH 端口,外部扫描器无法探测到 SSH 服务存在。日常通过 mihomo 隧道直连,极端环境下走 Cloudflare Tunnel,实现主备双通道。

6.1 前提条件#

  • VPS 完成了本章所有前置步骤(防火墙已部署、代理已在运行)
  • 本地已运行 mihomo 客户端且节点正常
  • 本地已安装 cloudflared
WARNING

先建逃生通道,最后再封门。 顺序为:配置 Xray 白名单 → 放行 lo 环回 → 部署 mihomo 隧道并测试 → 部署 CF Tunnel 并测试 → 确认两条通道都能用 → 最后再 iptables DROP 公网端口。 如果先封门再建通道,一旦通道配置失败,将无法再连接服务器。

6.2 配置 Xray 路由白名单与 Freedom 放行规则#

确认 Xray 配置中已有 SSH 环回白名单。

cat 查看 /usr/local/etc/xray/config.json,确认在 routing.rules 中 geoip:private -> blocked 之前存在以下规则:

{
"type": "field",
"ip": ["127.0.0.1", "::1"],
"port": "22-65535",
"outboundTag": "direct"
}

接着确认 Freedom 放行规则

Xray-core v26.7.11+ 在 freedom(直连)出站中内置了私网地址阻断,需在 outbounds 的 direct 出站中添加 finalRules 以放行 SSH 环回:

{
"protocol": "freedom",
"tag": "direct",
"settings": {
"finalRules": [
{
"action": "allow",
"network": "tcp",
"port": "22-65535",
"ip": ["127.0.0.1/32", "::1/128"]
}
]
}
}
关于 finalRules 的说明

Xray-core v26.7.11+ 在 freedom(直连)出站中内置了私网地址阻断(defaultBlockPrivateRule),防止 VLESS/VMess 等代理被滥用扫描内网

这是一项安全增强,默认阻断所有对 127.0.0.1/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 等私有地址的访问。

本节 SSH 隧道依赖代理将流量路由到 127.0.0.1:<SSH_PORT>,必须在此阻断之上显式放行

配置中的 finalRules 即在 freedom 出站的终局检查阶段、在默认阻断之前插入一条 allow 规则

仅放行 127.0.0.1 和 ::1 的 TCP 端口范围(22-65535),不影响对其他私有 IP 的默认拦截

另外,如果你觉得 22-65535 范围太大了,可以只放行 SSHD 实际监听的单个端口

如果不配置此规则,SSH 隧道连接将被 Xray 拦截,日志表现为:

proxy/freedom: blocked target: tcp:127.0.0.1:<SSH_PORT>, blackholing connection for XXs

如果在 4.3 节使用了模板文件,这条规则已预置,只需确认即可。

改完后需要重启 Xray:

TIP

关于 UDS 套接字独立目录设计: 在本方案的配置下,Xray 的套接字置于 /run/xray/,Caddy 的套接字置于 /run/caddy/。两者的运行目录生命周期已完全隔离,单独重启 Xray 或 Caddy 不再会清理对方的套接字,两服务可完全独立重启。

Terminal window
sudo systemctl restart xray

6.3 主通道:mihomo 隧道#

6.3.1 VPS 端:确认 lo 环回放行#

在第 2.4.2 节的初始化防火墙脚本中已全局放行了所有本地回环流量(iptables -A INPUT -i lo -j ACCEPT),因此经过代理隧道到达 VPS 本地接口的流量会被自动放行,此处仅需确认即可。

6.3.2 本地:mihomo 配置添加隧道#

在 mihomo 配置文件的顶级字段添加:

tunnels:
# 类型, 本地监听端口, VPS 目标(127.0.0.1 = lo 环回), 出站节点/组
- tcp,127.0.0.1:42222,127.0.0.1:<SSH_PORT>,<YOUR_PROXIES>
- tcp,127.0.0.1:42223,[::1]:<SSH_PORT>,<YOUR_PROXIES>
  • 42222/42223 是 Windows 本地监听端口
  • 目标填 127.0.0.1(环回),不能填公网 IP
  • 目标端口填 SSHD 实际监听的端口,执行 sudo ss -tnlp | grep sshd 确认

重启 mihomo。

6.3.3 本地:SSH config#

在 Windows 编辑 ~/.ssh/config:

# 主通道:Mihomo 隧道(依赖本地 Mihomo 开对应端口)
Host my-vps-mihomo
HostName 127.0.0.1
Port 42222
User <YOUR_USER>
IdentityFile ~/.ssh/id_ed25519_vps
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
Host my-vps-mihomo-v6
HostName 127.0.0.1
Port 42223
User <YOUR_USER>
IdentityFile ~/.ssh/id_ed25519_vps
IdentitiesOnly yes
AddressFamily inet6
ServerAliveInterval 30
ServerAliveCountMax 3

6.3.4 测试主通道#

PowerShell 7
ssh my-vps-mihomo

如果能登录则继续下一步

如果不能登录,请检查 mihomo 隧道配置和节点状态,修好再走

首次连接会提示主机指纹未识别,确认 yes 即可。如果之前通过同一端口连过其他机器,会报 REMOTE HOST IDENTIFICATION HAS CHANGED,需清除旧指纹:

PowerShell 7
ssh-keygen -R '[127.0.0.1]:42222'

6.4 备用通道:Cloudflare Tunnel#

6.4.1 CF 面板创建隧道#

  1. Zero Trust → Networks(网络) → Connectors(连接器) → Create a tunnel(创建隧道)
  2. 隧道类型 → 选择 Cloudflared
  3. 命名 xxxx-ssh(这里的命名是隧道的显示名称,随便取) → Save tunnel(保存隧道)
  4. CF 在这里会给出三个安装命令,复制粘贴执行前面两个即可

6.4.2 VPS 端安装 cloudflared#

在 VPS 上执行安装命令(即 CF 给出的前两个命令):

Terminal window
# 添加 Cloudflare apt 源
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | sudo tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# 安装 cloudflared
sudo apt-get update && sudo apt-get install cloudflared -y
# 注册为系统服务(开机自启)
sudo cloudflared service install <YOUR_TOKEN>
# 确认服务已启动
sudo systemctl status cloudflared

当执行完第二个命令之后,VPS 终端会输出类似:

... INF Using Systemd
... INF Linux service for cloudflared installed successfully

同时 CF 面板应显示绿色的「已连接」。

6.4.3 面板配置路由#

现在配置路由,在 Tunnel 面板的路由隧道页面,按以下配置 Public Hostname(主机名):

字段值
Subdomain(子域名)<YOUR_SSH_SUBDOMAIN>(子域名前缀,可自己取)
Domain(域)<YOUR_DOMAIN> (下拉选择你已托管的域名)
Path(路径)留空
Type(类型)SSH
URLlocalhost:<SSH_PORT>(填你设定的 SSH 端口)

6.4.4 本地 SSH config + 测试备用通道#

在 Windows 本地 ~/.ssh/config 添加:

Host my-vps-cf
HostName <YOUR_SSH_SUBDOMAIN>.<YOUR_DOMAIN>
User <YOUR_USER>
IdentityFile ~/.ssh/id_ed25519_vps
IdentitiesOnly yes
ProxyCommand cloudflared access ssh --hostname %h
ServerAliveInterval 30
ServerAliveCountMax 3

测试:

PowerShell 7
ssh my-vps-cf

如果能登录则继续下一步

如果不能登录,请检查 cloudflared 状态和 Tunnel 路由配置


6.5 公网 SSH 隐身#

确认两条通道都可用后,执行以下操作封闭公网 SSH 端口:

Terminal window
# 确认 SSHD 实际监听的端口
sudo ss -tnlp | grep sshd
# 赋值 SSHD 实际监听端口到本会话临时变量
SSH_PORT=$(sudo ss -tnlp | grep sshd | sed 's/.*:\([0-9]*\) .*/\1/' | head -1)
# 公网 DROP — 排除 lo 环回,避免误伤隧道流量
sudo iptables -I INPUT 2 ! -i lo -p tcp --dport $SSH_PORT -j DROP
sudo ip6tables -I INPUT 2 ! -i lo -p tcp --dport $SSH_PORT -j DROP
# 持久化
sudo netfilter-persistent save
CAUTION

我已踩过的坑:

  • 漏写 ! -i lo:公网 DROP 同时也会丢弃 lo 环回上的隧道流量,两条通道都会断
  • 端口写成 22:SSHD 实际可能监听在高位端口(如 54321),写错端口等于没封
  • 在配好通道之前执行:没有任何逃生通道就封门,等于把自己锁在外面

如果误操作导致失联,只能通过 VPS 厂商的 Rescue Mode 或 VNC 进入服务器, 挂载原系统磁盘删除 /etc/iptables/rules.v4 和 /etc/iptables/rules.v6 来恢复。 部分厂商可能不提供 Rescue/VNC,这种情况下只能重装系统。 请务必确认两条通道都测试通过后,再执行以下命令。

查看规则确认:

Terminal window
sudo iptables -L INPUT -v -n --line-numbers

预期规则 2 应为 DROP tcp dpt:<SSH_PORT> 且 !lo。


6.6 卸载 Fail2ban#

封门后,公网 <SSH_PORT> 已被 iptables DROP,外部扫描连 SYN 包都到不了 SSHD,/var/log/auth.log 不会有任何失败记录,Fail2ban 已无用武之地。

Terminal window
# 停止并禁用
sudo systemctl stop fail2ban
sudo systemctl disable fail2ban
# 卸载
sudo apt purge fail2ban -y
# 删除配置残留
sudo rm -rf /etc/fail2ban
# 确认已消失
sudo systemctl status fail2ban # 应显示 Unit not found 或 inactive

6.7 验证#

PowerShell 7
# 主力通道 — 应正常登录
ssh my-vps-mihomo
# 备用通道 — 应正常登录
ssh my-vps-cf
# 直连 — 应超时(Connection timed out)
ssh <YOUR_USER>@<VPS_IP> -p <SSH_PORT>

【完】


友情推荐#

Rokutosei
Rokutosei ✦ 六等星
“君と集まって星座になれたら”
[弃用] 服务端:单端口五合一部署及完整安全加固
https://blog.rokutosei.com/posts/proxy/vlessxhttpproxy/
作者
Rokutosei
发布于
2026-07-07
修改于
2026-09-11
许可协议
CC BY-NC-SA 4.0
✧ 文章目录 ✦
1
1. 准备工作
2
2. 系统基础防护
2.1 系统基础优化
2.2 SSH 密钥登录
2.3 创建新用户
2.4 部署防火墙
2.5 部署 Fail2ban(可选)
3
3. Cloudflare 配置
3.1 添加 DNS 记录
3.2 SSL/TLS 设置
3.3 缓存规则设置
4
4. 安装 Xray-core 与 Caddy
4.1 安装 Xray-core
4.2 生成凭据
4.3 写入 Xray 配置文件
4.4 安装 Caddy
4.5 编写 Caddyfile
4.6 用户组与运行目录设置
4.7 申请公有受信任证书
4.8 启动服务
4.9 限制 Systemd Journal 日志大小
4.10 后续更新
5
5. 客户端配置
5.1 mihomo 客户端
5.2 ALPN、ECH 与 XMUX 说明
6
6. 可选进阶:部署零信任 SSH
6.1 前提条件
6.2 配置 Xray 路由白名单与 Freedom 放行规则
6.3 主通道:mihomo 隧道
6.4 备用通道:Cloudflare Tunnel
6.5 公网 SSH 隐身
6.6 卸载 Fail2ban
6.7 验证
7
友情推荐