自建 VPN 排障与改造:从 Cloudflare 中转切换到受信任证书直连

这次排障起因是自建 VPN 连接不稳定、文件下载极慢。小页面能打开,TCP 端口也能连接,但实际下载经常只有几 KB/s,最终超时。

经过源站直连对照、服务器 TCP 统计和客户端抓包,问题逐步收窄到引入 Cloudflare 后的传输路径。最终选择的改造方向是:保留原中转节点,新增使用公开受信任证书的源站直连节点。

本文同时记录排障证据和部署方案,作为以后更换域名、调整线路、续期证书及回滚的依据。

记录状态:源站直连测速、Cloudflare 路径对照和客户端抓包已经完成;新增域名、签发证书与自动续期部分是本次确定的实施方案。撰写时尚未收到这些部署步骤的完成输出,因此不能把方案写成已验收上线。

脱敏说明:所有自有域名统一替换为 example.com;源站 IPv4 替换为文档示例地址 203.0.113.10;源站 IPv6 替换为 2001:db8::10。客户端地址、UUID、MAC、用户名、邮箱、连接标识和原始日志未公开。测速数值保留,便于复核。文中的示例地址不能直接用于实际部署。

一、改造前的架构

服务器位于美国芝加哥,系统为 Ubuntu 22.04,规格为 1 vCPU、2GB RAM。客户端使用 Windows v2rayN,实际传输协议是 VLESS + WebSocket + TLS。

原链路为:

Text
客户端 → vpn.example.com(Cloudflare 橙云)
       → 源站 Nginx:443
       → 127.0.0.1:10000 Xray
       → 目标网站

原客户端关键参数:

参数值
地址vpn.example.com
端口443
协议VLESS
encryptionnone
flow空
传输ws
WS Hostvpn.example.com
WS path/ray
TLS开启
SNIvpn.example.com
allowInsecurefalse
Mux关闭

Cloudflare 使用 Full (strict) 模式,源站 Nginx 配置的是 Cloudflare Origin CA 证书。这种证书适合 Cloudflare 回源验证,不等于普通客户端默认信任的公开证书。

服务器上的 Nginx 还承载博客、CMS 和密码管理服务,共用 443 端口。因此,不能把全局错误日志中的所有 TLS 错误都归到 VPN 节点。

二、排障依据:小页面正常不等于隧道正常

早期测试说明,以下环节能够工作:

  • Windows 可以建立到源站 443 的 TCP 连接。
  • 经 Cloudflare 和源站直连的小型 HTTPS 页面均可返回 200。
  • /ray 的标准 WebSocket Upgrade 请求,在 Cloudflare 和直连路径均可返回 101。
  • Xray 监听 127.0.0.1:10000,Nginx 监听公网 443,与架构一致。

这些结果验证了基础可达性和协议入口,但不能证明大流量传输正常。未发送有效 VLESS 数据时,WS 握手后的空闲超时也不能作为故障证据。

服务器资源快照为:负载约 0.10 / 0.15 / 0.11,可用内存约 1.1GiB。采样时没有明显资源压力,但这不是对所有时间段的保证。

2.1 相同目标的下载对照

主要使用同一个 5,000,000 字节测速资源:

Text
https://speed.cloudflare.com/__down?bytes=5000000
测试样本结果
源站直连,较早样本5MB 完整下载,5.596 秒,893,509 B/s
Cloudflare 节点,较早样本45 秒仅收到 622,592 字节,13,833 B/s,超时
Cloudflare 节点,后续样本45.005 秒收到 564,423 字节,12,541 B/s,超时
Cloudflare 节点,物理网卡抓包样本45.014 秒收到 904,896 字节,20,102 B/s,超时
手机 Wi-Fi 经 Cloudflare 已无法正常连接时,电脑改为源站直连5MB 完整下载,7.946 秒,629,244 B/s

这些是不同时间的短时样本,不是长期带宽上限。最后一次对照尤其重要:故障时段内,Cloudflare 节点不可用,而直连仍能完整下载。

另外,固定 Cloudflare IPv4、IPv6 节点的早期样本也出现过连接超时。因此,不能只凭最近慢速连接使用 IPv6,就断言关闭 IPv6 必然解决问题。

2.2 服务器已经把数据交给 Cloudflare

一轮慢速下载期间,两次 ss -tinp 采样显示:

环节后一次采样的累计数据
测速目标 → XrayXray 收到 5,013,124 字节
Xray → Nginx发送并获确认 5,015,731 字节
Nginx → Cloudflare发送并获确认 5,045,195 字节

不同层包含不同协议开销,数值略有差异是合理的。关键是这条 Nginx 连接的 bytes_sent 与 bytes_acked 相等,发送队列为零,说明约 5MB 已经被 Cloudflare 的 TCP 对端确认接收。

这只能证明 Cloudflare 对端收到数据,不能证明 Windows 收到。 但它明显降低了本轮源站供数、回环转发和源站至 Cloudflare 传输作为主要瓶颈的可能性。

2.3 客户端抓包显示真实的接收缺口

后续在 Windows 物理网卡采集到 Cloudflare 与客户端的双向流量。其中,本机发出的 67 条目标连接包记录里,64 条包含 SACK 选项。

累计 ACK 多次停在同一位置,而 SACK 确认后续非连续数据已到达;部分较早序号的数据晚于后续数据数秒出现。这说明接收端反复遇到数据缺口,TCP 必须等待补齐才能按顺序交付上层。

这支持下行传输存在丢包、乱序或显著延迟,但必须保留边界:

  • 64/67 不是丢包率,只是这份采样中的 SACK 记录比例。
  • 采集仍存在遗漏,不能精确计算重传率,也不能证明每一个晚到数据包一定是重传。
  • 没有看到客户端零窗口,不支持简单归因为应用处理不及时导致接收窗口耗尽。
  • 抓包不能单独定位故障发生在哪台路由器,也不能证明特定网络干扰。

2.4 手机对照需要考虑时间变化

手机在 Wi-Fi 和移动数据上曾短时表现很快,随后在 Wi-Fi 上也变慢,甚至无法连接。同一时段,电脑源站直连仍正常。

因此,不能把最初“手机快、电脑慢”的短时结果直接当作 Windows 独有故障。Happy Eyeballs 会在多个地址间选择连接,设备也可能选中不同的地址族或边缘路径。

三、没有采用哪些修改,以及原因

修改方向本次未优先实施的原因
升级服务器资源快照无明显压力,直连仍能正常下载
重装 Xray 或更换协议相同服务通过直连能够正常传输
批量更改 TLS 参数已有成功握手与持续传输样本,无证据表明 TLS 配置导致低速
开启 BBRss 已显示连接使用 BBR
更改 MTU没有足够证据支持将其作为首要修复
全局禁用 IPv6尚未完成可靠的地址族对照,且早期 IPv4 固定地址也有失败样本
卸载虚拟网卡安装多个网卡不等于存在冲突,手机也出现故障
根据握手错误封禁来源错误未与测速连接关联,且 Nginx 同时承载多个站点

Nginx 原配置中的 HTTP/1.1、Upgrade、Connection 等 WS 转发要素齐全,已有 101 验证。成功升级后进入隧道转发模式,不能仅因未写 proxy_buffering off 就认定缓冲造成低速。

四、改造目标与配置变更范围

新链路计划为:

Text
客户端 → direct.example.com(仅 DNS,灰云,IPv4)
       → 源站 Nginx:443(Let’s Encrypt 证书)
       → 127.0.0.1:10000 Xray
       → 目标网站
项目变更
DNS新增 direct.example.com 的 A 记录,指向源站,关闭代理
地址族初期仅配置 A,不配置 AAAA,沿用已验证的 IPv4 直连路径
Nginx新增独立站点配置与软链接
TLS 证书为新域名签发 Let’s Encrypt 公开证书
续期Certbot 定时续期,成功后校验并 reload Nginx
客户端新建直连节点,地址、Host、SNI 改为新域名;验证证书
Xray继续使用现有 VLESS 入站、UUID 和 /ray 路径
原中转节点保留,供回滚与后续对照

公开证书负责解决身份验证与信任链问题,本身不会提升网络带宽。性能改善来自绕过异常中转路径。

五、实施步骤

以下是可复用的部署操作。先把所有 example.com 和示例 IP 换成自己的值。 每个代码块整体执行完,再执行下一块;报错先停止,不要继续引用不存在的证书。Linux 命令在 Ubuntu SSH 中执行,PowerShell 命令在 Windows 中执行。

5.1 新增仅 DNS 的 A 记录

在 Cloudflare 控制台新增:

字段示例
类型A
名称direct
内容203.0.113.10,执行时替换为源站 IPv4
代理状态仅 DNS,灰云
TTL自动

先不新增 AAAA。原中转域名保留原设置。

Windows PowerShell:

Powershell
Resolve-DnsName direct.example.com -Type A

结果应为实际源站 IPv4,而不是 Cloudflare 边缘地址。

5.2 检查工具并备份

Ubuntu SSH:

Bash
command -v certbot
sudo nginx -t

已有 Certbot 时直接复用,避免 apt、snap、pip 混装。若未安装,本文采用 Ubuntu apt 包:

Bash
sudo apt update
sudo apt install -y certbot

备份与创建验证目录:

Bash
sudo cp -a /etc/nginx "/root/nginx-backup-$(date +%Y%m%d-%H%M%S)"
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge

5.3 先配置 HTTP 验证入口

签发前不能引用还不存在的证书。先新增只有 80 端口的站点:

Bash
sudo tee /etc/nginx/sites-available/direct.example.com.conf > /dev/null <<'EOF'
server {
    listen 80;
    server_name direct.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        default_type text/plain;
    }

    location / {
        default_type text/plain;
        return 200 "direct endpoint ready\n";
    }
}
EOF

首次部署时创建软链接:

Bash
sudo ln -s /etc/nginx/sites-available/direct.example.com.conf /etc/nginx/sites-enabled/direct.example.com.conf
sudo nginx -t && sudo systemctl reload nginx
echo 'acme-ready' | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/check

如果软链接已存在,先确认目标正确,不要反复创建或盲目覆盖。

Windows PowerShell,直连检查 HTTP 验证入口:

Powershell
& "$env:SystemRoot\System32\curl.exe" -q --noproxy "*" "http://direct.example.com/.well-known/acme-challenge/check"

预期返回 acme-ready。这里 --noproxy "*" 用于直接检查源站入口,不用于代理性能测试。

HTTP-01 验证需要公网访问 TCP 80;业务需要 TCP 443。检查 Vultr 防火墙和 Ubuntu 防火墙的规则,不要直接关闭整个防火墙。若源站只允许 Cloudflare IP 回源,也需要为新直连用途调整访问规则。无法开放 80 时应改用自动化 DNS-01 验证,而不是反复重试。

5.4 签发证书

Ubuntu SSH:

Bash
sudo certbot certonly \
  --webroot \
  -w /var/www/letsencrypt \
  --cert-name direct.example.com \
  -d direct.example.com

按提示输入自己的通知邮箱,阅读并确认服务条款。成功后检查实际输出,预期证书路径为:

Text
/etc/letsencrypt/live/direct.example.com/fullchain.pem
/etc/letsencrypt/live/direct.example.com/privkey.pem

若签发失败,先排查 DNS、80 端口、挑战路径及已有 CAA 记录是否允许 Let’s Encrypt 签发。不要在证书不存在时进入 HTTPS 配置步骤。

5.5 加入 HTTPS 与 WS 转发

证书签发成功后,替换本次新增站点文件:

Bash
sudo tee /etc/nginx/sites-available/direct.example.com.conf > /dev/null <<'EOF'
server {
    listen 80;
    server_name direct.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        default_type text/plain;
    }

    location / {
        return 301 https://direct.example.com$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name direct.example.com;

    ssl_certificate /etc/letsencrypt/live/direct.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/direct.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    location = / {
        default_type text/plain;
        return 200 "OK\n";
    }

    location /ray {
        proxy_pass http://127.0.0.1:10000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 86400;
        proxy_redirect off;
    }
}
EOF

sudo nginx -t && sudo systemctl reload nginx

这份配置继续把 WS 请求交给本机 Xray。执行前应确认现有 Xray 没有额外限制只接受旧 Host;若配置了此类限制,需要相应兼容新域名。本会话未检查完整 Xray JSON,不能假定不存在自定义限制。

5.6 配置自动续期及重载

证书续期后,Nginx 需要重新加载才能使用新证书:

Bash
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx
EOF

sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

若使用本文 apt 安装方式:

Bash
sudo systemctl enable --now certbot.timer
sudo systemctl list-timers --all certbot.timer
sudo certbot renew --cert-name direct.example.com --dry-run
sudo /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

如果原来是 snap 或其他方式安装 Certbot,应检查其已有续期机制,不要机械新增第二套定时任务。默认的续期演练不一定执行 deploy hook,因此这里单独运行重载脚本进行检查。

HTTP-01 自动续期依赖 DNS、80 端口和验证目录继续可用。后续若关闭 80、迁移服务器或更换验证方式,需要同步调整续期配置。

5.7 验证证书与配置客户端

Windows PowerShell,不添加 -k:

Powershell
& "$env:SystemRoot\System32\curl.exe" -q --noproxy "*" -v "https://direct.example.com/"

预期证书验证通过并返回 OK。这一步只验入口和证书,最终仍需代理下载验证。

新建客户端节点:

参数新值
addressdirect.example.com
port443
protocolVLESS
UUID复用原有效 UUID,不公开
encryptionnone
flow空
networkws
WS Hostdirect.example.com
path/ray
TLS开启
SNIdirect.example.com
allowInsecure / 跳过证书验证false / 关闭
Mux保持关闭

将新节点设为活动节点并重载内核。保持 TUN 关闭,以显式代理验证:

Powershell
& "$env:SystemRoot\System32\curl.exe" -q --noproxy localhost --proxy socks5h://127.0.0.1:10808 -sS -o NUL --connect-timeout 10 --max-time 45 -w "code=%{http_code} total=%{time_total}s bytes=%{size_download} speed=%{speed_download}B/s\n" "https://speed.cloudflare.com/__down?bytes=5000000"

不要把代理目标放进 --noproxy,否则会绕过代理。curl 的 -4/-6 也不能用来控制 Xray 连接 VPN 节点的地址族。

验收至少包括:证书验证通过、客户端跳过验证关闭、5MB 下载完整完成,以及手机独立客户端访问正常。新节点的实际速度以部署后测试为准,不能把临时 IP 直连样本当作新域名节点的最终验收结果。

六、配置文件与后续变更清单

路径或位置用途何时需要检查
Cloudflare DNS 中的 direct A 记录直连指向更换服务器 IPv4
/etc/nginx/sites-available/direct.example.com.conf新直连站点更换域名、证书或 WS 路径
/etc/nginx/sites-enabled/direct.example.com.conf启用站点软链接启停或回滚站点
/var/www/letsencrypt/.well-known/acme-challenge/HTTP-01 验证修改 80 端口、验证目录或续期方式
/etc/letsencrypt/live/direct.example.com/当前证书与私钥链接排查证书路径和有效期
/etc/letsencrypt/renewal/direct.example.com.confCertbot 续期参数修改验证方式或 webroot
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh续期成功后的重载修改 Nginx 或服务命令位置
certbot.timerapt 版本定时续期检查定时任务是否启用
Windows、手机的新节点address、Host、SNI、验证选项更换域名或协议参数

常用只读检查命令:

Bash
sudo nginx -t
sudo certbot certificates
sudo systemctl list-timers --all certbot.timer
sudo journalctl -u certbot.service -n 50 --no-pager

live/ 下面通常是指向 archive/ 的软链接,迁移证书不能只拷贝几个可能失效的链接。更换服务器时优先重新签发;如需迁移完整 ACME 状态,应安全转移必要目录与权限,不在博客附件中公开。

七、回滚方式

新站点与旧中转站点分开,回滚不需要恢复整个 Nginx 目录。

  1. 客户端切回原节点。注意原 Cloudflare 路径仍可能异常,配置回滚不保证线路恢复。
  2. 如需停用新站点,仅移除其启用软链接:
Bash
sudo unlink /etc/nginx/sites-enabled/direct.example.com.conf
sudo nginx -t && sudo systemctl reload nginx
  1. 保留 sites-available 文件、证书和 DNS,便于再次启用。不要删除其他站点或直接清空 /etc/letsencrypt。
  2. 若确定永久停用,再有计划地删除 DNS 和对应证书。停用期间 HTTP-01 续期可能失败,应同步处理该证书的续期安排。

再次启用:

Bash
sudo ln -s /etc/nginx/sites-available/direct.example.com.conf /etc/nginx/sites-enabled/direct.example.com.conf
sudo nginx -t && sudo systemctl reload nginx

八、排障工具与记录中的注意事项

Windows 上曾有异常旧 curl 或 PowerShell 别名,因此统一显式调用系统 curl。抓包需要管理员 PowerShell;每个代码块依次执行即可,pktmon start 成功返回后会在后台继续采集。

本次记录中有几类不能作为网络结论的工具问题:

  • Start-Process -ArgumentList 未正确保留带空格的测速格式参数,造成 Bad hostname 和结果文件混入正文。最终已改为直接调用 curl,并用 --stderr 保存诊断。
  • PowerShell 的 NativeCommandError 可能只是对原生程序 stderr 的包装,不能仅凭这个名称认定请求失败。
  • 0 字节 pcapng 或不存在的 ETL 表示没有可用采集数据,不代表链路没有流量。
  • 多个组件记录同一包会形成重复项,不能直接当作网络重传。
  • pktmon etl2txt --stats-only 在本机将统计输出到终端,没有生成预期文件;打包前需要确认文件实际存在。
  • 测速目标返回的 CF-RAY ...-ORD 和 server-timing 属于源站访问测速服务这一段,不能拿来判断客户端连接 VPN 域名时进入的边缘机房。

九、结论与后续维护原则

本次最强证据是:Cloudflare 节点在多个样本中反复低速或超时,而相同服务、相同目标通过源站直连能完整下载;服务器曾将约 5MB 数据交给 Cloudflare 并获完整确认,客户端则在另一轮慢速样本中报告大量接收缺口。

据此,将恢复使用的重点放在绕过异常中转路径是合理的;但不能宣称已经定位某家运营商、具体丢包节点或网络干扰来源。

后续维护遵循三点:保留可比较的旧节点;每次只改变少量变量;在同一故障时段使用真实下载验证,而不依赖 ping、小页面或连接延迟作为吞吐结论。

公网直连会直接使用源站地址,不再经过 Cloudflare 代理保护。当前服务器本就已用于直连诊断,新域名上线后仍应保持必要端口规则、更新和日志监控。公开受信任证书解决信任问题,不能替代这些基础维护。

初次部署文档:https://www.rendering.me/blog/YNOxge

参考资料

感谢读到这里

日志还会继续,欢迎常来。

返回首页