Files
tamabox/outputs/tutorial-dual-cdn-split-routing.md
T
2026-09-08 10:10:44 +08:00

315 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 以狼的提问箱为例-免备案双 CDN 分流实战教程:境内走 EdgeOne、境外走 Cloudflare
> **教程性质**:
> 本文以 狼的提问箱 项目为例,但所有原理与步骤对任何「不备案、有海外用户、想让境内/境外各走最优线路」的站点通用。
>
> 场景速览:域名权威 DNS 托管在 **DNSPod**(免费版支持按「境内 / 境外 / 默认」线路返回不同解析结果);境内线路 CNAME 到**腾讯 EdgeOne 国际站**(免备案区域,大陆用户落到香港等最近节点,走腾讯跨境骨干,比 Cloudflare 免费版大陆绕行明显更快);境外/默认线路 CNAME 到 **Cloudflare 优选 IP**(经 Cloudflare for SaaS 自定义主机名识别流量,由既有 Cloudflare Tunnel 回源)。
---
## 1. 为什么能这么搭 —— 三个关键认知
整套方案成立的前提,是先把三个「概念解耦」想清楚:
### 1.1 Cloudflare for SaaS:让「流量识别」与「DNS 解析权」解耦
Cloudflare 普通接入(Free 套餐)要求把整域 NS 迁到 Cloudflare,这与「DNS 要放在 DNSPod 做分线路」**天然冲突**——NS 在谁手里,谁才说了算。
Cloudflare for SaaS(免费额度 100 个自定义主机名)绕开这个限制:
- 你有一个**承载 zone**(例如 `cf-zone.example.net`,托管在 Cloudflare);
- 在承载 zone 里把目标域名(`box.example.com`)加为 **Custom Hostname**,并验证归属;
- Cloudflare 边缘从此「认识」这个域名:**任何带着 `Host: box.example.com` 到达 CF 边缘的请求**,都会被匹配证书、按配置路由(本项目为 Tunnel 回源)。
关键结论:**CF 侧只负责"来了流量怎么认、怎么转",不负责"流量怎么来"**。流量能不能到 CF 边缘,取决于你自己的 DNS(DNSPod)把境外用户解析到哪。所以「境外走 CF」必须在 DNSPod 配一条境外线路指向 CF 的 IP/CNAME——CF 自己不会拉客。
### 1.2 DNSPod 分线路:免费就能「同一域名,不同答案」
腾讯云解析(DNSPod)免费版就支持按查询来源返回不同记录:线路可选「中国境内 / 境外 / 默认」,境内细分到省、运营商。**默认线路是兜底**,任何未命中的查询都走它。
要理解一个易错点:**分线路的判定发生在 DNSPod 的权威服务器上**,依据是「递归 DNS 服务器的出口 IP 归属地」——所以「境外用户走境外线路」成立的前提是境外用户的递归 DNS(8.8.8.8 等)能真的问到 DNSPod 权威,且没被中间网络劫持/污染(第 6 节有实战案例)。
### 1.3 EdgeOne 国际站:选对「加速区域」就不用备案
EdgeOne 分「中国大陆可用区 / 全球可用区(不含中国大陆)/ 海外可用区」等区域配置。官方口径:
- 选 **全球可用区(不含中国大陆)** 或海外区域 → **不要求 ICP 备案**;
- 选中国大陆区域 → 必须备案。
不备案的代价:大陆用户**没有本地节点**,请求落到最近的海外节点(大陆一般落香港,也有日韩/新加坡等),由腾讯跨境骨干回源。实测体验通常仍明显优于 Cloudflare 免费版大陆线路(后者常绕美/日且抖动大)——所以「不备案也选 EO 当境内线路」是有实测前提的,切换前建议先测速对比。
> 补充:Cloudflare for SaaS 启用通常需要账户先绑定支付方式,额度内不扣费。
---
## 2. 目标架构
```
DNS 层 DNSPod(example.com 整域权威,免费版支持按线路返回)
NS = byron.dnspod.net / sunshine.dnspod.net(官方 NS)
box CNAME 记录
├─ 境内 ──▶ box.example.com.edgeone.example (EdgeOne 接入目标)
├─ 境外 ──▶ cf-pref.example.net (CF 优选 IP 域名)
└─ 默认 ──▶ cf-pref.example.net (兜底,语义同境外)
┌──────────────┴──────────────────────────────┐
大陆用户 ▶▶(命中境内线路) ◀◀ 海外用户(命中境外/默认线路)
▼ ▼
EdgeOne 国际站 Cloudflare 边缘
区域: 全球(不含中国大陆) 优选 IP: 198.51.100.x(示例段)
证书: TrustAsia, CN=box.example.com 识别: for SaaS 自定义主机名
回源: EO 直连源站公网端口 回源: Cloudflare Tunnel(纯出站)
│ │
└──────────────────┬──────────────────────────┘
▼
源站防火墙 iptables+ipset(仅放行 EO 回源网段;cloudflared 走 127.0.0.1 不受影响)
▼
HTTP 应用直接监听公网高位端口(本项目 TamaBox,无 nginx)
```
两条回源路径互不干扰:EO 是 **入站直连**(需开放端口 + 白名单锁定),CF 是 **Tunnel 出站**(无需任何入站规则)。
---
## 3. 前置条件清单
| # | 条件 | 说明 |
|---|---|---|
| 1 | 一个域名(`example.com`),权威 DNS 托管在 DNSPod | 免费版即可;注册局委派的 NS = byron/sunshine.dnspod.net |
| 2 | 一个 Cloudflare 账户 + 承载 zone(`cf-zone.example.net`) | 用于 for SaaS 与 Tunnel;**域名本身的 DNS 不用迁给 CF** |
| 3 | cloudflared Tunnel 已在跑 | 服务器 → CF 纯出站连接,绑定承载域 `box.cf-zone.example.net` |
| 4 | 服务器有公网 IP,且能开一个高位入站端口 | 给 EO 回源用;**不需要 nginx**,应用直接监听即可 |
| 5 | EdgeOne 账户(本例国际站 edgeone.ai) | 选免备案区域 |
| 6 | CF 账户已绑支付方式 | 启用 for SaaS 的前提(额度内不扣费) |
| 7 | 服务器 1C1G 级别即可 | iptables/ipset 是内核规则,无常驻进程 |
---
## 4. 分步实施
> 建议顺序:**先 CF(步骤 A)→ 再 EO(步骤 B)→ 再 DNS(步骤 C)→ 最后服务器(步骤 D)**。
> 前两步做完,流量仍在旧路径上,随时可暂停;步骤 C/D 才真正切换与收敛。
### Step A —— Cloudflare 侧:SaaS 自定义主机名 + Tunnel(本次零改动,讲解原理)
本项目 `box.example.com` 的 SaaS 自定义主机名此前已配好(放在承载 zone `cf-zone.example.net` 下),Tunnel 已正常回源。若从零配置,动作是:
1. 承载 zone `cf-zone.example.net` → **SSL/TLS → Custom hostnames** → Add custom hostname,填 `box.example.com`;
2. 按提示完成归属验证(加一条 TXT 或 CNAME 验证记录);
3. 在 Tunnel 的路由里把 `box.example.com` 指到本地 HTTP 服务(`http://127.0.0.1:端口`);
4. 记录页里能拿到该主机名的 **CNAME target**(形如 `<hash>.cdn.cloudflare.net`)——它是「把流量引到 CF 边缘」的官方入口。
> ⚠️ 本项目实际没用 SaaS CNAME target,而是用**优选 IP CNAME(`cf-pref.example.net`)**:一个第三方维护的、解析到 CF 低延迟边缘 IP 的域名。原理相同——解析结果落在 CF 边缘,CF 靠 `Host: box.example.com` 识别出这是 SaaS 自定义主机名流量再按 Tunnel 路由。**优选域名的可用性依赖第三方维护,失效时换回官方 CNAME target 即可**(第 7 节维护清单)。
>
> 本项目 DNSPod 上境外/默认线路指向的是 `cf-pref.example.net`。
### Step B —— EdgeOne 侧:添加站点(CNAME 接入,免备案区域)
1. EO 控制台 → 添加站点 → 填 `box.example.com`;
2. **接入方式选 CNAME 接入**(⚠️ 不要选 NS 接入——NS 接入会要求把域名托管给 EO,与 DNSPod 分线路冲突,和「DNS 在 CF 手里没法分线路」是同一个问题);
3. **加速区域选「全球可用区(不含中国大陆)」**(免备案的关键选择);
4. 归属验证:按 EO 提示添加验证记录(本项目用 TXT:`edgeonereclaim.example.com TXT ...`),**记录加在 DNSPod 的默认线路**(CA/验证服务器多在境外,放默认线路全球可见最稳);
5. 配置回源:源站地址 = 你的公网 IP + 高位端口,Host 头 = `box.example.com`;回源协议按你的源站监听协议选(应用直监听 HTTP 就选 HTTP,TLS 由 EO 边缘终结);
6. 免费证书:CNAME 验证通过 + 回源配置就绪后,EO 自动签发(本项目 TrustAsia,`CN=box.example.com`)。签发需要几分钟到几小时——**在证书签发完成前,443 会拿 `*.cdn.myqcloud.com` 兜底证书应答**,浏览器会出现证书/HSTS 报错(第 6 节排障 6.1),属正常过程,等签发即可;
7. **开启「客户端 IP 头」**(关键,默认关闭!):站点 → 网络优化 / 规则引擎 → 客户端 IP 头部 → 开启,**自定义头部名必须填 `EO-Client-IP`**——应用侧固定读取这个名字;
8. **删除/改写伪造的 CF 头**:在 EO 加一条规则引擎规则,把客户端可能携带的 `CF-Connecting-IP` / `X-Real-IP` 等头删掉——否则大陆攻击者经 EO 带假 CF 头,会被应用当成 CF 流量优先信任(头信任模型见 Step D)。
### Step C —— DNSPod 分线路:三条记录
权威 DNS 在 DNSPod(整域 zone `example.com`),所以**直接在整域 zone 里配,不需要任何子域委派**。
| 主机记录 | 类型 | 线路 | 记录值 | TTL |
|---|---|---|---|---|
| `box` | CNAME | **中国境内** | `box.example.com.edgeone.example`(EO 分配的目标) | 120~600 |
| `box` | CNAME | **境外** | `cf-pref.example.net`(CF 优选 / 或 SaaS CNAME target) | 600 |
| `box` | CNAME | **默认** | `cf-pref.example.net`(兜底,语义同境外) | 600 |
要点:
- **EO 归属验证 TXT 保持「默认」线路**(全球可见),别只放境内;
- 若之前误把 EO 的 CNAME 建在「默认」线路,会导致全球都走 EO——把它改成「中国境内」,境外/默认补 CF 两条;
- TTL 别太大,方便回滚与换优选域时快速生效。
### Step D —— 服务器侧:应用监听 + 信任头 + 端口锁定
#### D1 应用直接监听公网端口(免 nginx)
本项目 TamaBox 自带 HTTP 服务器,改配置即可:
```ini
[server]
listen = 0.0.0.0:18080 ; 从仅本机 127.0.0.1 改为全网卡,EO 才能远程连入(18080 仅为示例端口)
```
- cloudflared 连的是 `127.0.0.1:端口`,`0.0.0.0` 监听兼容回环连入,**Tunnel 分支不受影响**;
- nginx 在本方案中从头到尾不是必需——防火墙(iptables/ufw)是内核规则,零常驻进程,比 nginx 轻得多,适合 1C1G 小机器。
#### D2 应用信任两家 CDN 的权威头
`conf/app.ini`:
```ini
[security]
trust_proxy = true
```
配合代码层支持(本项目 `internal/shared/ip.go` 的 `ResolveClientIP`),信任顺序:
1. `CF-Connecting-IP`(Cloudflare 权威头,海外分支);
2. `EO-Client-IP`(EdgeOne 权威头,境内分支);
3. 兜底:TCP 对端 RemoteAddr。
> ⚠️ **头信任的安全前提(缺一不可)**:
> 1. 源站防火墙**只放行两家 CDN 的回源来源**(EO 直连需要;CF 分支天然安全——Tunnel 是出站,外人到不了源站);
> 2. EO 侧已删除伪造的 CF 头(Step B-8);
> 3. 若还有 CF 侧直连,同样在 CF 规则里删除伪造的 EO 头。
>
> 任一缺失,「信任头」就会被攻击者直连源站 + 自带假头绕过——限流/封禁会拿到伪造 IP。
#### D3 端口白名单:iptables + ipset 自动维护(EO 免费档没有官方回源段)
EO 官方回源清单由「源站防护 / Origin ACL」提供(配套 `DescribeOriginACL` API),**但该功能只支持付费套餐**——免费档在控制台找不到入口,也没有可拉取的清单 URL。
> 🔴 **免费档方案定稿(2026-09-03 实测)**:**不要**逐个收集 EO 出口 IP 做白名单——实测发现 EO 回源出口是**海量轮换节点池**(一次 23 秒抓包就出现 13 个不同出口、散布多个腾讯云 /16 段),逐 IP 白名单必然间歇漏放 → 境内访问偶发 **521**(EO 边缘回源失败)。正解 = 直接放行 EO 所属的**腾讯云大段**(CIDR 整段入清单,脚本原生支持)。实测生效清单见下。
交付脚本 `scripts/edgeone-origin-whitelist.sh`(bash,需 root,架构要点):
- 白名单放 **ipset 集合** `EO_ORIGIN`,iptables 一条规则引用它:`自建链 EO_IN` 内「命中集合放行 → 其余 REJECT tcp-reset」。INPUT 层规则默认**只对公网网卡 eth0 生效**(`EO_IFACE` 可配,网卡名不同必改否则端口裸奔);cloudflared 本机回连走独立的 `-i lo` ACCEPT 放行,与网卡限定解耦;
- 更新走「临时集填充 → `ipset swap` 原子替换」:**同步全程无规则清空窗口**;
- **fail-safe**:拉取失败 / 清单为空 / 全非法 → 拒绝更新、保留现网,绝不误删导致端口裸奔;
- 命令:`sync`(周期入口)/ `auto`(采集当前回源 IP → 新段预警)/ `learn`(即时观测)/ `show` / `flush`(回滚)/ `--dry-run`;
- 清单来源:本地文件(`/etc/edgeone-origin/cidr.txt`)+ 可选 URL,取并集。脚本支持每行一个 IP 或 CIDR。
落地四步:
```bash
# 1. 改端口(默认 18080 → 你的实际端口)
# 2. 安装 ipset
apt-get install -y ipset
# 3. 建清单:放行腾讯云 EO 大段(每行一个 CIDR,# 注释;以下为实测生效配置)
mkdir -p /etc/edgeone-origin
cat > /etc/edgeone-origin/cidr.txt <<'EOF'
43.146.0.0/16
43.152.0.0/16
43.168.0.0/16
43.174.0.0/16
43.175.0.0/16
EOF
# 段哪来的?用脚本 auto / tcpdump 持续观察回源出口,把出现的 IP whois 归并到所属腾讯云段
# 注意:种子/采样是 EO 的"出口 IP",不是应用日志里的客户端 IP(日志里是 EO-Client-IP,
# 客户端真实 IP,不能当白名单)!段级方案下无需逐个采种,新段靠 auto 月度预警
bash edgeone-origin-whitelist.sh sync --dry-run
bash edgeone-origin-whitelist.sh sync
# 4. 验证:外网机器 nc -zv <IP> 18080 应 refused;境内访问 https://box.example.com 应正常(无 521)
# 5. 挂定时:cron「17 4 */3 * *」或 systemd timer 每 3 天一次 sync(EO 段以月计才变)
```
> 日后升级付费套餐:把 `DescribeOriginACL` API 的输出写入 `cidr.txt` 即可,脚本零改动。
---
## 5. 验证方法(含大陆 DNS 劫持陷阱)
### 5.1 分线路是否生效(🔴 重点:别在大陆本机 nslookup 境外 DNS)
大陆网络对发往境外公共 DNS(8.8.8.8 / 1.1.1.1 / 9.9.9.9 / OpenDNS,**UDP 53 和 TCP 53 都会被劫持**)的查询会返回**按境内线路伪造的答案**——在本机 `nslookup box.example.com 8.8.8.8` 永远显示 EO,即使境外线路早已配好。正确验证通道:
```bash
# 境内视角(本机直接查即可,通道可信)
nslookup box.example.com 223.5.5.5 # 期望 → CNAME edgeone.example → 203.0.113.x(EO 示例段)
nslookup box.example.com 119.29.29.29 # 同上(DNSPod 自家 DNS)
# 境外视角:用「境外服务器代为查询」,浏览器直接打开下面两个 API 之一
# https://api.hackertarget.com/dnslookup/?q=box.example.com
# https://networkcalc.com/api/dns/lookup/box.example.com
# 期望 → CNAME cf-pref.example.net → 198.51.100.x(CF 边缘 IP 示例段)
```
两条结果**不一样 = 分线路生效**。
### 5.2 HTTPS 证书是否就绪
```bash
echo | openssl s_client -connect box.example.com:443 -servername box.example.com \
| openssl x509 -noout -subject -issuer -dates
# 期望 subject = CN=box.example.com(而不是 *.cdn.myqcloud.com 兜底证书)
```
### 5.3 真实 IP 是否被正确识别
境内访问后看源站/EO 访问日志中的客户端 IP = 你的大陆公网出口 IP(说明 `EO-Client-IP` 生效);海外访问后同理应为你的海外出口 IP(`CF-Connecting-IP`)。
---
## 6. 今日排障实录(每个坑都值得记)
### 6.1 「网站使用 HSTS」报错 = EO 证书还没签发(不是配置错)
现象:DNS 切到 EO 后浏览器报 HSTS 错误页。
原因链:之前走 CF HTTPS 时应用下发了 `Strict-Transport-Security: max-age=31536000; includeSubDomains`(按 ExternalURL 以 https 开头就对每个响应下发)→ 浏览器记录「一年内必须 HTTPS」→ 切到 EO 后证书尚未签发、443 拿 `*.cdn.myqcloud.com` 兜底证书 → 证书与域名不匹配 → HSTS 错误页。
处理:等 EO 证书签发(几分钟~几小时)即可,**域名不在 HSTS preload 列表,浏览器自动恢复,无需清缓存**。紧急回滚 = DNS 改回 CF。
(若在 preload 列表里才是真麻烦——需先修好证书再等 preload 移除,故平时别主动提交 preload。)
### 6.2 误判「权威 DNS 在 Cloudflare」(方向性错误)
现象:一度以为 `example.com` 权威在 Cloudflare,推导出「需要子域 NS 委派」的复杂方案,差点让用户在 CF 做一堆不存在 zone 的操作。
根因:apex `example.com` 本身是一条 CNAME → `your-project.pages.dev`(Cloudflare Pages 项目)——nslookup 查 NS 时**一路跟随 CNAME 链**走到 pages.dev 的 NS(CF),被误读为「example.com zone 在 CF」。
正确做法:**查域名权威必须问注册局(RDAP)或直查 SOA**,不能信递归 CNAME 链:
```bash
# RDAP(注册局铁证):https://rdap.org/domain/example.com → 应返回 byron/sunshine.dnspod.net
# 或直查 SOA:nslookup -type=SOA example.com byron.dnspod.net
```
用户一句「我的 Cloudflare 根本没有 example.com zone」就是最有效的纠偏信号——**控制台里没有的 zone,DNS 上不可能归它管**。
### 6.3 「境外也返回 EO」的假象 = 大陆 DNS 劫持(连 TCP 53 都劫)
现象:从大陆本机用 nslookup 测 8.8.8.8 / 9.9.9.9 / OpenDNS(UDP 和 TCP 都试),境外视角全部返回 EO,一度以为分线路没生效、甚至怀疑 DNSPod 配置错了。
真相:大陆网络劫持出境 DNS 查询,返回按境内线路伪造的 EO 答案,请求根本没到境外服务器。
破案手段:用**境外服务器**(hackertarget / networkcalc API)代为查询,立刻看到 CF 答案——DNSPod 三线路配置完全正确,从未出过错。教训见 5.1。
### 6.4 残留的阿里云验证记录
实测发现 DNSPod 上有历史残留 `_dnsauth.box.example.com CNAME → xxxxxxxx...aliyunauth.certificate-validations.com`(以前阿里云申请证书留下的)。`_dnsauth` / `_acme-challenge` 下的旧验证记录会干扰证书自动续期验证——**证书验证记录的坑**,建议确认证书正常后删除。
### 6.5 免费档没有「源站防护」,回源 IP 段拿不到
见 Step D3:免费档无官方清单,用 learn 观察法兜底;付费后切 `DescribeOriginACL`,脚本零改动。
---
## 7. 日常维护清单
| 周期 | 事项 |
|---|---|
| 证书到期前 | EO 免费证书自动续期并**重新做域名验证**——⚠️ CA 验证服务器多在境外,**所有验证记录务必放默认线路**;到期前留意 EO 控制台证书状态,失败会提示重新验证 |
| 每 3 天(定时任务) | 白名单 sync(EO 回源段 3~6 个月才变,每 3 天足够);sync 是全量重建,清单里删掉的旧 IP 会自动剔除 |
| 每月 | 抽查清单有无漏网的新回源 IP(auto 已每日自动补种,此处仅兜底核对);确认优选域名 `cf-pref.example.net` 仍解析到 CF 边缘(失效则换回官方 SaaS CNAME target,只改 DNSPod 一条记录) |
| EO 回源突然失败 | 先临时放行全段定位,再按 learn 结果收紧——EO 免费档收不到段变更通知,要有这个预案 |
| 长期 | 若升级 EO 付费套餐:启用源站防护拿官方段 → 接入 DescribeOriginACL 输出到 cidr.txt |
---
## 8. 回滚
1. **快速回滚(境外也要回 CF 之外的兜底)**:DNSPod 把「境内」线路改指向 CF(或删掉境内线路,让默认兜底接管)→ 全球走 CF,分钟级生效;
2. **彻底回滚(回到纯 CF 时代)**:删除 EO 站点/记录、撤防火墙放行、DNSPod 三线路合并为默认→CF;
3. EO 证书到期前回滚,记得去 EO 控制台删除站点避免残留验证记录干扰。
---
## 附:配置快照(示例,2026-09-03 实测定稿,域名已脱敏)
```
域名: box.example.com(权威 DNS: DNSPod,NS=byron/sunshine.dnspod.net)
境内: CNAME box.example.com.edgeone.example → 203.0.113.x (EdgeOne 国际站,区域不含大陆,示例段)
境外/默认: CNAME cf-pref.example.net → 198.51.100.x (CF 优选 IP,示例段)
CF 侧: SaaS 自定义主机名(承载 zone cf-zone.example.net)+ Tunnel 出站回源
EO 证书: TrustAsia, CN=box.example.com(签发后约 90 天到期,自动续期)
EO 归属验证: edgeonereclaim.example.com TXT(DNSPod 默认线路 ✅)
EO 客户端头: EO-Client-IP(手动开启,头名固定 EO-Client-IP)
源站: TamaBox 直监听 0.0.0.0:高位端口(无 nginx);iptables+ipset 白名单
应用信任: trust_proxy=true;ResolveClientIP 信任 CF-Connecting-IP → EO-Client-IP → RemoteAddr
```