写在前面

原文地址:https://blog.cloudflare.com/cloudflares-new-waf-compiling-to-lua/

原作者:John Graham-Cumming
写于 23 Aug 2013

关于本文:
这虽然是一篇很老的文章了,但是对于了解cloudflare waf的原理和优化过程仍旧有很大的帮助。
个人认为,通过查看 CloudFlare 博客的文章,对运维方面也有一些帮助,这是翻译的第一篇,以后会陆续选取一些有意义的 (我喜欢的分类,比如DDoS、架构、nginx、Golang等等) 博客翻译。
翻译水平有限,有不通顺的语句,请见谅。

正文

在我们的网络中,我们使用nginx来做第一线的web服务、代理以及流量过滤。在某些情况下,我们在nginx的C语言的核心代码中加入了我们自己的模块, 但是最近我们做出了一个重大的举措,那就是将lua和nginx结合。

现在新的CloudFlare WAF项目,几乎是全部用Lua编写的,这在我们的另一篇博客讲到.

Astronaut_moon_rock.jpg

Lua WAF使用了nginx Lua 模块 来集成lua代码,可以像通常nginx的处理阶段中运行 Lua 代码. WAF全部的执行步骤是由如下的nginx配置控制:

1
2
3
4
5
6
7
8
location / {
set $backend_waf "WAF_CORE";
default_type 'text/plain';
access_by_lua '
local waf = require "waf"
waf.execute()
';
}

这个 access_by_lua 指令告诉nginx,执行这个Lua 代码作为access阶段的处理程序. 然后,waf 模块中的Lua代码会判断这个请求是否应该被阻挡或者放行到源服务器去执行. WAF 通过调用 ngx.exit() 来结束判断,返回响应码200 (让nginx继续处理该请求) 或者403 (阻断请求).

所有的WAF工作都是用Lua编写,在waf模块中全部实现。

WAF的实现,我们希望能够读取为流行的 mod_security 开源WAF编写的已有的WAF配置,并且支持我们自己简单的 WAF 语言. mod_security 代码必须在Apache的配置文件内运行,这导致它的代码有些难以阅读, 但是有大量的规则使用它来编写(比如流行的OWASP 规则集) ,而我们希望能够原生运行.

在编写新的WAF之前,我们其实只是为mod_security运行Apache 和 nginx, 但是这样的组合运行很慢,麻烦, 并且不能随着 CloudFlare 的业务增长而拓展. 我们已经在妥协,只是为了让他能继续运行。

一个用mod_security原生语言编写的规则大概如下所示(这是我们真实执行的自定义规则的轻微模糊版本). 第一个代码块显示了 mod_security 的版本, 第二块相当于是CloudFlare自己的语言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SecRule REQUEST_HEADERS:User-Agent "@beginsWith DataStore/"
"id:100000,phase:0,t:none,deny,chain,msg:'DataStore Attack'"
SecRule REQUEST_METHOD "@streq GET" "chain"
SecRule REQUEST_URI "\/\?-?\d+=-?\d+" ""

rule 100000 DataStore Attack

REQUEST_HEADERS:User-Agent has-prefix DataStore/ and

REQUEST_METHOD is GET and

REQUEST_URI matches /?-?\d+=-?\d+

deny

我们将开拓能力来编写自定义规则,无论是以mod_security还是 CloudFlare样式,因为WAF涉及太广泛. 最重要的是,尽管WAF是在Lua中实现的,但CloudFlare WAF语言并不是Lua。

实际上,WAF首先采用以mod_security或CloudFlare样式编写的规则,并将它们转换为编码规则常用的JSON格式,并用WAF UI添加使用的其他信息(比如是否可以禁用规则)。

cloudflare-waf-screenshot_2.png
cloudflare-waf-screenshot_2.png

然后JSON 格式被编译进Lua 程序,并由WAF执行. 在编译步骤中,我们可以使之支持多种输入语言 (就像以上提到的两种),并执行优化,以便WAF能快速执行.

比如, 编译器执行以下任务:

  • 子句重新排序,以便在子句不匹配时可以快速跳过规则
  • 优化和简化正则表达式
  • 更换操作符,以便尽可能使用快速的操作符(例如简单的字符串匹配)
  • 向WAF runtime提供有关是否需要宏扩展的提示。
  • 全局优化,例如识别重复使用相同的字符串或变量,并确保它们只计算一次
  • Lua优化,例如使用全局函数的本地引用

以上规则由以下lua代码实现:

1
2
3
4
5
6
7
8
9
10
11
if waf_begins(waf, v3_6, '3_6', t3_1, '3_1', 'DataStore/', false) then
waf.vars['RULE']['ID'] = '100000'
if waf_eq(waf, v3_7, '3_7', t3_1, '3_1', 'GET', false) then
if waf_regex(waf, v3_4, '3_4', t3_1, '3_1', [=[\/\?-?\d+=-?\d+]=],
false, nil, false) then
waf_activate(waf, rulefile)
waf_msg(waf, 'DataStore Attack')
waf_deny(waf, rulefile)
end
end
end

生成的代码可读性差,因为它基本上是 WAF 汇编语言,并且是自动生成的。WAF 运行时 实现的函数,用于规则匹配比如waf_begins, waf_eq, and waf_regex。而这些功能本身是高度优化的。

总体目标是,在线上环境时,WAF能在<1ms内做出阻断/放行决策。

WAF 运行时的优化,来自于在一系列的行级时间信息测试下WAF的 性能表现,以及在CloudFlare 的网络下运行详细的基于systemtap的指令.

为了得到在运行测试时的行级信息,我们写了一个简短的行级代码分析器,用来在运行WAF核心代码的时候发送一系列的测试请求. 这个分析器, 名叫 lulip, 是一个开源的项目. 他会输出哪些行被调用最频繁,以及哪些行消耗了最多的执行时间。

比如, 以下是一次测试的简化版输出:

1
2
3
4
5
6
7
file:line     count   elapsed (ms)   line
wr.lua:1129 2 822.455 hash = ngx_sha1_bin(value)
wr.lua:1172 428 470.849 captures, err = ngx_re_match(v, p)
wr.lua:1197 3762 207.487 x = string_find(v, f)
wr.lua:212 157 154.386 string_gsub(v, "//([^/]+)//", "%1")
wr.lua:1196 3788 87.475 for i=1,g() do
wr.lua:1158 1563 52.906 if not f() then

它告诉我们,ngx_sha1_bin (在nginx Lua其实是一个ngx.sha1_bin 的本地 Lua 引用函数)仅仅被调用了两次,但是运行花费了823ms. 下一条最昂贵的代码是在1172行,总共花费了471ms的时间但是被调用了428次. 使用这些细节信息,我们能够去优化特定的代码块.

来自systemtap 或者堆栈跟踪信息会发送到我们自己的pastebin,然后会被解析并自动生成一张火焰图 ,上面显示代码运行的位置. 将鼠标放在任何区域,会给出运行这段代码所花费的百分比信息.

Screen_Shot_2013-08-23_at_2.57.02_PM_1.png
Screen_Shot_2013-08-23_at_2.57.02_PM_1.png

在优化WAF的开始阶段,火焰图迅速展示了 LuaJIT的成本,由于广泛使用的 closures导致的缓慢。我们修改了编译器,去掉他们的使用。

从同一信息生成的另一个视图,显示了在函数内花费的总时间 (忽略掉他自己调用的任何函数). 这样能迅速的识别出热门的函数. 在这里可以很容易看出,正则表达式的处理和字符串匹配是花费时间最多的操作(这不奇怪,因为WAF主要是做这些事情).

Screen_Shot_2013-08-23_at_2.55.07_PM.png

通过检查这些追踪情况,我们觉得对LuaJIT 开源项目发起改进是值得的.

优化 WAF 的关键来源于两个方面: 重复的测试数据和检查运行代码的工具. 火焰图和lulip二者的结合,意味着可以通过精确查看花费的时间来让对WAF的性能进行更大的优化。使用规则编译器意味着可以根据需要,对所有规则进行快速优化. 揣测缓慢的原因是没有意义的,我们只需要对他进行精确衡量。

生成的代码使用大量的局部变量局部变量 (大多数是由编译器自动生成的) 和memoization.我们还使用nginx Lua加载的Lua代码缓存来加速自定义规则的加载,lua_shared_dict使用的是两级内存高速缓存 ,并且使用 memcached来获得额外的加速.而我们的全球分布式数据存储 意味着新的规则可能在几秒钟内应用完成.

最后, 为了衡量WAF在生产环境中的运作,我们建立了一个全局的测量系统,收集CloudFlare网络部分中的所有指标. 以下图表是WAF在几个小时内的运行情况; 以微秒为单位显示处理请求的平均时间. WAF处理每个请求的平均时间在380us到480us之间, 完全在1ms的目标内.

Screen_Shot_2013-08-23_at_4.41.07_PM.png

优化后的语言、编译器和WAF核心结合在一起,意味着我们现在拥有一个非常快速的Lua WAF,它为我们提供了极大的灵活性,然而这一切都是在nginx的核心内运行。 这是目前为止的又一个项目,它表明Lua是一种优秀的嵌入式语言的项目,可以编写CloudFlare所需的各种可扩展逻辑。

CloudFlare waf产品页面:https://www.cloudflare.com/waf/