nginx

使用NGINX的limit_req_zone和limit_req针对IP进行速率限制防止DDos

参考:
https://www.nginx.com/blog/rate-limiting-nginx/
https://chenyongjun.vip/articles/81
https://haicoder.net/note/nginx-interview/nginx-interview-nginx-limit-ip-frequency.html
https://www.65535.fun/?p=530
https://www.w3help.cc/a/202109/892265.html
https://ithelp.ithome.com.tw/articles/10269559

NGINX 最有用但经常被误解和错误配置的功能之一是速率限制。它允许您限制用户在给定时间段内可以发出的 HTTP 请求数量。请求可以像GET对网站主页的POST请求或对登录表单的请求一样简单。

速率限制可用于安全目的,例如减缓暴力密码猜测攻击。它可以通过将传入请求率限制为真实用户的典型值来帮助抵御 DDoS 攻击,并且(通过日志记录)识别目标 URL。更一般地,它用于保护上游应用服务器不会同时被过多的用户请求淹没。

在这篇博客中,我们将介绍 NGINX 速率限制的基础知识以及更高级的配置。速率限制在 NGINX Plus 中的工作方式相同。

NGINX Plus R16及更高版本支持“全局速率限制”:集群中的 NGINX Plus 实例对传入请求应用一致的速率限制,无论请求到达集群中的哪个实例。(集群中的状态共享也可用于其他 NGINX Plus 功能。)

NGINX 速率限制是如何工作的

NGINX 速率限制使用漏桶算法,该算法广泛用于电信和分组交换计算机网络,以在带宽有限时处理突发事件。以桶为例,水从顶部倒入,从底部泄漏;如果倒入水的速度超过漏水的速度,水桶就会溢出。在请求处理方面,水代表来自客户端的请求,而桶代表一个队列,请求根据先进先出(FIFO)调度算法等待处理。漏水代表请求退出缓冲区供服务器处理,溢出代表被丢弃且从未服务的请求。

配置基本限速
速率限制配置有两个主要指令,limit_req_zone和limit_req,如本例所示:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
 
server {
    location /login/ {
        limit_req zone=mylimit;
        
        proxy_pass http://my_upstream;
    }
}

该limit_req_zone指令定义了速率限制的参数,同时limit_req在它出现的上下文中启用速率限制(在示例中,对于/login/ 的所有请求)。

该limit_req_zone指令通常在http块中定义,使其可用于多个上下文。它需要以下三个参数:

键 – 定义应用限制的请求特征。在示例中,它是 NGINX 变量$binary_remote_addr,它保存客户端 IP 地址的二进制表示。这意味着我们将每个唯一的 IP 地址限制为由第三个参数定义的请求速率。(我们使用这个变量是因为它比客户端 IP 地址的字符串表示占用更少的空间,$remote_addr)。

区域 – 定义用于存储每个 IP 地址状态及其访问请求限制 URL 的频率的共享内存区域。将信息保存在共享内存中意味着它可以在 NGINX 工作进程之间共享。定义有两部分:由zone=关键字标识的区域名称和冒号后面的大小。大约 16,000 个 IP 地址的状态信息需要 1 兆字节,因此我们的区域可以存储大约 160,000 个地址。

如果 NGINX 需要添加新条目时存储空间耗尽,它会删除最旧的条目。如果释放的空间仍然不足以容纳新记录,NGINX 返回状态代码。此外,为了防止内存耗尽,每次 NGINX 创建一个新条目时,它最多会删除两个在前 60 秒内未使用的条目。503 (Service Temporarily Unavailable)

速率 – 设置最大请求速率。在示例中,速率不能超过每秒 10 个请求。NGINX 实际上以毫秒为粒度跟踪请求,因此此限制对应于每 100 毫秒 (ms) 1 个请求。因为我们不允许突发(请参阅下一节),这意味着如果请求在前一个允许的请求之后不到 100 毫秒到达,则该请求将被拒绝。
该limit_req_zone指令设置了速率限制和共享内存区域的参数,但它实际上并不限制请求速率。为此,您需要通过在那里包含指令来将限制应用于特定location或server块limit_req。在示例中,我们将请求速率限制为/login/。

因此,现在每个唯一的 IP 地址被限制为每秒 10 个/login/请求 ——或者更准确地说,不能在前一个 URL 的 100 毫秒内发出对该 URL 的请求。

处理突发
如果我们在 100 毫秒内收到 2 个请求会怎样?对于第二个请求,NGINX503向客户端返回状态代码。这可能不是我们想要的,因为应用程序在本质上往往是突发的。相反,我们希望缓冲任何多余的请求并及时为它们提供服务。这是我们使用burst参数 to 的地方limit_req,就像在这个更新的配置中一样:

location /login/ {
    limit_req zone=mylimit burst=20;
 
    proxy_pass http://my_upstream;
}

该burst参数定义了客户端可以超过区域指定速率的请求数(对于我们的示例mylimit区域,速率限制为每秒 10 个请求,或每 100 毫秒 1 个)。在前一个请求之后 100 毫秒内到达的请求被放入队列中,这里我们将队列大小设置为 20。

这意味着如果 21 个请求同时从给定的 IP 地址到达,NGINX 会立即将第一个请求转发到上游服务器组,并将其余 20 个请求放入队列中。然后它每 100 毫秒转发一个排队的请求,并且503仅当传入的请求使排队的请求数超过 20 时才返回给客户端。

无延迟排队
burst可以使流量顺畅流动的配置,但不是很实用,因为它会使您的站点看起来很慢。在我们的示例中,队列中的第 20 个数据包等待 2 秒才能转发,此时对它的响应可能不再对客户端有用。要解决这种情况,请添加nodelay参数以及burst参数:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    proxy_pass http://my_upstream;
}

使用该nodelay参数,NGINX 仍然根据该burst参数在队列中分配槽位并强加配置的速率限制,但不会通过间隔转发排队请求。相反,当一个请求“太快”到达时,只要队列中有一个可用的插槽,NGINX 就会立即转发它。它将该插槽标记为“已占用”,并且在适当的时间过去之前(在我们的示例中,在 100 毫秒之后)不会释放它以供另一个请求使用。

和之前一样,假设 20 槽队列是空的,并且 21 个请求同时从给定的 IP 地址到达。NGINX 立即转发所有 21 个请求,并将队列中的 20 个插槽标记为已占用,然后每 100 毫秒释放 1 个插槽。(如果有 25 个请求,NGINX 会立即转发其中的 21 个,将 20 个插槽标记为已占用,并拒绝 4 个状态为 的请求 503。)

现在假设在第一组请求转发后 101 毫秒,另外 20 个请求同时到达。队列中只有 1 个插槽已被释放,因此 NGINX 转发 1 个请求并拒绝其他 19 个状态为 503。如果在 20 个新请求到达之前 501 毫秒过去了,则有 5 个空闲槽,因此 NGINX 立即转发 5 个请求并拒绝 15 个。

其效果相当于每秒 10 个请求的速率限制。nodelay如果您想在不限制请求之间允许的间距的情况下施加速率限制,则该选项很有用。

注意:对于大多数部署,我们建议 在指令中包含burst和nodelay参数limit_req。

两阶段限速
使用 NGINX Plus R17 或 NGINX Open Source 1.15.7,您可以将 NGINX 配置为允许突发请求以适应典型的 Web 浏览器请求模式,然后将额外的过度请求限制到一定程度,超过该点的额外过度请求将被拒绝。使用指令的delay参数启用两阶段速率限制limit_req。

为了说明两阶段速率限制,我们在这里配置 NGINX 以通过施加每秒 5 个请求 (r/s) 的速率限制来保护网站。网站通常每页有 4 到 6 个资源,并且永远不会超过 12 个资源。该配置允许最多 12 个请求的突发,其中前 8 个请求被无延迟地处理。在执行 5 r/s 限制的 8 次过度请求后添加延迟。超过 12 个请求后,任何进一步的请求都会被拒绝。

limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s;

server {
    listen 80;
    location / {
        limit_req zone=ip burst=12 delay=8;
        proxy_pass http://website;
    }
}

前 8 个请求( 的值delay)由 NGINX Plus 毫不延迟地代理。接下来的 4 个请求 ( burst – delay) 被延迟,以便不超过定义的 5 r/s 速率。接下来的 3 个请求被拒绝,因为已超过总突发大小。后续请求被延迟。

高级配置举例
通过将基本速率限制与其他 NGINX 功能相结合,您可以实现更细微的流量限制。

列入许可名单
此示例展示了如何对来自不在“允许名单”中的任何人的请求施加速率限制。

geo $limit {
    default 1;
    10.0.0.0/8 0;
    192.168.0.0/24 0;
}
 
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
 
limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
 
server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;
 
        # ...
    }
}

此示例同时使用了geo和map指令。该geo块为许可名单中的 IP 地址和所有其他地址 分配值 0to 。然后我们使用映射将这些值转换为键,例如:$limit1

如果$limit是0,$limit_key则设置为空字符串
如果$limit是1,$limit_key则设置为二进制格式的客户端IP地址
将两者放在一起,$limit_key对于列入许可名单的 IP 地址设置为空字符串,否则设置为客户端的 IP 地址。当limit_req_zone目录的第一个参数(键)为空字符串时,不应用限制,因此允许列入的 IP 地址(在 10.0.0.0/8 和 192.168.0.0/24 子网中)不受限制。所有其他 IP 地址限制为每秒 5 个请求。

该limit_req指令将限制应用于/位置,并允许超过配置限制的最多 10 个数据包的突发,转发没有延迟

limit_req在一个位置包含多个指令
您可以limit_req在一个位置包含多个指令。应用与给定请求匹配的所有限制,这意味着使用最严格的限制。例如,如果多个指令施加延迟,则使用最长的延迟。同样,如果这是任何指令的效果,请求也会被拒绝,即使其他指令允许它们通过。

扩展前面的示例,我们可以对许可名单上的 IP 地址应用速率限制:

http {
    # ...
 
    limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;
 
    server {
        # ...
        location / {
            limit_req zone=req_zone burst=10 nodelay;
            limit_req zone=req_zone_wl burst=20 nodelay;
            # ...
        }
    }
}

许可名单上的 IP 地址与第一个速率限制 ( req_zone ) 不匹配,但与第二个 ( req_zone_wl )匹配,因此限制为每秒 15 个请求。不在许可名单中的 IP 地址与两个速率限制均匹配,因此适用更严格的限制:每秒 5 个请求。

配置相关功能
日志记录
默认情况下,NGINX 会记录由于速率限制而延迟或丢弃的请求,如下例所示:

2022/01/06 04:20:00 [error] 120315#0: *32086 limiting requests, excess: 1.000 by zone "mylimit", client: 192.168.1.2, server: nginx.com, request: "GET / HTTP/1.0", host: "nginx.com"

日志条目中的字段包括:

2021/06/13 04:20:00 – 写入日志条目的日期和时间
[error] – 严重程度
120315#0 – NGINX worker的进程ID和线程ID,用#符号隔开
*32086 – 受速率限制的代理连接的 ID
limiting requests – 日志条目记录速率限制的指示符
excess – 超过此请求所代表的配置速率的每毫秒请求数
zone – 定义强制速率限制的区域
client – 发出请求的客户端的 IP 地址
server – 服务器的 IP 地址或主机名
request – 客户端发出的实际 HTTP 请求
host – HostHTTP 标头的值
默认情况下,NGINX 在error级别记录拒绝的请求,如上[error]例所示。(它在低一级记录延迟请求,所以warn默认情况下。)要更改日志记录级别,请使用limit_req_log_level指令。这里我们在warn级别设置拒绝的日志请求:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn; 
    proxy_pass http://my_upstream;
}

发送给客户端的错误代码
默认情况下,当客户端超过其速率限制时,NGINX 以状态代码503 ( Service Temporarily Unavailable)响应。使用limit_req_status指令设置不同的状态代码(444在本例中):

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_status 444;
}

拒绝对特定位置的所有请求
如果您想拒绝对特定 URL 的所有请求,而不仅仅是限制它们,请location为其配置一个块并包含指令:deny all

location /foo.php {
deny all;
}