引擎限速-慢速攻击的思考

  今天处理了一个限速&&慢速攻击的问题! 现在来对这个问题做一个整体的分析与 回顾

原理

是以极低的速度往服务器发送HTTP请求。由于Web Server对于并发的连接数都有一定的上限,因此若是恶意地占用住这些连接不释放,那么Web Server的所有连接都将被恶意连接占用,从而无法接受新的请求,导致拒绝服务。

3种攻击方式

  • Slow headers(也称slowloris):Web应用在处理HTTP请求之前都要先接收完所有的HTTP头部,Web服务器再没接收到2个连续的\r\n时,会认为客户端没有发送完头部,而持续的等等客户端发送数据,消耗服务器的连接和内存资源。
  • Slow body(也称Slow HTTP POST):攻击者发送一个HTTP POST请求,该请求的Content-Length头部值很大,使得Web服务器或代理认为客户端要发送很大的数据。服务器会保持连接准备接收数据,但攻击客户端每次只发送很少量的数据,使该连接一直保持存活,消耗服务器的连接和内存资源。
  • Slow read(也称Slow Read attack):客户端与服务器建立连接并发送了一个HTTP请求,客户端发送完整的请求给服务器端,然后一直保持这个连接,以很低的速度读取Response,比如很长一段时间客户端不读取任何数据,通过发送Zero Window到服务器,让服务器误以为客户端很忙,直到连接快超时前才读取一个字节,以消耗服务器的连接和内存资源。

漏洞验证

使用工具:slowhttptest

SlowHTTPTest是一款对服务器进行慢攻击的测试软件,所谓的慢攻击就是相对于cc或者DDoS的快而言的,并不是只有量大速度快才能把服务器搞挂,使用慢攻击有时候也能到达同一效果。slowhttptest包含了之前几种慢攻击的攻击方式,包括slowloris, Slow HTTP POST, Slow Read attack等。那么这些慢攻击工具的原理就是想办法让服务器等待,当服务器在保持连接等待时,自然就消耗了资源。

一般修复方案;

  • 限制 HTTP 数据的大小
  • 设定URL白名单和黑名单,识别坏的IP
  • 定义最小的输入数据速率
  • 对请求的数据解析时间进行计数,超过限定时间后认为可能为攻击,可以关闭链接。
  • 对web服务器的http头部传输的最大许可时间进行限制,修改成最大许可时间为5秒
  • 限制同一ip同时建立的连接数

涉及到公司的实现方式就不写了, 实际上 限速这个东西大同小异

 核心是 限速

首先限速:何为限速?? 要限制什么的速度??

所以这就需求的问题了?

那限制什么的速率呢?以什么为参考点展开进行思考呢?首先看报文! 这是我们最先能拿到的东西;那么报文来了我们应该怎样看呢?

很快就想到了tcp分层;所以 以什么为参考点展开进行思考呢?---->ip  port http各个字段

限速就是要:限制某个ip、某个端口 、某个http中get post  host 等出现的次数、报文交互量、c/s交互时间、网络流量、cpu 使用率 内存等了

限速实现

目前都是多核编程,一般使用多线程 多进程利用多核提升性能,但是限速都是 针对全局统计,所以肯定会涉及到多线程访问同一个变量的问题

来看一下 内核 以及nginx 等对 “统计总数据”的实现

 内核:

先看ip数据包文统计实现:

__TCP_INC_STATS(net, TCP_MIB_INSEGS);
__SNMP_INC_STATS((net)->mib.tcp_statistics, field)
#define __SNMP_INC_STATS(mib, field)    \
            __this_cpu_inc(mib->mibs[field])
#define __this_cpu_inc(pcp)        __this_cpu_add(pcp, 1)
#define __this_cpu_add(pcp, val)                    \
({                                    \
    __this_cpu_preempt_check("add");                \
    raw_cpu_add(pcp, val);                        \
})

#define raw_cpu_add(pcp, val)        __pcpu_size_call(raw_cpu_add_, pcp, val)

#define __pcpu_size_call(stem, variable, ...)                \
do {                                    \
    __verify_pcpu_ptr(&(variable));                    \
    switch(sizeof(variable)) {                    \
        case 1: stem##1(variable, __VA_ARGS__);break;        \
        case 2: stem##2(variable, __VA_ARGS__);break;        \
        case 4: stem##4(variable, __VA_ARGS__);break;        \
        case 8: stem##8(variable, __VA_ARGS__);break;        \
        default:                         \
            __bad_size_call_parameter();break;        \
    }                                \
} while (0)

可以看到是per-cpu变量存储当前数据。

也就是使用local 变量,没有使用global 变量

那如果要是需要知道到底有多少 TCP_MIB_INSEGS数据呢?

unsigned long snmp_fold_field(void __percpu *mib, int offt)
{
    unsigned long res = 0;
    int i;

    for_each_possible_cpu(i)
        res += snmp_get_cpu_field(mib, i, offt);
    return res;
}

也就是 遍历每个per_cpu 数据, 将各个local 变量加在一起 !!

但是此时对数据精度要求不高,

那么nginx 是怎样处理的呢?

Nginx:

 1.限流算法

最简单粗暴的限流算法就是计数器法了,而比较常用的有漏桶算法和令牌桶算法;

1.限流算法

最简单粗暴的限流算法就是计数器法了,而比较常用的有漏桶算法和令牌桶算法;


作者:陈雷
链接:https://www.imooc.com/article/91383
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作

1.限流算法

最简单粗暴的限流算法就是计数器法了,而比较常用的有漏桶算法和令牌桶算法;


作者:陈雷
链接:https://www.imooc.com/article/91383
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作

1.1计数器

计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。

那么我们我们可以设置一个计数器counter,其有效时间为1分钟(即每分钟计数器会被重置为0),每当一个请求过来的时候,counter就加1,如果counter的值大于100,就说明请求数过多;

这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题。

如下图所示,在1:00前一刻到达100个请求,1:00计数器被重置,1:00后一刻又到达100个请求,显然计数器不会超过100,所有请求都不会被拦截;

 

1.2 漏桶算法

如下图所示,水(请求)从上方倒入水桶,从水桶下方流出(被处理);来不及流出的水存在水桶中(缓冲),以固定速率流出;

水桶满后水溢出(丢弃)。可以看到漏桶算法天生就限制了请求的速度,可以用于流量整形和限流控制;

这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃

 

1.3 令牌桶算法

  令牌桶是一个存放固定容量令牌的桶,按照固定速率r往桶里添加令牌;桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃;

当一个请求达到时,会尝试从桶中获取令牌;如果有,则继续处理请求;如果没有则排队等待或者直接丢弃;请求要消耗等比例的令牌才能被处理;

  相比漏桶算法,令牌桶算法不同之处在于它不但有一只“桶”,还有个队列,这个桶是用来存放令牌的,队列才是用来存放请求的。从作用上来说,漏桶和令牌桶算法最明显的区别就是是否允许突发流量(burst)的处理,漏桶算法能够强行限制数据的实时传输(处理)速率,对突发流量不做额外处理;而令牌桶算法能够在限制数据的平均传输速率的同时允许某种程度的突发传输。

 

可知:漏桶算法的流出速率恒定或者为0,而令牌桶算法的流出速率却有可能大于r

为了防止出现计数器的“错误”,目前对统计计数时间精度要求高

  • 比如:限制了每个IP访问的速度为2r/s,使用单个IP在10ms内发并发送了6个请求,只有1个成功,剩下的5个都被拒绝。设置的速度是2r/s,转换一下就是500ms内单个IP只允许通过1个请求,从501ms开始才允许通过第二个请求。

对Nginx 相关数据结构进行分析:

 ngx 流程图

 

 

 

 这个结构体的主要功能是用来创建三个部分( main server location )的配置和合并配置。主要包括以下几个函数:

(1)读入配置文件前调用;

2)读入配置文件后调用;

(3)创建全局部分配置时调用;

(4)初始化全局部分配置时调用;

(5)创建主机部分配置时调用;

(6)与全局部分配置合并时调用;

7)创建位置部分配置时调用;

8)与主机部分配置合并时调用;

而在limit_req模块我们关注的是(2)、(7)和(8)。模块的处理顺序是(7)->(8)->(2),create函数用来为特定的位置部分的配置结构体分配内存,merge函数用来设定默认值和与继承过来的配置合并,这个合并函数还要负责检验读入的数值是否有效,postconfig函数用来初始化处理函数。

在本模块中,create、merge函数的主要功能是:对模块的日志级别进行设置。

 模块指令(ngx_command_t

 

ngx_http_limit_req_zone的主要功能:

(1)解析配置文件的命令参数:name、size(K/M)、scale(r/s、r/m)、rate

(2)申请配置文件空间ngx_http_limit_req_ctx_t,填充参数;

(3)申请共享内存,并初始化ngx_http_limit_req_init_zone

请求的处理函数逻辑

主要逻辑为

  • lrcf = ngx_http_get_module_loc_conf(r, ngx_http_limit_req_module),读取位置部分的配置到lrcf中;
  • vv = ngx_http_get_indexed_variable(r, ctx->index),根据索引得到变量值如$binary_remote_addr
  • ngx_http_limit_req_expire(ctx, 1),删除too old的节点;
  • rc=ngx_http_limit_req_lookup(lrcf, hash, vv->data, len, &excess),查找客户端节点信息,并得到相应的状态信息如NGX_BUSY、NGX_AGAIN、NGX_OK、NGX_ DECLINED
  • 根据rc的返回值,进行相应的处理;

其中最重要的逻辑在函数ngx_http_limit_req_lookup()中,这个函数主要流程是怎样呢?对于每一个请求:

  1. 从根节点开始查找红黑树,找到key对应的节点;
  2. 找到后修改该点在LRU队列中的位置,表示该点最近被访问过;
  3. 执行漏桶算法;
  4. 没找到时根据LRU淘汰,腾出空间;
  5. 生成并插入新的红黑树节点;
  6. 执行下一条限流规则。
static ngx_int_t
ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_t hash,
    ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account)
{
   ------
    ngx_rbtree_node_t          *node, *sentinel;
    ngx_http_limit_req_ctx_t   *ctx;
    ngx_http_limit_req_node_t  *lr;
    tp = ngx_timeofday();
    now = (ngx_msec_t) (tp->sec * 1000 + tp->msec);

    ctx = limit->shm_zone->data;

    node = ctx->sh->rbtree.root;
    sentinel = ctx->sh->rbtree.sentinel;

    while (node != sentinel) {

        if (hash < node->key) {node = node->left; continue; }//  从根节点开始查找红黑树

        if (hash > node->key) {node = node->right;continue;}
        /* hash == node->key */
        lr = (ngx_http_limit_req_node_t *) &node->color;
        rc = ngx_memn2cmp(key->data, lr->data, key->len, (size_t) lr->len);

        if (rc == 0) {// 找到
            ngx_queue_remove(&lr->queue);// 修改该点在LRU队列中的位置,表示该点最近被访问过
            ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
            ms = (ngx_msec_int_t) (now - lr->last);
            excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;// 执行漏桶算法
            if (excess < 0) {
                excess = 0;
            }
            *ep = excess;
            if ((ngx_uint_t) excess > limit->burst) { return NGX_BUSY;}// 超过了突发门限,拒绝

            if (account) {// 是否是最后一条规则
                lr->excess = excess;
                lr->last = now;
                return NGX_OK;// 未超过限制,通过
            }
            lr->count++;
            ctx->node = lr;
            return NGX_AGAIN; // 执行下一条限流规则
        }
        node = (rc < 0) ? node->left : node->right;
    }
    *ep = 0;
    size = offsetof(ngx_rbtree_node_t, color)
           + offsetof(ngx_http_limit_req_node_t, data)
           + key->len;
// not found
    ngx_http_limit_req_expire(ctx, 1);// 根据LRU淘汰,腾出空间
    node = ngx_slab_alloc_locked(ctx->shpool, size);// 生成新的红黑树节点
 -------------------------
    node->key = hash;
    lr = (ngx_http_limit_req_node_t *) &node->color;
    lr->len = (u_short) key->len;
    lr->excess = 0;
    ngx_memcpy(lr->data, key->data, key->len);
    ngx_rbtree_insert(&ctx->sh->rbtree, node);// 插入该节点,重新平衡红黑树
    ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
    if (account) {
        lr->last = now;
        lr->count = 0;
        return NGX_OK;
    }
    lr->last = 0;
    lr->count = 1;
    ctx->node = lr;
    return NGX_AGAIN;//  执行下一条限流规则
}

  LRU算法的实现很简单,如果一个节点被访问了,那么就把它移到队列的头部,当空间不足需要淘汰节点时,就选出队列尾部的节点淘汰掉

漏桶算法是如何实现的

核心为:

excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000
待处理请求书-限流速率*时间段+1个请求(速率,请求数等都乘以1000了)

 

burst实现

  burst是为了应对突发流量的,偶然间的突发流量到达时,应该允许服务端多处理一些请求才行;
当burst为0时,请求只要超出限流速率就会被拒绝;当burst大于0时,超出限流速率的请求会被排队等待 处理,而不是直接拒绝;
排队过程如何实现?而且nginx还需要定时去处理排队中的请求;
ngx事件都有一个定时器,nginx是通过事件与定时器配合实现请求的排队与定时处理;

//计算当前请求还需要排队多久才能处理
delay = ngx_http_limit_req_account(limits, n, &excess, &limit);
 
//添加可读事件
if (ngx_handle_read_event(r->connection->read, 0) != NGX_OK) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
 
r->read_event_handler = ngx_http_test_reading;
r->write_event_handler = ngx_http_limit_req_delay; //可写事件处理函数
ngx_add_timer(r->connection->write, delay);    //可写事件添加定时器(超时之前是不能往客户端返回的)

 


计算delay的方法:就是遍历所有的限流策略,计算处理完所有待处理请求需要的时间,返回最大值;

ngx_http_limit_req_account(ngx_http_limit_req_limit_t *limits, ngx_uint_t n,
    ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit)
{
    ngx_int_t                   excess;
    ngx_time_t                 *tp;
    ngx_msec_t                  now, delay, max_delay;
    ngx_msec_int_t              ms;
    ngx_http_limit_req_ctx_t   *ctx;
    ngx_http_limit_req_node_t  *lr;

    excess = *ep;

    if (excess == 0 || (*limit)->nodelay) {
        max_delay = 0; //已经没有超出值剩余或者没有location配置了nodelay(意味着想立即处理) 

    } else {
        ctx = (*limit)->shm_zone->data; /*得到限制的上下文*/
        max_delay = excess * 1000 / ctx->rate;/*用计算得到的请求频率(注意由于取整被放大了1000倍) 以及剩余的值来算出最长的延时时间(毫秒)*/
    }

    while (n--) {
        ctx = limits[n].shm_zone->data;
        lr = ctx->node;/*取得location配置的上下文限制信息*/

        if (lr == NULL) {
            continue;
        }

        ngx_shmtx_lock(&ctx->shpool->mutex); /*加锁 设置节点的信息/

        tp = ngx_timeofday();

        now = (ngx_msec_t) (tp->sec * 1000 + tp->msec);
        ms = (ngx_msec_int_t) (now - lr->last);

        excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;/*获得剩余的请求数量*/

        if (excess < 0) { 
            excess = 0; /*这里的剩余值不得小于0*/
        }

        lr->last = now;
        lr->excess = excess;
        lr->count--;/*更新最近访问时间和剩余次数以及减少一次使用计数*/

        ngx_shmtx_unlock(&ctx->shpool->mutex);

        ctx->node = NULL;

        if (limits[n].nodelay) {
            continue;
        }

        delay = excess * 1000 / ctx->rate;/*通过剩余的次数与请求频率计算得到延时的时间*/

        if (delay > max_delay) {
            max_delay = delay;
            *ep = excess;
            *limit = &limits[n];
        }
    }

    return max_delay;
}

 

 

ngx_http_limit_req_handler(ngx_http_request_t *r)
{//ngx_http_limit_req_handler  会执行漏桶算法
    uint32_t                     hash;
    ngx_str_t                    key;
    ngx_int_t                    rc;
    ngx_uint_t                   n, excess;
    ngx_msec_t                   delay;
    ngx_http_limit_req_ctx_t    *ctx;
    ngx_http_limit_req_conf_t   *lrcf;
    ngx_http_limit_req_limit_t  *limit, *limits;

    if (r->main->limit_req_set) {/*请求已经被标记为请求限制 直接返回decline 拒绝进行后续的http处理流程*/
        return NGX_DECLINED;
    }

    lrcf = ngx_http_get_module_loc_conf(r, ngx_http_limit_req_module);
    limits = lrcf->limits.elts; /*取得location级别的限制数组*/

    excess = 0;

    rc = NGX_DECLINED;

#if (NGX_SUPPRESS_WARN)
    limit = NULL;
#endif

    for (n = 0; n < lrcf->limits.nelts; n++) {
        /*这里会遍历配置的限制数组(即ngx_http_limit_req_limit_t数组) */
        limit = &limits[n];

        ctx = limit->shm_zone->data;

        if (ngx_http_complex_value(r, &ctx->key, &key) != NGX_OK) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        if (key.len == 0) {
            continue;
        }

        if (key.len > 65535) {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "the value of the \"%V\" key "
                          "is more than 65535 bytes: \"%V\"",
                          &ctx->key.value, &key);
            continue;
        }
        /*用key值计算得到hash值*/
        hash = ngx_crc32_short(key.data, key.len);

        ngx_shmtx_lock(&ctx->shpool->mutex);
        /*这里会进行红黑树的查找及插入 */
        rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess,
                                       (n == lrcf->limits.nelts - 1));

        ngx_shmtx_unlock(&ctx->shpool->mutex);

        ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "limit_req[%ui]: %i %ui.%03ui",
                       n, rc, excess / 1000, excess % 1000);

        if (rc != NGX_AGAIN) {
            break;
        }
    }

    if (rc == NGX_DECLINED) {
        return NGX_DECLINED; /*没有设置location级别的请求限制 直接返回decline*/
    }

    r->main->limit_req_set = 1;/*主请求标记 为已经设置过限制*/

    if (rc == NGX_BUSY || rc == NGX_ERROR) {
        /*前面处理出错或者达到了限制的条件 */

        if (rc == NGX_BUSY) {
            ngx_log_error(lrcf->limit_log_level, r->connection->log, 0,
                          "limiting requests, excess: %ui.%03ui by zone \"%V\"",
                          excess / 1000, excess % 1000,
                          &limit->shm_zone->shm.name);
        }

        while (n--) {
            ctx = limits[n].shm_zone->data;
 /*
             发现上下文中的红黑树节点是空的 跳过
             node正常情况下会从ngx_http_limit_req_lookup函数中得到设置得到
*得到每一个location的限制上下文*/

            if (ctx->node == NULL) {
                continue;
            }

            ngx_shmtx_lock(&ctx->shpool->mutex);
            /*由于需要改变共享内存中红黑树节点的属性(引用计数) 用于同步worker进程 加锁*/
            ctx->node->count--;

            ngx_shmtx_unlock(&ctx->shpool->mutex);
            /*将请求限制的node指针置为空 请求限制上下文就是ngx_http_limit_req_ctx_t*/
            ctx->node = NULL;
        }

        return lrcf->status_code;//返回nginx配置文件中配置的状态码
    }
    /*上面的处理是达到了设置限制的条件*/

    /* rc == NGX_AGAIN || rc == NGX_OK */

    if (rc == NGX_AGAIN) {
        excess = 0;
    }
//计算得到需要的延时时间
    delay = ngx_http_limit_req_account(limits, n, &excess, &limit);

    if (!delay) {
        return NGX_DECLINED;
    }

    ngx_log_error(lrcf->delay_log_level, r->connection->log, 0,
                  "delaying request, excess: %ui.%03ui, by zone \"%V\"",
                  excess / 1000, excess % 1000, &limit->shm_zone->shm.name);

    if (ngx_handle_read_event(r->connection->read, 0, NGX_FUNC_LINE) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    /*设置读写事件处理函数*/
    r->read_event_handler = ngx_http_test_reading;
    r->write_event_handler = ngx_http_limit_req_delay;
      /*写事件延时标志设置增加一个写事件的延时定时器*/
    ngx_add_timer(r->connection->write, delay, NGX_FUNC_LINE);

    return NGX_AGAIN;
}

 

ngx_http_limit_req_expire(ngx_http_limit_req_ctx_t *ctx, ngx_uint_t n)
{
    ...
    //取得当前的时间    
    now = ngx_current_msec;
    /*
    1. n==1删除一个或者2个请求频率为0的节点
    2. n==0会强制删除一个LRU最老的节点以及一个或者2个请求频率为0的节点
    */
    while (n < 3) {

        if (ngx_queue_empty(&ctx->sh->queue)) { 
           /*LRU队列为空 意味着没有节点 也就无需进行淘汰处理 直接返回*/
            return;
        }
        /*取得最老的LRU节点*/
        q = ngx_queue_last(&ctx->sh->queue);
        /*得到LRU节点对应的红黑树节点 这里使用结构体成员与结构体地址的偏移量计算得到的*/
        lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);

        if (lr->count) {
            //统计的请求次数为空 直接返回
            return;
        }

        if (n++ != 0) {
           //n==1时第一次判定就会成立 而n==0时第一次判定不会成立
            /*得到时间差 并取得绝对值*/
            ms = (ngx_msec_int_t) (now - lr->last);
            ms = ngx_abs(ms);

            if (ms < 60000) {
                /*时间差小于60秒 也会直接返回*/
                return;
            }
            /*
            取得还能容忍请求的超出数量
            */
            excess = lr->excess - ctx->rate * ms / 1000;

            if (excess > 0) { //还有剩余 直接返回
                return;
            }
        }
        /*移除LRU队列中的节点*/
        ngx_queue_remove(q);
        /*通过结构体偏移量的计算得到红黑树节点地址*/
        node = (ngx_rbtree_node_t *)
                   ((u_char *) lr - offsetof(ngx_rbtree_node_t, color));
        /*删除红黑树节点*/
        ngx_rbtree_delete(&ctx->sh->rbtree, node);
        /*释放红黑树节点所占用的共享内存*/
        ngx_slab_free_locked(ctx->shpool, node);
    }
}

 

转载自:

https://www.cnblogs.com/CarpenterLee/p/8084533.html

https://www.cnblogs.com/wjoyxt/p/6128183.html

https://www.cnblogs.com/CarpenterLee/p/8084533.html

 https://blog.csdn.net/huzilinitachi/article/details/79694641

posted @ 2021-07-13 09:54  codestacklinuxer  阅读(173)  评论(0编辑  收藏  举报