nginx 防火墙模块开发总结

为什么要写这篇文章?

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

为什么要开启这个项目?

那段时间我的站点有时会打不开,起初以为是网络问题,后来越来越严重,进入后台才发现数据库 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/

效果图

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

开发文档

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

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

注释内容
开发文档

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

性能优化

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

缓存

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

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

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

前缀树优化

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

本模块所使用的前缀树

从图示就可以看出此结构可以方便地处理单个 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 语言) 下,截图纪念一下。

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

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

验证码

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

reCAPTCHA

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

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

集成 ModSecurity

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

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

后续计划

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

本文作者:ADD-SP
本文链接https://www.addesp.com/archives/2876
版权声明:本博客所有文章除特别声明外,均默认采用 CC-BY-NC-SA 4.0 许可协议。

评论

  1. 3年前
    2021-3-31 20:42:55

    感谢大佬开发的工具,终于找到个好用的了!
    在Github上搜索的要么不更新,要么功能比较简陋,还有就是很复杂,对新手不友好
    总之,为大佬点个赞! (☆ω☆)

    • 博主
      BobMaster
      3年前
      2021-3-31 20:44:31

      感谢支持啦,有什么问题欢迎在 Github 上提问。

  2. max
    3年前
    2021-5-28 9:50:21

    大佬,,,我想知道,如果已经安装好了这个工具,,后续怎样更新的,, 又要重新安装nginx吗

    • 博主
      max
      3年前
      2021-5-28 10:06:10

      如果是安装的静态模块的话需要重新安装整个 nginx,动态模块的话只需要重新编译并替换模块即可。

    • max
      ADD-SP
      3年前
      2021-5-28 11:21:16

      我看你写的官方文档,没有看到描述升级编译的操作,,表示不太懂,,新手的话,没有文档描述不太懂这些操作

    • 博主
      max
      3年前
      2021-5-28 11:23:06

      要升级模块的话按照文档的安装说明重新安装就行,要升级 nginx 的话也需要按照文档的安装说明重来一遍。

    • max
      ADD-SP
      3年前
      2021-5-28 11:26:28

      就是无论试用静态安装或者动态安装,,也需要重新安装nginx对吧,,,,还是说动态安装的模式,,,重新编译nginx之后,,只需要把动态库替代一下就好….但是我还有一个疑问,,如果nginx升级了,,那么是不是就得重新安装整个nginx了

    • 博主
      max
      3年前
      2021-5-28 11:27:54

      如果安装的是动态模块,无论是升级 nginx 还是升级模块,都需要重新编译安装整个 nginx。如果使用的是动态模块,无论升级 nginx 还是升级模块,都只需要重新编译并替换动态模块即可。

    • max
      ADD-SP
      3年前
      2021-5-28 11:31:25

      多谢大佬,,我明白了. 大佬,我还有一个疑问,,,,就是已经安装在服务器的nginx是1.6版本的,没有waf,,,现在我是重新安装nginx1.18或者1.20并且动态安装waf,,,是会替代旧版本的,,还是会同时存在两个nginx版本,,,还是会整体都不兼容了

    • 博主
      max
      3年前
      2021-5-28 11:33:46

      跨版本升级 nginx 还真没试过。可以备份一下配置文件之类的重要文件,然后删除整个旧版的 nginx,然后安装最新的。

    • max
      ADD-SP
      3年前
      2021-5-28 11:37:13

      好的,多谢大佬…后续版本会不会新增一个傻瓜式的拦截国外或者指定国家ip的操作,比较期待

    • 博主
      max
      已编辑
      3年前
      2021-5-28 11:54:52

      暂时不去做这类功能,因为有现成的模块。可以参考一下 为nginx提供GeoIP2支持 | 糖菓·部落

    • max
      ADD-SP
      3年前
      2021-5-28 16:52:57

      大佬,,第一次安装nginx 使用动态模块安装,,,是不是”./configure ARG –add-dynamic-module=/usr/local/src/ngx_waf” 然后 make && make install ,,,,后续更新才使用 “./configure –add-dynamic-module=/usr/local/src/ngx_waf –with-compat” 然后 “make modules” ,,最后替代一下动态模块

    • 博主
      max
      3年前
      2021-5-28 16:56:41

      ./configure –add-dynamic-module=/path/to/ngx_waf –with-compat
      make -j$(nproc)
      make install

      无论第几次安装或者第几次更新,只要是动态模块,都是用上面的命令。

    • max
      ADD-SP
      3年前
      2021-5-28 16:59:07

      大佬,,,你文档中描述动态模块,,是使用 “make modules”,,,没有使用”make -j$(nproc) && make install” 哦….有点懵逼了

    • 博主
      max
      已编辑
      3年前
      2021-5-28 17:00:41

      ./configure –add-dynamic-module=/path/to/ngx_waf –with-compat
      make modules -j$(nproc)

      我搞错了,改成上面那样。

    • max
      ADD-SP
      3年前
      2021-5-28 17:04:35

      再次确认一下
      动态模块第一次才用
      make modules -j$(nproc)
      make install

      后面已安装了,升级waf 就是以下方式了吧
      make modules -j$(nproc)
      cp objs/*.so /usr/local/nginx/modules

    • 博主
      max
      3年前
      2021-5-28 17:09:38

      首先声明,无论是不是第一次安装,动态模块的编译方式都没有区别。

      make install 会自动将本次编译的所有动态模块拷贝到一个目录,这个目录是在编译 nginx 的时候指定的,如果你知道编译 nginx 时指定的模块所在目录的位置可以使用这种方式。

      cp objs/*.so /usr/local/nginx/modules 表示直接拷贝本次编译的所有动态模块到某一个目录,通常是在不知道起初编译 nginx 时指定的模块目录的时候使用。

      所以使用哪种方式看你的喜好,并不会影响模块功能。

    • max
      ADD-SP
      3年前
      2021-5-28 17:17:12

      明白了,,多谢大佬

  3. max
    3年前
    2021-6-04 16:32:48

    大佬,请教一个问题 waf_cache capacity=50;50设置到10000会不会有问题,是使用那个缓存的,存在内存条中的吗?还有,我可以通过什么方式查询被拉黑的名单,然后可以从名单中删除某一个ip,实现不拉黑了

    • 博主
      max
      3年前
      2021-6-04 17:08:03

      无法查询哪些 IP 被 CC 防护拉黑,只能通过重启或者重载 nginx 来清空 CC 防护的拉黑名单。capacity 表示最高的缓存项数量,每个缓存项占用的内存不可预测。所以不建议设置得太高,否则理论上可能会导致内存占用会逐渐增加直到内存不足。

  4. leo
    3年前
    2021-6-04 17:00:44

    根据文档设置

    # 设置 Post 请求体缓冲区大小
    client_body_buffer_size: 10M;

    # 永远将请求体存放在内存中
    client_body_in_file_only: off;

    会报一下错误,百度也没有相关的解析

    nginx: [emerg] unknown directive "client_body_buffer_size:" in /xxx/xx/xx

    • 博主
      leo
      3年前
      2021-6-04 17:08:35

      把冒号删掉,文档里写错了,抽空我改改。

  5. leo
    3年前
    2021-6-04 17:23:48

    新手,弱弱地问一下,url白名单,是指用户请求时referer中的url,,还是用户访问本服务器部署的项目中的url

    • 博主
      leo
      3年前
      2021-6-04 17:26:57

      如果用户访问 https://example.com/index.html?a=b&c=d,url 就是 /index.html

    • leo
      ADD-SP
      3年前
      2021-6-04 17:28:15

      多谢大佬,,,就是我写正则的时候,只需要匹配域名后面的url就可以了是吧

    • 博主
      leo
      3年前
      2021-6-04 17:50:44

      对于 url 黑白名单这么干就行了。

  6. 3年前
    2021-6-07 5:43:55

    点个赞

  7. max
    3年前
    2021-6-24 14:17:43

    大佬,我部署到测试环境的nginx使用没有问题,,,今天部署到正式环境的nginx好多上传和表单提交都被拦截了,,nginx版本都是1.20.1,,其他nginx的配置文件几乎一模一样,会是什么原因导致的呢

    • 博主
      max
      3年前
      2021-6-24 14:19:54

      应该是误报,可以选择清空 POST 黑名单或者关闭 POST 检测。

    • 博主
      max
      3年前
      2021-6-24 14:20:47

      不过标准的做法应该是通过观察拦截日志来删除特定的规则或者添加白名单。

    • max
      ADD-SP
      3年前
      2021-6-24 14:22:42

      上传文件报的是xss攻击,表单就是post攻击,,但是运营的项目在测试服务器和正式服务器都是一样的项目,,,正式服务器上几乎大部分的接口都报错,所以非常疑惑,,如果删除相关的拦截规则黑名单,那么这个waf作用就不大了吧

    • 博主
      max
      3年前
      2021-6-24 14:29:17

      XSS 检测使用的是第三方库,误报确实比较严重,可以在配置文件中的 waf_mode 的末尾追加一项 !LIB-INJECTION-XSS 来关闭基于第三方库的 XSS 检测。

  8. 2年前
    2022-4-20 12:39:55

    真的很不错,希望继续持续更新

  9. 阿里灰太狼
    2年前
    2022-6-22 21:21:09

    感谢博主做出的贡献,喝水不忘挖井人,star 已给,衷心希望博主能长期维护这个项目,最后祝博主生活愉快!

  10. mjj
    1年前
    2022-11-14 17:36:36

    大佬,可以新增国内的Captcha吗?比如网易云盾或者腾讯防水墙?

发送评论 编辑评论


上一篇
下一篇