自建 VPN 排障与改造:从 Cloudflare 中转切换到受信任证书直连
cloudflare, Let's Encrypt
cloudflare, Let's Encrypt
这次排障起因是自建 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。
原链路为:
客户端 → vpn.example.com(Cloudflare 橙云)
→ 源站 Nginx:443
→ 127.0.0.1:10000 Xray
→ 目标网站
原客户端关键参数:
| 参数 | 值 |
|---|---|
| 地址 | vpn.example.com |
| 端口 | 443 |
| 协议 | VLESS |
| encryption | none |
| flow | 空 |
| 传输 | ws |
| WS Host | vpn.example.com |
| WS path | /ray |
| TLS | 开启 |
| SNI | vpn.example.com |
| allowInsecure | false |
| Mux | 关闭 |
Cloudflare 使用 Full (strict) 模式,源站 Nginx 配置的是 Cloudflare Origin CA 证书。这种证书适合 Cloudflare 回源验证,不等于普通客户端默认信任的公开证书。
服务器上的 Nginx 还承载博客、CMS 和密码管理服务,共用 443 端口。因此,不能把全局错误日志中的所有 TLS 错误都归到 VPN 节点。
早期测试说明,以下环节能够工作:
/ray 的标准 WebSocket Upgrade 请求,在 Cloudflare 和直连路径均可返回 101。127.0.0.1:10000,Nginx 监听公网 443,与架构一致。这些结果验证了基础可达性和协议入口,但不能证明大流量传输正常。未发送有效 VLESS 数据时,WS 握手后的空闲超时也不能作为故障证据。
服务器资源快照为:负载约 0.10 / 0.15 / 0.11,可用内存约 1.1GiB。采样时没有明显资源压力,但这不是对所有时间段的保证。
主要使用同一个 5,000,000 字节测速资源:
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 必然解决问题。
一轮慢速下载期间,两次 ss -tinp 采样显示:
| 环节 | 后一次采样的累计数据 |
|---|---|
| 测速目标 → Xray | Xray 收到 5,013,124 字节 |
| Xray → Nginx | 发送并获确认 5,015,731 字节 |
| Nginx → Cloudflare | 发送并获确认 5,045,195 字节 |
不同层包含不同协议开销,数值略有差异是合理的。关键是这条 Nginx 连接的 bytes_sent 与 bytes_acked 相等,发送队列为零,说明约 5MB 已经被 Cloudflare 的 TCP 对端确认接收。
这只能证明 Cloudflare 对端收到数据,不能证明 Windows 收到。 但它明显降低了本轮源站供数、回环转发和源站至 Cloudflare 传输作为主要瓶颈的可能性。
后续在 Windows 物理网卡采集到 Cloudflare 与客户端的双向流量。其中,本机发出的 67 条目标连接包记录里,64 条包含 SACK 选项。
累计 ACK 多次停在同一位置,而 SACK 确认后续非连续数据已到达;部分较早序号的数据晚于后续数据数秒出现。这说明接收端反复遇到数据缺口,TCP 必须等待补齐才能按顺序交付上层。
这支持下行传输存在丢包、乱序或显著延迟,但必须保留边界:
手机在 Wi-Fi 和移动数据上曾短时表现很快,随后在 Wi-Fi 上也变慢,甚至无法连接。同一时段,电脑源站直连仍正常。
因此,不能把最初“手机快、电脑慢”的短时结果直接当作 Windows 独有故障。Happy Eyeballs 会在多个地址间选择连接,设备也可能选中不同的地址族或边缘路径。
| 修改方向 | 本次未优先实施的原因 |
|---|---|
| 升级服务器 | 资源快照无明显压力,直连仍能正常下载 |
| 重装 Xray 或更换协议 | 相同服务通过直连能够正常传输 |
| 批量更改 TLS 参数 | 已有成功握手与持续传输样本,无证据表明 TLS 配置导致低速 |
| 开启 BBR | ss 已显示连接使用 BBR |
| 更改 MTU | 没有足够证据支持将其作为首要修复 |
| 全局禁用 IPv6 | 尚未完成可靠的地址族对照,且早期 IPv4 固定地址也有失败样本 |
| 卸载虚拟网卡 | 安装多个网卡不等于存在冲突,手机也出现故障 |
| 根据握手错误封禁来源 | 错误未与测速连接关联,且 Nginx 同时承载多个站点 |
Nginx 原配置中的 HTTP/1.1、Upgrade、Connection 等 WS 转发要素齐全,已有 101 验证。成功升级后进入隧道转发模式,不能仅因未写 proxy_buffering off 就认定缓冲造成低速。
新链路计划为:
客户端 → 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 中执行。
在 Cloudflare 控制台新增:
| 字段 | 示例 |
|---|---|
| 类型 | A |
| 名称 | direct |
| 内容 | 203.0.113.10,执行时替换为源站 IPv4 |
| 代理状态 | 仅 DNS,灰云 |
| TTL | 自动 |
先不新增 AAAA。原中转域名保留原设置。
Windows PowerShell:
Resolve-DnsName direct.example.com -Type A
结果应为实际源站 IPv4,而不是 Cloudflare 边缘地址。
Ubuntu SSH:
command -v certbot
sudo nginx -t
已有 Certbot 时直接复用,避免 apt、snap、pip 混装。若未安装,本文采用 Ubuntu apt 包:
sudo apt update
sudo apt install -y certbot
备份与创建验证目录:
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
签发前不能引用还不存在的证书。先新增只有 80 端口的站点:
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
首次部署时创建软链接:
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 验证入口:
& "$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 验证,而不是反复重试。
Ubuntu SSH:
sudo certbot certonly \
--webroot \
-w /var/www/letsencrypt \
--cert-name direct.example.com \
-d direct.example.com
按提示输入自己的通知邮箱,阅读并确认服务条款。成功后检查实际输出,预期证书路径为:
/etc/letsencrypt/live/direct.example.com/fullchain.pem
/etc/letsencrypt/live/direct.example.com/privkey.pem
若签发失败,先排查 DNS、80 端口、挑战路径及已有 CAA 记录是否允许 Let’s Encrypt 签发。不要在证书不存在时进入 HTTPS 配置步骤。
证书签发成功后,替换本次新增站点文件:
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,不能假定不存在自定义限制。
证书续期后,Nginx 需要重新加载才能使用新证书:
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 安装方式:
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、迁移服务器或更换验证方式,需要同步调整续期配置。
Windows PowerShell,不添加 -k:
& "$env:SystemRoot\System32\curl.exe" -q --noproxy "*" -v "https://direct.example.com/"
预期证书验证通过并返回 OK。这一步只验入口和证书,最终仍需代理下载验证。
新建客户端节点:
| 参数 | 新值 |
|---|---|
| address | direct.example.com |
| port | 443 |
| protocol | VLESS |
| UUID | 复用原有效 UUID,不公开 |
| encryption | none |
| flow | 空 |
| network | ws |
| WS Host | direct.example.com |
| path | /ray |
| TLS | 开启 |
| SNI | direct.example.com |
| allowInsecure / 跳过证书验证 | false / 关闭 |
| Mux | 保持关闭 |
将新节点设为活动节点并重载内核。保持 TUN 关闭,以显式代理验证:
& "$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.conf | Certbot 续期参数 | 修改验证方式或 webroot |
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh | 续期成功后的重载 | 修改 Nginx 或服务命令位置 |
certbot.timer | apt 版本定时续期 | 检查定时任务是否启用 |
| Windows、手机的新节点 | address、Host、SNI、验证选项 | 更换域名或协议参数 |
常用只读检查命令:
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 目录。
sudo unlink /etc/nginx/sites-enabled/direct.example.com.conf
sudo nginx -t && sudo systemctl reload nginx
sites-available 文件、证书和 DNS,便于再次启用。不要删除其他站点或直接清空 /etc/letsencrypt。再次启用:
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 保存诊断。NativeCommandError 可能只是对原生程序 stderr 的包装,不能仅凭这个名称认定请求失败。pktmon etl2txt --stats-only 在本机将统计输出到终端,没有生成预期文件;打包前需要确认文件实际存在。CF-RAY ...-ORD 和 server-timing 属于源站访问测速服务这一段,不能拿来判断客户端连接 VPN 域名时进入的边缘机房。本次最强证据是:Cloudflare 节点在多个样本中反复低速或超时,而相同服务、相同目标通过源站直连能完整下载;服务器曾将约 5MB 数据交给 Cloudflare 并获完整确认,客户端则在另一轮慢速样本中报告大量接收缺口。
据此,将恢复使用的重点放在绕过异常中转路径是合理的;但不能宣称已经定位某家运营商、具体丢包节点或网络干扰来源。
后续维护遵循三点:保留可比较的旧节点;每次只改变少量变量;在同一故障时段使用真实下载验证,而不依赖 ping、小页面或连接延迟作为吞吐结论。
公网直连会直接使用源站地址,不再经过 Cloudflare 代理保护。当前服务器本就已用于直连诊断,新域名上线后仍应保持必要端口规则、更新和日志监控。公开受信任证书解决信任问题,不能替代这些基础维护。
日志还会继续,欢迎常来。
返回首页