为什么要写这篇文章?

我起初并没有写这篇文章的意思,因为无论是从难度还是代码复杂度来说这个项目在我写过的项目里只能算中等,但是它却有一个特点,就是我的第一个专门开发出来供他人使用并且希望被他人使用的项目。先前的项目要不然不能公开,要不然个人属性非常强,不适合他人使用。所以这篇文章主要是以纪念为目的的。

为什么要开启这个项目?

那段时间我的站点有时会打不开,起初以为是网络问题,后来越来越严重,进入后台才发现数据库 IO 拉满了。看了看 nginx 的日志才发现站点被疯扫,于是打算做点什么。

我先从网上搜索到了一些基础的拦截扫站的规则,直接 copy 到 nginx.conf 里就能用,但是这些规则都特别多,而且杂,直接丢到配置文件里会大大降低配置的可读性,所以当初就被否了。

后来我拉黑了一些 IP,不过这也就一时起作用,过段时间换了 IP 继续来扫。补救措施是用 whois 找到 IP 地址快直接全部拉黑。但是这导致了一些误伤,于是这个方案也被否了。

然后我找到了 ngx_lua_waf,用了一段觉得还行,但是也发现了一些缺陷,最明显的就是拉黑 IP 的时候只能拉黑单个 IP,而不能拉黑一个地址块。但是这个模块大概已经停止维护了,不能指望作者更新了。

然后在 Github 上看了几个防火墙模块,要不然功能不全,要不然使用复杂,于是萌生了自己写模块的想法。

为什么用 C 语言开发?

主要是因为这样可以不用安装 lua_nginx_module,其次是因为 C 的执行效率理论上要高于 lua,因为 lua 需要启动一个 VM。

不过我主要考虑的是安装是否方便,性能倒不是那么看重,因为 lua_nginx_module 身经百战,性能完全可以保证。

不过用 C 语言开发必然增加开发难度,最重要的是要自己控制内存,这类 Web 服务器上的模块一旦内存泄露、段错误啥的那麻烦就大了,不过我还是有信心不出大问题的。

设计原则

只实现基础的防护,即 IP 检测、Url 检测、Get 参数检测、Cookie 检测、Post 检测、Referer 检测和 CC 防御。不会引入更复杂的功能,比如根据某个 IP 的行为进行综合判断是否拦截。

在保证代码良好可读性的情况下提高性能。

语义化版本

说实话我之前还真没把这个当回事,但是当我打算把这个项目给别人用的时候我发现版本号似乎也能传递一些比较重要的信息,所以开发这个项目的时候我是一直遵守这个标准的。

简单来说语义化版本将版本号分为 X.Y.Z。

  • 当你做了不向下兼容的更改之后 X 要递增。
  • 当你做了向下兼容的功能性新增的时候 Y 要递增。
  • 当你做了向下兼容的修正时 Z 要递增。

这样版本号就可以向用户传递对应的信息,这无疑是十分有用的,最明显的就是当用户看到 X 递增的时候就会十分谨慎,因为贸然更新可能会导致现有的程序出现不兼容的情况。

Change Log

作为一个打算给他人使用的项目,一个清晰的 Change Log 就是很必要的了,它可以将每次更新的内容展示出来,方便他人了解也方便自己记录。

格式上基本遵循:https://keepachangelog.com/zh-CN/1.0.0/

「代码发布」Nginx 防火墙模块开发总结插图
效果图

从效果上来看应该还算是清晰准确的 Change Log。

开发文档

项目写到后期就不得不写开发文档了,首先是方便了自己,其次也是方便别人。可以从头开始写效率太低了,强烈推荐 doxygen。这个工具可以通过代码内的注释自动生成文档,这样为源代码写注释的同时还能生成开发文档,十分方便。

下面是对应的注释以及生成的开发文档。

「代码发布」Nginx 防火墙模块开发总结插图1
注释内容

开发文档简单,清晰,还能自动生成函数调用图和被调用图等一系列图。如果觉得丑还能自定义 CSS,不过对于我这个前端困难户来说就不搞了。

性能优化

性能优化的主要工作就是降低请求检查花费的时间。

缓存

Url 检查、Get 参数检查、Post 检查、Cookie 检查和 Referer 检查都需要执行正则匹配,暂时没发现什么合适的方法去优化,只能一个一个地去检查。时间复杂度为 $\text{O}(nm)$,$n$ 为需要测试的正则条数,$m$ 为执行正则匹配的时间。

不过由于本模块只会在 nginx 启动时读取规则,运行时规则不会改变,所以对于同一个 URL 无论检测多少次结果都不会变,于是本模块会缓存检查的结果,每次检查前先读取缓存,如果命中则直接取出结果,反之则走流程检查。除了 IP 检查和 Post 检查以外所有的检查都使用了缓存机制。

缓存淘汰策略为 LRU。当缓存的内存不足时会引起频繁的淘汰,增加内存碎片。于是本模块会周期性地按照 LRU 的策略淘汰掉一定比例的缓存。

前缀树优化

IP 检查很有搞头。本模块使用了一种经过修改的前缀树来改进 IP 检查的性能,见下图。

「代码发布」Nginx 防火墙模块开发总结插图2
本模块所使用的前缀树

从图示就可以看出此结构可以方便地处理单个 IP 的检查和 IP 地址块的检查。

前缀树的查找时间为 $\text{O}(h)$,其中 $h$ 为树的高度。对于 IPV4 来说一共有 32 位,那么树高最多 32,IPV6 对应的树高最多为 128。那么查找时间复杂度为 $\text{O}(1)$,也就是说执行 IP 检查的时间基本上不会受到 IP 黑白名单规模的影响。

测试

经过比较极限的测试,QPS 降低了约 4%,详情见性能测试。

我上 Trending 了!

2021 年 4 月 8 日晚上我发现我的项目出现在了 Github Trending(C 语言) 下,截图纪念一下。

「代码发布」Nginx 防火墙模块开发总结插图3

希望不要明天起来就没了,多撑几天吧。

第二天晚更新:博主已经凉了,散了吧。

验证码

这里的验证码特指用于人机验证的验证码,比如滑块解锁等,也就是我们通常所说的 CAPTCHA,比较有名的就是 reCAPTCHA。

「代码发布」Nginx 防火墙模块开发总结插图4
reCAPTCHA

目前已经集成了 reCAPTCHA 的全部种类的验证码以及 hCAPTCHA。本来想着集成一下腾讯云的验证码,但是发现集成起来好麻烦,远没有前两种简单,就一直搁置到现在。

验证码的集成也带来了不小的好处,比如 CC 防护不再只能简单地拉黑了。有了验证码后就可以将 CC 防护的阈值降低一些,超出时弹出验证码,通过验证则放行,反之则拉黑一段时间。同时,根据某个用户的建议,现在当触发任何的拦截时,就会对该 IP 启用验证码,当且仅当通过验证时才会放行,否则无法进行任何访问。

集成 ModSecurity

ModSecurity 是一个广泛使用的开源的 WAF 引擎,并且在网络上有现成的安全规则可以套用,所以本模块也继承了 ModSecurity 的引擎,可以加载 ModSecurity 的规则。

(ModSecurity 的内存泄漏问题官方什么时候修一下)

后续计划

本项目已经完成了性能优化,后续应该不会有大的改动,不过如果有合适的需求就另说了。有了新的进展我也会更新本文。

ESWINK , 版权所有丨如未注明 , 均为原创

原文标题:「代码发布」Nginx 防火墙模块开发总结

Eswink原创声明