← 返回首页

CF 扛住了 DDoS,但 CC 还在回源

2026-07-21 03:40 13 次阅读

写在前面:DDoS 被 CDN 背后的 anycast 网络吸走的那一刻,我以为噩梦结束了。直到我发现,另一类攻击正悄悄顺着缓存的缝隙,一波波敲我源站的大门。这篇聊聊 CC,以及一个小博客该怎么用「让请求死在边缘」的思路把自己藏起来。

一、先分清两件事:DDoS 和 CC 不是一回事

很多人把这两个词混着用,但它们打的根本不是一层。

  • DDoS(分布式拒绝服务) 大多是 L3/L4 的流量型攻击:UDP 反射、SYN flood、巨量带宽把你的链路或机器打满。这类攻击靠的是「量」,攻击者并不关心你的网页长什么样。
  • CC(Challenge Collapsar,挑战黑洞)L7 应用层攻击:攻击者发的是一个个「看起来完全正常」的 HTTP 请求——刷你的首页、点你的接口、撞你的登录页。它不打带宽,打的是你的应用逻辑和数据库

我这次的遭遇很典型: DDoS (volumetric) 那波,CDN 的 anycast 网络直接把流量吸走、源站 IP 藏在代理后面,攻击者根本摸不到我机器。可紧接着来的 CC,走的是正经的 HTTPS 请求,CDN 默认不会拦「看起来合法的请求」,于是这些请求一路穿透,回了我的源站。

二、为什么 CC 更阴:它专打「应用层」

关键差异在这:

DDoS 打的是「管道」,CC 打的是「你的代码」。

我的源站是一台 2C2G 的轻量服务器。CC 这波峰值带宽才 60+ Mbps——单看带宽,毛毛雨。但 CC 的杀伤力不在带宽,在 RPS(每秒请求数)和连接消耗

  • 每个 GET 请求都要 Go 起 goroutine、走一遍路由、查一遍数据库;
  • 如果页面没缓存,每一次都是一次实打实的源站计算 + 一次 DB 往返;
  • 攻击者只要用够大的 IP 池、把 RPS 堆上去,2C2G 的 CPU 和数据库连接池会先一步被榨干——带宽还闲着,服务已经 502 了

我这次幸运在峰值低。但「万一」永远是存在的:万一对方换一个更离谱的 IP 池、万一 RPS 翻十倍?2C2G 扛不住是真的扛不住。所以问题不是「现在够不够」,是「怎么让请求尽量别回来」。

三、防御的第一性原理:让请求死在边缘

对一个小博客来说,绝大多数访客请求的都是同一批静态内容:首页、文章页、关于页。这些内容不随请求变化,天然适合缓存。防御 CC 的核心就一句话:

能缓存在边缘的,绝不让它回源。

1. 把静态页丢给边缘缓存

对 HTML 页面开 Cache Everything,给一个合理的边缘 TTL(比如一小时)。这样同一篇热门文章,第一个请求回源、后续的请求全在 CDN 边缘被消化,源站一次都不碰。

# 命中以下路径 → 边缘缓存 HTML
(http.request.uri.path eq "/") or
(http.request.uri.path eq "/about") or
(http.request.uri.path eq "/changelog") or
(http.request.uri.path wildcard r"/post/*")

2. 忽略查询串,否则随机参数绕过缓存

这是最容易踩的坑。CDN 的缓存键默认包含查询字符串,于是攻击者刷 /?x=10086/?x=10087……每一个都是不同的缓存键 → 全 miss → 全回源。缓存形同虚设。

解决:把缓存键设为忽略查询串。这样所有查询变体坍缩成同一个缓存对象,随机参数洪水被边缘一口吞掉。

注意:如果你的网站确实依赖查询参数(如分页、筛选),不要全局忽略查询串。可以在特定路径上做例外处理,或者用「规范化查询串」代替「全部忽略」。

3. 开启 stale-while-revalidate

缓存总有过期的一刻。开启「重新验证时提供过时内容」后,TTL 到期的那个请求不会卡住访客去等源站——CDN 先吐旧页,后台再去刷新。源站即使短暂抽风,用户也毫无感知,CC 想靠「打穿缓存瞬间」制造 502 的算盘也落空。

注意:stale-while-revalidate 只能扛短暂的源站抖动。如果源站挂了超过几分钟,过时内容也会过期,用户还是会看到错误页。

四、缓存护不住的地方,用限流补

缓存只管 GET 静态面。有两处它管不了,必须另想办法:

  • 登录 / 凭证接口:这是凭证填充的入口,绝不能缓存,必须回源。
  • 动态 API:比如文章列表、阅读量这类实时数据。

对登录面,把有限的限流配额全砸上去——限制单 IP 单位时间的尝试次数,再叠加泄露凭证检查(提交的密码若是已知泄露库里的,直接拦)。一个小博客的爆破面就这一处,守住它,应用层最危险的入口就封死了。

动态 API 则可以走「绕过缓存」:明确告诉 CDN 这些路径不缓存,保证数据实时;代价是它们仍会回源,所以限流配额要优先保护其中最敏感的那一个(登录),其余靠 CDN 的 L7 防护兜底。

五、一个容易翻车的坑:缓存一上,计数就冻

我差点栽在这:阅读量本来是在渲染文章页时服务端顺手记的。一旦 HTML 被边缘缓存,请求不回源,计数逻辑根本不执行——阅读量直接冻在缓存那一刻的数字上

解法也很直接:把计数从「页面渲染」里剥离出来,改成前端异步上报

# 文章页加载后,浏览器发一个 beacon
POST /api/view?post=<id>
# 服务端记录(按 IP + 时间窗去重)并返回最新值,前端刷新显示

这个端点本身不进缓存(绕过),所以无论 HTML 缓存多久,计数都实时。缓存负责扛量,beacon 负责计数,各司其职。

注意:beacon 返回新计数后,前端需要用 JavaScript 更新 DOM 中的显示值,否则用户看到的还是缓存时的旧数字。

六、残留的诚实

写技术文不能只讲漂亮话,得把边界说清楚:

  • **缓存只护 GET 静态面。**登录、API 等动态接口还得靠限流 + WAF,别指望一层解决所有事。
  • 单 IP 限流挡不住分布式 CC。对方用成千上万个 IP 低速打,按 IP 限流就失效了——这种得靠 CDN 的 L7 DDoS 缓解 + Bot 管理,不是应用层能单独兜底的。
  • 缓存键和 TTL 要配对。忽略查询串、合理 TTL、serve-stale,三者少一个,CC 都可能从缝隙漏进来。

小结

被 DDoS 打醒之后我才真正明白:小站点的生存之道不是「扛」,是「藏」

volumetric 攻击交给 CDN 的网络;应用层攻击靠「边缘缓存把量吃掉 + 限流把入口焊死 + 计数改成异步不依赖渲染」。三层叠起来,源站能碰到的请求就所剩无几——剩下的那点回源,2C2G 也随便扛住了。

没有银弹,但「让请求死在边缘」这一条,值得我们每个小站长刻在脑子里。

← 查看上一篇:凌晨一点,我的博客被 DDoS 了
已经是最新文章了