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 语言) 下,截图纪念一下。

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

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

后续计划

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

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

评论

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

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

    • ADD-SP 博主
      3月前
      2021-3-31 20:44:31

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

  2. max
    4周前
    2021-5-28 9:50:21

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

    • ADD-SP 博主
      4周前
      2021-5-28 10:06:10

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

    • max
      4周前
      2021-5-28 11:21:16

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

    • ADD-SP 博主
      4周前
      2021-5-28 11:23:06

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

    • max
      4周前
      2021-5-28 11:26:28

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

    • ADD-SP 博主
      4周前
      2021-5-28 11:27:54

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

    • max
      4周前
      2021-5-28 11:31:25

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

    • ADD-SP 博主
      4周前
      2021-5-28 11:33:46

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

    • max
      4周前
      2021-5-28 11:37:13

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

    • ADD-SP 博主
      已编辑
      4周前
      2021-5-28 11:54:52

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

    • max
      4周前
      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” ,,最后替代一下动态模块

    • ADD-SP 博主
      4周前
      2021-5-28 16:56:41

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

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

    • max
      4周前
      2021-5-28 16:59:07

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

    • ADD-SP 博主
      已编辑
      4周前
      2021-5-28 17:00:41

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

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

    • max
      4周前
      2021-5-28 17:04:35

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

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

    • ADD-SP 博主
      4周前
      2021-5-28 17:09:38

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

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

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

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

    • max
      4周前
      2021-5-28 17:17:12

      明白了,,多谢大佬

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

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

    • ADD-SP 博主
      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

    • ADD-SP 博主
      3周前
      2021-6-04 17:08:35

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

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

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

    • ADD-SP 博主
      3周前
      2021-6-04 17:26:57

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

    • leo
      3周前
      2021-6-04 17:28:15

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

    • ADD-SP 博主
      3周前
      2021-6-04 17:50:44

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

  6. kp
    2周前
    2021-6-07 5:43:55

    点个赞

发送评论 编辑评论


				
上一篇
下一篇