背景

之前网站一直没什么安全性的措施(因为我的假设是没人会来看我的网站的……),直到前段时间服务器被某个高一的 dalao D 得实在不行了…… 装了个 Wordfence Security 插件保护 WordPress,但是要全站的保护还是得从 nginx 入手……

于是花了半个下午的时间研究 Nginx 限流功能的配置
去网上搜索一大堆教程全都是抄来抄去的,找到有点质量的文章真是太不容易了……

之前用的 AppNode 也开过 nginx IP 限流(方便得多……),一分钟内请求超过指定次数就会显示一个静态页面,从而在一定程度上防范 DoS 或者 CC 攻击。但是使用过程中一个很严重的问题就是对搜索引擎不友好,Google、百度都显示抓取失败了。

三种方式

目前判断搜索引擎爬虫的似乎主要有三种方式:

通过 IP 判断缺点很明显,首先谷歌官方说了人家没有什么爬虫的 IPlist:Verifying Googlebot - Search Console Help

Google doesn't post a public list of IP addresses for webmasters to whitelist. This is because these IP address ranges can change, causing problems for any webmasters who have hard-coded them, so you must run a DNS lookup as described next.

(百度肯定更没有了)
网上去搜索 Google、百度的爬虫 IP 列表,全都是复制来复制去的几年前的内容(吐血)
Github 上逛一圈好像也没找到什么维护 IPlist 的项目,自己维护工作量极大,不太可能……


谷歌推荐的爬虫验证方式是 DNS 反查(rDNS):

  1. Run a reverse DNS lookup on the accessing IP address from your logs, using the host command.
  2. Verify that the domain name is in either googlebot.com or google.com
  3. Run a forward DNS lookup on the domain name retrieved in step 1 using the host command on the retrieved domain name. Verify that it is the same as the original accessing IP address from your logs.

百度的这个文档也说:

站长可以通过 DNS 反查 IP 的方式判断某只 spider 是否来自百度搜索引擎。根据平台不同验证方法不同,如 linux/windows/os 三种平台下的验证方法分别如下:

  1. 在 linux 平台下,您可以使用 host ip 命令反解 ip 来判断是否来自 Baiduspider 的抓取。Baiduspider 的 hostname 以 *.baidu.com*.baidu.jp 的格式命名,非 *.baidu.com*.baidu.jp 即为冒充。

历经千辛万苦找到了 nginx 有一个现成的插件:rDNS
但是 V2EX 上 dalao 们的讨论又泼了盆冷水:如何验证百度蜘蛛
划重点:rDNS 非常消耗资源,而且也可以伪造……

解析记录有 2 种 一种正向 一种反向
反向的意思 就是 IP 解析到域名
这个前提是你要有这个权限 就是有 ASN 并且 IP 是你自己的 或者运营商愿意提供权限(正常情况下是不允许 管理机构有要求)不然你解析什么?
这个和正向一样 也是 DNS 解析记录 只是反过来了
你域名解析到 IP 域名最少你要有管理权限是吧?一个道理
你域名有解析权限了 也可以解析到任意 IP 比如百度?比如谷歌?还是一个道理 反向解析也一样

不过更重要的是:nginx 安装第三方模块是要重新编译整个程序的,太麻烦了……


那么只剩下一种可行的方法:UA 判断。
缺点很明显:很容易伪造!!!在谷歌官方的 UA 列表页面,它给你了一个 Warning:

These values can be spoofed. If you need to verify that the visitor is Googlebot, you should use reverse DNS lookup.

但是至少是一种简单易行的方法……
探索了这么长时间就是想水篇博客……

前置知识

推荐一篇博客,介绍 nginx 限流很详细。

nginx 有一个取出 UA 的全局变量 $http_user_agent

nginx 配置文件中的 if 语句,条件中比较运算符用法:
两边字串严格相等;~* 两边字串不分大小写地相等;
!~ 两边字串严格不相等;!~* 两边字串不区分大小写地不相等。

UA 判断

谷歌官方说它的 UA 列表是:

Overview of Google crawlers (user agents) - Search Console Help

这 UA 也太多了……那就直接检测包含 Googlebot Baiduspider 子串的好了……
写个 if 判断一下吧:(正则表达式令人头疼)

if ($http_user_agent !~ .*(Googlebot|Baiduspider).*)
{
    limit_req zone=reqlim burst=10 nodelay;
}

reload 之后,nginx 会无情地告诉你:

nginx: [emerg] "limit_req" directive is not allowed here in /usr/local/nginx/conf/vhost/skywt.cn.conf:69

「If」 is evil

参见这个页面:If Is Evil | NGINX。「if 不是像你想的那样工作的!」

The only 100% safe things which may be done inside if in a location context are:

Anything else may possibly cause unpredictable behaviour, including potential SIGSEGV.

根据文档里的建议,我们只能写成这种形式:

location /{
    error_page 418 = @human;
    recursive_error_pages on;
    if ($http_user_agent !~ .*(Googlebot|Baiduspider).*)
    {
        return 418;
    }
}

location @human
{
    limit_req zone=reqlim burst=10 nodelay;
}

(注意如果装了 wordpress 应该要在 @robotslocation / 最后加一句 try_files $uri $uri/ /index.php?$args;
@ 开头的路径用于处理内部重定向。
HTTP 418 是「I‘m a teapot」错误(算是 HTTP 协议的一个彩蛋??),所以这么写(应该)不会有影响。

配置并发限制也是类似。

本地测试

用 Apache Benchmark 进行压力测试。
先发 30 个请求,假装我们是搜索引擎爬虫:

ab -n 30 -c 1 -H "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://skywt.cn/

返回信息:

Complete requests:      30
Failed requests:        0

表示服务器没有做限流,允许了全部请求。
(通过这个例子可以知道伪造一个 UA 是多么容易(吐血))

再随便搞个 UA 尝试:

ab -n 30 -c 1 -H "User-Agent:Mozilla/5.0 (X11; Linux i686; rv:70.0) Gecko/20100101 Firefox/70.0" https://skywt.cn/

输出:

Complete requests:      30
Failed requests:        15
   (Connect: 0, Receive: 0, Length: 15, Exceptions: 0)
Non-2xx responses:      15

15 次请求被拒绝了。

后记

改天还是研究下 rDNS 吧,用了 UA 检测了攻击者还是可以继续攻击……
(况且我还发了篇这么详细的博客讲述我的配置……)