Files
vpsetup/docs/cdn-list-audit.md
eli ca5d8ee84a refactor: CDN 清单生成器移交 gc-toolkit/cdn-list
这份清单存在的唯一原因是 GCP 把 CDN 出站单独计价(Carrier Peering,$0.08/GiB 从
第一字节起算、无 200 GiB 免费额度),而消费它的 VPC 拒绝规则 cdn-egress-deny 本身
也是 GCP 资源。所以生成器归 gc-toolkit 更合理。

- 删掉本仓库的 tools/refresh-cdn-lists.sh(功能已合并进 gc-toolkit,避免两份分叉)
- 脚本头部与 docs/cdn-list-audit.md 改成指路 gc-toolkit
- 清单内容本次无变化:v4 271 条 / v6 193 条,与 gc-toolkit 生成的 canonical 文件一致

迁移后 gc-toolkit 那边多了两件事:生成 canonical 的 cdn-cidrs-v4/v6.txt,
以及 --vpsetup 参数(一次运行同时同步本仓库的内嵌清单与 v6 文件)。
2026-11-08 13:12:01 +08:00

229 lines
9.8 KiB
Markdown
Raw Permalink 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 网段清单:来源、依据与实测结论
状态:已采纳并上线(2026-11-08)
影响:`setup_cdn_bypass_tproxy.sh` 内嵌 v4 清单 + `cdn-cidrs-v6.txt`,
以及 GCP 项目里的 egress 拒绝规则(`cf-no-cdn` / `fastly-no-cdn` / `no-akamai-0` /
`no-akamai-1`;新项目 `vps-free-xu11` 里是单条 `cdn-egress-deny`)
---
## 0. 这份清单干什么用
GCP 的 CDN 出站走 `Network Data Transfer Out via Carrier Peering Network` 科目:
**$0.08/GiB、从第一字节起算、没有 200 GiB 免费额度**(官方原话,见
[Network Connectivity pricing](https://cloud.google.com/network-connectivity/pricing):
"Egress charges for CDN Interconnect appear on the invoice as Network Data Transfer Out via
Carrier Peering Network")。普通互联网出站则有 200 GiB 免费。
CDN 绕行就是把"目的地属于 CDN 网络"的出站流量 TPROXY 到上游 Oracle 节点,
让 GCP 看到的目的是 Oracle 而不是 CDN,从而落回有免费额度的科目。
---
## 1. 依据是什么(本文档最重要的部分)
### 1.1 GCP 怎么判定"这是 CDN 流量"
按**目的地属于哪个网络**自动判定,不看你配了什么、也没有 IP 清单可查:
> If your CDN provider is already part of the program, you don't have to do anything.
> Traffic from supported Google Cloud locations to your CDN provider **automatically** takes
> advantage of the direct connection.
> —— [CDN Interconnect overview](https://docs.cloud.google.com/network-connectivity/docs/cdn-interconnect)
所以**真正的 ground truth(GCP 内部的路由分类表)我们看不到**,只能靠账单科目反推。
### 1.2 Cloudflare / Fastly:官方公开,权威
| 来源 | 条数(v4 / v6) |
|---|---|
| `https://www.cloudflare.com/ips-v4`、`/ips-v6` | 15 / 7 |
| `https://api.fastly.com/public-ip-list`(`addresses` / `ipv6_addresses`) | 19 / 3 |
这两家没有争议,直接用。
### 1.3 Akamai:没有官方公开清单,只能用 ASN/BGP 派生
**已穷举确认**(不是关键词搜索):
- 官方 TechDocs 里的 CIDR 列表全是**客户作用域**的:Site Shield、Origin IP ACL、
Client Access Control —— 给的是"你这个合同的回源入口",不是"Akamai 全部边缘",要登录。
- 官方 GitHub 组织 `github.com/akamai` **207 个仓库**全量枚举,没有任何网段仓库;
`github.com/linode` **118 个**同样没有。
- 第三方研究文档结论一致:
[firehol/update-ipsets 调研](https://github.com/firehol/update-ipsets/blob/master/.agents/sow/research/sow-0017/soft-cdn-edges.md)
—— "Akamai CDN: **No official public unauthenticated bulk feed**;Authenticated API exists
for customers;建议 BGP enumeration of AS20940 + AS16625"。
**实际采用的数据源**:[`SecOps-Institute/Akamai-ASN-and-IPs-List`](https://github.com/SecOps-Institute/Akamai-ASN-and-IPs-List)
- 做法:按 40 个 Akamai ASN(`AS20940` CDN 边缘、`AS16625`、…、`AS63949` Linode 云主机)
从 RADb 拉 BGP 宣告记录,拼成 IP 列表。
- 更新频率:**每小时**(`Last-Checked-At` 文件,2026-11-08 08:03 IST)。
- 提供的文件:
| 文件 | 说明 |
|---|---|
| `akamai_ip_list.lst` | v4,9462 行,**按注册原样、互相重叠**(有 `2.16.0.0/13` 又有 `2.16.0.0/24`) |
| `akamai_ip_cidr_blocks.lst` | v4,**已合并好**,235 条 —— 与上面那个 collapse 后完全等价 |
| `akamai_ipv6_list.lst` | v6,3849 行,**同样未合并**(仓库没提供 v6 合并版,必须自己合) |
| `akamai_asn_list.lst` | 收录的 40 个 ASN |
**关键教训**:之前内嵌的那份是拿 `akamai_ip_list.lst` **手工合并**、且是**一两年前的快照**。
2026-11-08 复核发现:
- 已过时:265 段 / **209,106 个地址** Akamai 已不再宣告(还在绕,纯浪费);
另有 49 段 / **13,504 个地址**是新宣告的(当时没绕 → 在按 $0.08/GiB 计费)。
- 合并方式不必要:手动合并出 368 条,而仓库自带的合并版只要 **235 条**,覆盖完全相同。
⇒ **不要自己合并 v4**,直接用 `akamai_ip_cidr_blocks.lst`;v6 仓库没给合并版,自己 collapse。
### 1.4 Linode 在里面,怎么办
`akamai_asn_list.lst` 含 `AS63949`(Akamai Connected Cloud / Linode)。那是**客户虚拟机**,
不是 CDN 边缘,严格说不该算 CDN 目的地。
2026-11-08 评估过"精确挖掉":
| | 条数 |
|---|---|
| 挖之前 | 368 |
| 精确挖掉后 | **381**(减法会把整块切成碎片) |
而且挖完必须同步改 GCP 拒绝规则,否则那些站点会被 VPC **直接丢包**(不是回退直连)。
净负收益 ⇒ **保留**。若将来"Linode 站点被绕行"这件事本身不可接受,再单独处理。
---
## 2. 条数要精简吗:不需要
### 2.1 这不是 iptables 规则链,是一个集合查表
`ip daddr { … 269 个 … } accept` 编译成**一条** lookup 指令。用
`nft --debug=netlink list chain ip cdn_bypass_mark output` 直接看到:
```
[ payload load 4b @ network header + 16 => reg 1 ]
[ lookup reg 1 set __set0 0x0 ]
```
不是 269 条 `cmp`。集合元素在 nft 用哈希表/红黑树存放
([官方 wiki](https://wiki.nftables.org/wiki-nftables/index.php/Sets)),区间集是 rbtree,
查找 O(log n)。
### 2.2 实测
在 `gcoregon-2` 上做 A/B,目标选**不在集合里**的站点(不经过代理,隔离查找本身),
每组 120 次,并**人为把集合灌到 5 万条**做灵敏度对照:
| 集合元素数 | 均值 | 中位 | P90 |
|---|---|---|---|
| 188 | 0.2108s | 0.2099s | 0.2164s |
| 488 | 0.2101s | 0.2094s | 0.2157s |
| 5,188 | 0.2112s | 0.2095s | 0.2154s |
| **50,188** | 0.2095s | 0.2085s | 0.2158s |
| 188(还原) | 0.2107s | 0.2101s | 0.2176s |
**5 万条都测不出差异** ⇒ 条数在性能上不是问题,不构成精简理由。
### 2.3 而且 GCP 也没有 256 条上限(之前的推测是错的)
曾推测 `no-akamai-0`(256) + `no-akamai-1`(112) 的拆分是因为单条规则上限 256。
实测:300 / 1000 / **3000** 个 `destinationRanges` 全部创建成功。
那个拆分是**人为的**,不是平台限制。
### 2.4 强行合并到"几十条"的代价
对 Akamai 段做 trie 上的精确 DP,得到"条数 ↔ 多绕地址"前沿:
| 条数 | 多绕地址 | 占全部公网 IPv4 |
|---|---|---|
| 368 | 0 | 0% |
| 300 | 186,048 | 0.004% |
| 256 | 1,624,863 | 0.038% |
| 210 | 14,755,006 | 0.34% |
| 150 | 124,964,718 | 2.9% |
| 50 | 1,157,258,014 | **27%** |
| 20 | 2,385,523,230 | **56%** |
"几十条"意味着把公网 27%~56% 的地址丢给 3 台 Oracle 转发 —— 不可接受。
---
## 3. 最终清单
| | 内容 | 条数 |
|---|---|---|
| v4 | CF 15 + Fastly 19 + Akamai 235(仓库自带合并版) | **269** |
| v6 | CF 7 + Fastly 3 + Akamai 183(自己 collapse) | **193** |
审计日期记录在脚本里的 `CDN_LIST_AUDIT_DATE`;超过 180 天未刷新会打印告警。
验证记录(2026-11-08):
- 两台机器渲染出的 `/etc/nftables/cdn-bypass.nft` **md5 一致**(`c8f16ac8…`)。
- 真实 Akamai 边缘地址能命中:`www.akamai.com` → `2600:1417:4400:26::17c9:673c` 命中
`2600:1400::/24`;`www.microsoft.com` → `2600:1417:4400:1188::356e` 同样命中
(微软是 Akamai 客户)。`www.apple.com` 的 AAAA 走移动线路,不在清单里,符合预期。
---
## 4. 怎么刷新
生成器已经挪到 **gc-toolkit** 的 `cdn-list` 子工具(这份清单存在的唯一原因是 GCP 计费,
VPC 拒绝规则本身也是 GCP 资源):
```bash
cd ../gc-toolkit
./cdn-list/refresh.sh # 只报告差异
./cdn-list/refresh.sh --apply --vpsetup ../vpsetup # 落盘并同步本仓库的内嵌清单
```
`.github/workflows/refresh-cdn-list.yml` 每天 04:00 UTC 自动跑一次(GitHub 托管 runner,
无需自备机器)。
历史:原先本仓库有过 `tools/refresh-cdn-lists.sh`,功能等价但只服务本仓库;已合并进
gc-toolkit,避免两份生成器分叉。
### 手工流程(脚本不可用时的兜底)
见 `setup_cdn_bypass_tproxy.sh` 头部注释的【怎么更新这份清单】。要点:
1. **先看仓库有没有在更新**:`curl -s $B/Last-Checked-At`
2. v4 用 `akamai_ip_cidr_blocks.lst`(已合并),**不要**拿 `akamai_ip_list.lst` 自己合
3. v6 用 `akamai_ipv6_list.lst`,**必须**自己 collapse(仓库没给合并版)
4. CF / Fastly 用官方两个 URL
5. 三个来源 cat 到一起 collapse,替换清单体,更新 `CDN_LIST_AUDIT_DATE`
---
## 5. 两侧必须一致
主机侧清单和 GCP 拒绝规则的网段集合必须是同一份。不一致会导致:
- 主机侧少了 → 那些 CDN 目的地直连出去,按 $0.08/GiB 计费(**漏绕**)
- GCP 侧多了 → 拒绝规则把本应经代理绕出去的流量挡住(**误丢包**)
---
## 6. 未完成
- `gcoregon` 所在项目(`western-watch-268811`)的四条拒绝规则仍是旧的 368 段快照,
未与新的 269 段对齐(主机的绕行名单已更新,所以现在是"主机新、GCP 旧")。
影响:老的 209k 段仍被 VPC 拒绝但主机已不再绕它们 → 那些目的地会丢包。
**需要尽快对齐**(改成单条 269 段即可,3000 条实测可行)。
- 清单是**内嵌**的(被封锁的机器在代理起来之前访问不了 cloudflare.com),
不会自动更新,只能靠 §4 的手工流程 + 180 天告警。
---
## 7. 本次纠正的错误认知
1. ❌ "GCP 单条规则上限 256 个网段" → 实测 3000 个也能建。
2. ❌ "Akamai v6 没有可用来源,所以故意不收" → 前提错了。该仓库有
`akamai_ipv6_list.lst`。之前 v6 只有 CF+Fastly 10 条,
**所有走 v6 的 Akamai 边缘流量都没被绕行**,一直在按 CDN 计费。
3. ❌ "精确挖掉 Linode 会更简洁" → 368 → 381,反而更多。
4. ❌ "清单 188 条够用" → 那是 1~2 年前的快照,已漂移约 20 万地址。