Cloudflare 防火墙规则背后的工程实践

一个越来越难用的防火墙

如果你用过 Cloudflare 的防火墙,你可能经历过这样的困境:想同时根据 IP 和 URI 拦截某个请求,发现做不到。想说"来自某个 AS 号、且访问路径包含 /wp-admin"的请求才拦截,也做不到。

这不是功能没有,而是架构本身的限制。这篇博客完整讲述了 Cloudflare 是如何一步步从"每个维度单独一套规则"演进到"支持任意组合的表达式防火墙"的,以及为什么最终选择用 Rust 来构建核心匹配引擎。

原文博客: https://blog.cloudflare.com/how-we-made-firewall-rules/


旧系统是怎么工作的

Cloudflare 最早的防火墙能力非常原始:只能针对 IP 地址进行封禁。

复制代码
if request IP equals 203.0.113.1 then block

随着需求增加,逐渐加入了 CIDR 范围、ASN、国家、User Agent、URI 匹配,还有 Rate Limiting、Zone Lockdown 等功能。但这些功能在实现层面是相互独立的,每一个都只处理单一维度。

到 2017 年,这个防火墙的能力可以被一句话总结:

你可以按任何条件拦截流量,前提是你只挑一个条件。

这些规则在实现上分为两类:

Lookup 匹配 :针对 IP、CIDR、ASN、国家、User Agent 这类字段,构造一个 KV 键(比如 zone:www.example.com_ip:203.0.113.1)去全局分布式存储里查。O(1) 复杂度,性能极好,但只能查单一字段的值。如果要组合两个字段,就需要把所有可能的组合都写进 KV,键的数量会爆炸。

正则匹配:针对 URI 的 Page Rules,把所有规则合并成一个大正则:

复制代码
^(?<block__1>(?:.*/wp-admin/index.php))|(?<block__2>(?:.*/xmlrpc.php))$

正则的命名捕获组被用来编码动作类型。这个方案在 URI 匹配场景下出乎意料地好用,但一旦要同时匹配 URI 加 IP 范围,就没有自然的扩展方式。


灵感:Wireshark

工程师们很早就意识到,新方案的核心应该是一个表达式语言。最初的方案是用 JSON 来表达 DSL:

json 复制代码
{
  "And": [
    { "Equals": { "host": "www.example.com" } },
    { "Or": [
      { "Regex": { "path": "^(?:.*/wp-admin/index.php)$" } },
      { "Regex": { "path": "^(?:.*/xmlrpc.php)$" } }
    ]}
  ]
}

计算机处理没问题,但人看起来费力。在把这个 JSON 翻译成"人类语言"时,工程师意识到这个结构非常眼熟------这不就是 Wireshark 的过滤器语法吗?

Wireshark 是网络协议分析工具,它的 Display Filter 语法长这样:

复制代码
http.host eq "www.example.com" and (http.request.path ~ "wp-admin" or http.request.path ~ "xmlrpc.php")

简洁,人类可读,机器可解析,而且对安全工程师来说几乎零学习成本------他们每天排查攻击时就在用 Wireshark。

Cloudflare 决定借鉴这套语法,但不做 Wireshark 那样的离线数据包分析。他们要的是实时过滤:HTTP 请求进来,毫秒内判断是否匹配,给出处理动作。


核心引擎:wirefilter

基于这个思路,Cloudflare 用 Rust 实现了一个名为 wirefilter 的库(名字向 Wireshark 致敬)。

它做几件事:

  • 定义字段及其类型,比如 ip.src 是 IP 类型,http.host 是字符串类型
  • 给定一张请求属性表(由 HTTP 服务器填充)
  • 解析和校验表达式语法,检查字段名是否合法、操作符是否适用于该字段类型
  • 将表达式应用到请求属性表,返回 true/false

请求属性表的字段覆盖了一条 HTTP 请求的完整信息:

字段 示例值
http.host www.example.com
http.request.uri.path /articles/index
http.request.method GET
ip.src 203.0.113.1
ip.geoip.country GB
ip.geoip.asnum 64496
ssl true

wirefilter 被集成到了两个地方:用 Go 写的 REST API(负责校验用户输入的表达式),以及用 Lua 写的边缘代理(负责在请求进来时实际执行匹配)。

Go 侧的集成大致是这样:

go 复制代码
var scheme = filterexpr.Scheme{
    "http.host":             filterexpr.TypeString,
    "http.request.uri":      filterexpr.TypeString,
    "ip.src":                filterexpr.TypeIP,
    "ip.geoip.country":      filterexpr.TypeString,
    "ssl":                   filterexpr.TypeBool,
}

expressionHash, err := filterexpr.ValidateFilter(scheme, expression)

Lua 侧则负责在每次请求时填充属性表,然后拿 wirefilter 来做实际的匹配。


为什么选 Rust

这个问题的答案比较直接:需要保证 API 侧和边缘代理侧的行为完全一致

如果分别用 Go 和 Lua 各写一套实现,任何微小的差异都可能被攻击者利用------比如在 Go 侧判断为合法的规则,在 Lua 侧匹配逻辑略有不同,就可能绕过防火墙。

用一个共享库来封装匹配逻辑,Go 和 Lua 都通过 FFI 调用它,能从根本上消除这种不一致。

在候选语言里,C 和 C++ 有内存安全问题,Go 和 Lua 已经被排除(正是问题所在),JavaScript Worker 方案在性能和集成上有额外复杂度。Rust 在性能、内存安全、低内存占用、以及可以被其他产品复用(比如 Spectrum)这几个维度上综合表现最优。


规则优先级:一个云环境特有的问题

新系统支持复杂表达式后,随之而来的是一个新问题:怎么确定规则之间的执行顺序?

传统防火墙(iptables、家用路由器)用的是显式顺序:规则 1 到规则 N,匹配到第一条就停止。每次改动都重新发布全部规则,顺序是确定的。

但在云端这不可行。一个大客户可能有几十万条规则,每次改动都要重新发布全部规则代价太高,而且在分布式环境下,两条规则同时发布时可能出现竞态条件。

Cloudflare 的解决方案是 priority 值,是一个 int32,数字越小优先级越高。两条规则优先级相同时,再按动作类型排序:

  1. Log(最高优先级)
  2. Allow
  3. Challenge(CAPTCHA)
  4. JavaScript Challenge
  5. Block(最低优先级)

这套设计有几个好处:

  • 单条规则独立发布,发布速度不受规则总数影响
  • 优先级不要求唯一,可以给一批规则设置相同的优先级值,起到分组的效果
  • 如果你有一个已有的顺序化规则系统,可以直接把顺序号映射成 priority 值导入进来

字段命名的远见

注意到 wirefilter 的字段都有 http. 前缀了吗?这遵循了 Wireshark Display Filter Reference 的命名约定,而不只是一个风格习惯。

这意味着这套引擎从设计上就不是 HTTP 专属的。只要定义了对应协议的字段(比如 smtp.fromdns.query),同一套匹配引擎就可以用于 SMTP、DNS 或者 Layer 4 的流量过滤。Cloudflare 的 Spectrum 产品(支持任意 TCP/UDP 协议的代理)就是这套架构向前延伸的方向。


小结

这篇文章描述了一个典型的工程演进路径:从一堆各自独立、能用但不能组合的功能,到一套统一的、基于表达式的过滤引擎。

几个值得记住的设计决策:

把核心逻辑下沉为库,而不是在每个调用方分别实现。这是保证多语言环境下行为一致的唯一可靠手段,也是选 Rust 写 wirefilter 的直接动因。

表达式语言借鉴成熟工具的语法。Wireshark Display Filter 对安全工程师是零学习成本,从调查工具到防护工具的语法迁移是自然的。

云环境的顺序问题不能用传统方式解决。priority 值而非显式顺序,单条独立发布而非全量重发,是针对分布式环境做的专门设计。

字段命名是架构意图的一部分http.host 而不只是 host,这个前缀埋下了未来扩展到其他协议的伏笔。

相关推荐
编码浪子2 小时前
Rust 1.95 稳定版解读与生态新动向
开发语言·后端·rust
Rust研习社2 小时前
Rust 操作 Redis 从入门到生产级应用
开发语言·redis·后端·rust
土豆12502 小时前
Rust 生命周期开发实战:从"编译不过"到"一次过编"的实用指南
前端·rust
Rust研习社14 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
第一程序员21 小时前
2026年GitHub上最火的10个Python项目,Rust开发者必看
python·rust·github
mit6.8241 天前
Rust 在 Linux 7.0 内核毕业
rust
咸甜适中1 天前
rust格式化输出(println!、format!、...)
开发语言·rust
迪普阳光开朗很健康1 天前
告别繁琐!用ApkInfoQuick快速提取APK关键信息
android·rust·react