IPVS之安全防御

初始化

IPVS在初始化时,使用内核delayed work机制启动一个处理任务。每1秒钟(DEFENSE_TIMER_PERIOD)执行一次。

/* Timer for checking the defense */
#define DEFENSE_TIMER_PERIOD    1*HZ

static int __net_init ip_vs_control_net_init_sysctl(struct netns_ipvs *ipvs)
{
    atomic_set(&ipvs->dropentry, 0);
	
	ipvs->sysctl_amemthresh = 1024;

    /* Schedule defense work */
    INIT_DELAYED_WORK(&ipvs->defense_work, defense_work_handler);
    schedule_delayed_work(&ipvs->defense_work, DEFENSE_TIMER_PERIOD);

防御处理

static void defense_work_handler(struct work_struct *work)
{
struct netns_ipvs *ipvs =
container_of(work, struct netns_ipvs, defense_work.work);

update_defense_level(ipvs);
if (atomic_read(&ipvs->dropentry))
    ip_vs_random_dropentry(ipvs);
schedule_delayed_work(&ipvs->defense_work, DEFENSE_TIMER_PERIOD);

}

PROC控制文件

可用内存阈值amemthresh

PROC文件:/proc/sys/net/ipv4/vs/amemthresh 用于设置可用内存的阈值,默认值为1024,单位是页面。如果系统可用内存值低于此阈值,IPVS系统即认为已无内存可用,其将尝试释放部分自身所占用的内存空间。

$ cat /proc/sys/net/ipv4/vs/amemthresh
1024

表项丢弃策略

PROC文件:/proc/sys/net/ipv4/vs/drop_entry用于设置处于无可用内存时IPVS丢弃表项的策略。默认值为0,表明关闭表项丢弃策略。将此值设置为3,表示总是开启此丢弃策略。值1和2为动态模式,当值设置为1时,一旦检测到内存不足,将进行表项丢弃,并将此值更改为2;否则,内存充足,不进行丢弃处理。当此值设置为2时,一旦检测到内存不足,同样将进行表项丢弃操作,反之如果内存充足,停止丢弃操作,并将drop_entry的值设置为1。

由以上描述可知,对于PROC文件drop_entry设置为1,2的情况,在内存不足时,drop_entry的值为2,当内存充足时,其值为1。

$ cat /proc/sys/net/ipv4/vs/drop_entry 
0

报文丢弃策略

PROC文件:/proc/sys/net/ipv4/vs/drop_packet用于设置处于可用内存不足时IPVS的报文丢弃策略,默认值为0,表明关闭报文丢弃策略。drop_packet的作用于上一节drop_entry类似,值1和2也是动态模式,值3表示总是开启报文丢弃策略。

对于PROC文件drop_packet设置为1,2的情况,在内存不足时,drop_packet的值为2,当内存充足时,其值为1。

$ cat /proc/sys/net/ipv4/vs/drop_packet 
0

和报文丢包丢弃相关的一个PROC文件为:/proc/sys/net/ipv4/vs/am_droprate,其定义了在drop_packet值为3时,所使用的的丢包率,默认为10,即丢弃十分之一的报文。

$ cat /proc/sys/net/ipv4/vs/am_droprate
10

当drop_packet的值为1或者2时,丢包率由以下公式计算而来:

rate = amemthresh / (amemthresh - available_memory)

安全TCP策略

PROC文件:/proc/sys/net/ipv4/vs/secure_tcp用于设置TCP使用一个不同的状态变迁表,对于NAT转发模式而言,可将TCP的Established状态延迟到三次握手完成之后。默认值为零,表明关闭secure_tcp功能。secure_tcp值的设置与上两节的drop_entry和drop_packet意义类似。值1和2也是动态模式,值3表示总是开启安全TCP策略。

对于PROC文件secure_tcp设置为1,2的情况,在内存不足时,secure_tcp的值为2,当内存充足时,其值为1。

$ cat /proc/sys/net/ipv4/vs/secure_tcp 
0

更新防御等级

函数update_defense_level负责防御等级的更新处理,其由delayed work每隔1秒钟调度一次。

可用内存计算

首先使用si_meminfo获得系统的空闲内存页面数量freeram和缓存页面数量bufferram,两者之和作为系统当前可用内存值(以页面为单位),此值如果小于PROC系统文件amemthresh指定的值,即认为内存已经不足。amemthresh默认值为1024。

static void update_defense_level(struct netns_ipvs *ipvs)
{
    struct sysinfo i;
    static int old_secure_tcp = 0;
    int to_change = -1;

    /* we only count free and buffered memory (in pages) */
    si_meminfo(&i);
    availmem = i.freeram + i.bufferram;

    nomem = (availmem < ipvs->sysctl_amemthresh);

drop_entry处理

以下逻辑根据PROC系统文件drop_entry和nomem来计算IPVS的dropentry值,dropentry表示是否执行连接表项丢弃操作。

    switch (ipvs->sysctl_drop_entry) {
    case 0:
        atomic_set(&ipvs->dropentry, 0);
        break;
    case 1:
        if (nomem) {
            atomic_set(&ipvs->dropentry, 1);
            ipvs->sysctl_drop_entry = 2;
        } else {
            atomic_set(&ipvs->dropentry, 0);
        }
        break;
    case 2:
        if (nomem) {
            atomic_set(&ipvs->dropentry, 1);
        } else {
            atomic_set(&ipvs->dropentry, 0);
            ipvs->sysctl_drop_entry = 1;
        };
        break;
    case 3:
        atomic_set(&ipvs->dropentry, 1);
        break;
    }

如下函数todrop_entry负责判断是否丢弃待定的连接表项。首先如果连接建立还未超过60秒,将不进行删除操作,以便保证正常的连接处理。

static inline int todrop_entry(struct ip_vs_conn *cp)
{
    /* The drop rate array needs tuning for real environments. Called from timer bh only => no locking
     */
    static const char todrop_rate[9] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
    static char todrop_counter[9] = {0};

    /* if the conn entry hasn't lasted for 60 seconds, don't drop it. This will leave enough time for normal connection to get through. */
    if (time_before(cp->timeout + jiffies, cp->timer.expires + 60*HZ))
        return 0;

如下示意图,

  create
   time           expires
                     |
    |     timeout    |  60s | 
    |----------------|------|
         |
         |----------------|
         |    timeout     |
        now
       jiffies
    |    |
    |<60s|

如果此连接接收到的报文数量不在[0, 8]之间,包括0和8,不执行丢弃,即没有接收到报文的连接与接收到大于8个报文的连接,可能在正常的使用不执行丢弃。反之,接下来将处理接收报文数量在[0,8]直接的连接。

以todrop_rate数组的第9个元素值8为例,如果连接接收的报文数量等于8个,在静态数组todrop_counter的第9个元素首次赋值为8之后,要一致等到执行8次之后,todrop_counter[8]才会再次小于等于零,返回1执行一次丢弃连接操作。

即对于连接的接收报文数量,数组todrop_rate指定了依据报文数量而定的丢弃速率。报文数量越多,丢弃率越低。

    /* Don't drop the entry if its number of incoming packets is not located in [0, 8] */
    i = atomic_read(&cp->in_pkts);
    if (i > 8 || i < 0) return 0;

    if (!todrop_rate[i]) return 0;
    if (--todrop_counter[i] > 0) return 0;

    todrop_counter[i] = todrop_rate[i];
    return 1;

drop_packet处理

以下逻辑根据PROC系统文件drop_packet和nomem来计算IPVS的drop_rate丢弃速率值。drop_rate值为空表示不执行报文丢弃操作。丢弃率drop_rate值的计算参考以上PROC文件am_droprate的介绍。

    switch (ipvs->sysctl_drop_packet) {
    case 0:
        ipvs->drop_rate = 0;
        break;
    case 1:
        if (nomem) {
            ipvs->drop_rate = ipvs->drop_counter 
                = ipvs->sysctl_amemthresh / (ipvs->sysctl_amemthresh-availmem);
            ipvs->sysctl_drop_packet = 2;
        } else {
            ipvs->drop_rate = 0;
        }
        break;
    case 2:
        if (nomem) {
            ipvs->drop_rate = ipvs->drop_counter
                = ipvs->sysctl_amemthresh / (ipvs->sysctl_amemthresh-availmem);
        } else {
            ipvs->drop_rate = 0;
            ipvs->sysctl_drop_packet = 1;
        }
        break;
    case 3:
        ipvs->drop_rate = ipvs->sysctl_am_droprate;
        break;
    }

报文丢弃的处理位于IPVS系统的各个协议的调度函数中,以UDP协议为例,其位于函数udp_conn_schedule中。如下的判断函数ip_vs_todrop。

static int udp_conn_schedule(struct netns_ipvs *ipvs, int af, struct sk_buff *skb,
          struct ip_vs_proto_data *pd, int *verdict, struct ip_vs_conn **cpp, struct ip_vs_iphdr *iph)
{
    if (svc) {
        if (ip_vs_todrop(ipvs)) {
            /* It seems that we are very loaded. We have to drop this packet :(
             */
            *verdict = NF_DROP;
            return 0;

当drop_rate不为零时,每当drop_counter小于等于零时,丢弃当前的报文。

static inline int ip_vs_todrop(struct netns_ipvs *ipvs)
{
    if (!ipvs->drop_rate)
        return 0;
    if (--ipvs->drop_counter > 0)
        return 0;
    ipvs->drop_counter = ipvs->drop_rate;
    return 1;

secure_tcp处理

以下逻辑根据PROC系统文件secure_tcp和nomem来计算IPVS的to_change值,to_change表示是否更改TCP所使用的状态迁移表。

    switch (ipvs->sysctl_secure_tcp) {
    case 0:
        if (old_secure_tcp >= 2)
            to_change = 0;
        break;
    case 1:
        if (nomem) {
            if (old_secure_tcp < 2)
                to_change = 1;
            ipvs->sysctl_secure_tcp = 2;
        } else {
            if (old_secure_tcp >= 2)
                to_change = 0;
        }
        break;
    case 2:
        if (nomem) {
            if (old_secure_tcp < 2)
                to_change = 1;
        } else {
            if (old_secure_tcp >= 2)
                to_change = 0;
            ipvs->sysctl_secure_tcp = 1;
        }
        break;
    case 3:
        if (old_secure_tcp < 2)
            to_change = 1;
        break;
    }
    old_secure_tcp = ipvs->sysctl_secure_tcp;
    if (to_change >= 0)
        ip_vs_protocol_timeout_change(ipvs, ipvs->sysctl_secure_tcp > 1);

函数ip_vs_protocol_timeout_change负责具体执行TCP状态转移表的切换。目前只有TCP协议注册了timeout_change指针函数。

void ip_vs_protocol_timeout_change(struct netns_ipvs *ipvs, int flags)
{
    struct ip_vs_proto_data *pd;

    for (i = 0; i < IP_VS_PROTO_TAB_SIZE; i++) {
        for (pd = ipvs->proto_data_table[i]; pd; pd = pd->next) {
            if (pd->pp->timeout_change)
                pd->pp->timeout_change(pd, flags);

TCP协议的timeout_change指针函数如下:tcp_timeout_change。如果开启,将TCP状态转换表设置为tcp_states_dos;否则,使用tcp_states表。

static void tcp_timeout_change(struct ip_vs_proto_data *pd, int flags)
{
    int on = (flags & 1);       /* secure_tcp */

    /*
    ** FIXME: change secure_tcp to independent sysctl var
    ** or make it per-service or per-app because it is valid
    ** for most if not for all of the applications. Something
    ** like "capabilities" (flags) for each object.
    */
    pd->tcp_state_table = (on ? tcp_states_dos : tcp_states);
}

连接表项丢弃处理

如在defense_work_handler函数中,如果update_defense_level函数将dropentry设置为非零值,将使用ip_vs_random_dropentry函数随机丢弃一定数量的连接表项。

static void defense_work_handler(struct work_struct *work)
{
    struct netns_ipvs *ipvs =
        container_of(work, struct netns_ipvs, defense_work.work);

    update_defense_level(ipvs);
    if (atomic_read(&ipvs->dropentry))
        ip_vs_random_dropentry(ipvs);

如下,在函数ip_vs_random_dropentry中,其随机选取1/32数量(ip_vs_conn_tab_size>>5)的哈希链表,随后遍历每个选定链表,获取候选丢弃表项。对于模板连接,如果存在子连接,或者其所属的虚拟服务未设置了OPS(One-Packet Scheduling),由于还在使用,不能丢弃此模板连接。否则,尝试进行丢弃操作。

/* Called from keventd and must protect itself from softirqs */
void ip_vs_random_dropentry(struct netns_ipvs *ipvs)
{
    struct ip_vs_conn *cp, *cp_c; 

    /* Randomly scan 1/32 of the whole table every second
     */
    for (idx = 0; idx < (ip_vs_conn_tab_size>>5); idx++) {
        unsigned int hash = prandom_u32() & ip_vs_conn_tab_mask;

        hlist_for_each_entry_rcu(cp, &ip_vs_conn_tab[hash], c_list) {
            if (cp->ipvs != ipvs)
                continue;
            if (cp->flags & IP_VS_CONN_F_TEMPLATE) {
                if (atomic_read(&cp->n_control) || !ip_vs_conn_ops_mode(cp))
                    continue;
                else
                    /* connection template of OPS */
                    goto try_drop;
            }

对于TCP协议,如果其状态为IP_VS_TCP_S_SYN_RECV或者IP_VS_TCP_S_SYNACK,表明TCP连接尚处在建立阶段,直接break出来进行丢弃。而对于处在IP_VS_TCP_S_ESTABLISHED状态的TCP协议类型的连接,使用函数todrop_entry判断是否可进行丢弃。另外对于其它TCP状态的连接,不进行处理,遍历下一个。

            if (cp->protocol == IPPROTO_TCP) {
                switch(cp->state) {
                case IP_VS_TCP_S_SYN_RECV:
                case IP_VS_TCP_S_SYNACK:
                    break;

                case IP_VS_TCP_S_ESTABLISHED:
                    if (todrop_entry(cp))
                        break;
                    continue;

                default:
                    continue;
                }

与以上的TCP协议类似,对于SCTP协议,如果其连接处于IP_VS_SCTP_S_INIT1或者IP_VS_SCTP_S_INIT状态,直接break出来进行丢弃处理。如果连接处于IP_VS_SCTP_S_ESTABLISHED状态,使用函数todrop_entry进行丢弃判断。其它SCTP协议状态的连接,不进行处理,遍历下一个。

            } else if (cp->protocol == IPPROTO_SCTP) {
                switch (cp->state) {
                case IP_VS_SCTP_S_INIT1:
                case IP_VS_SCTP_S_INIT:
                    break;
                case IP_VS_SCTP_S_ESTABLISHED:
                    if (todrop_entry(cp))
                        break;
                    continue;
                default:
                    continue;
                }

除了以上的TCP和SCTP协议之外,其它的协议统一使用函数todrop_entry进行丢弃判断。

            } else {
try_drop:
                if (!todrop_entry(cp))
                    continue;
            }

执行到此处的连接,表示要被进行丢弃。由函数ip_vs_conn_expire_now将连接的超时定时器更改为当下,最终由超时处理函数ip_vs_conn_expire执行连接的删除操作。

            IP_VS_DBG(4, "del connection\n");
            ip_vs_conn_expire_now(cp);
            cp_c = cp->control;
            /* cp->control is valid only with reference to cp */
            if (cp_c && __ip_vs_conn_get(cp)) {
                IP_VS_DBG(4, "del conn template\n");
                ip_vs_conn_expire_now(cp_c);
                __ip_vs_conn_put(cp);
            }

secure_tcp处理

static unsigned int ip_vs_in(struct netns_ipvs *ipvs, unsigned int hooknum, struct sk_buff *skb, int af)
{
    ip_vs_in_stats(cp, skb);
    ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pd);
    if (cp->packet_xmit)
        ret = cp->packet_xmit(skb, cp, pp, &iph);
        /* do not touch skb anymore */

函数ip_vs_set_state封装了IPVS支持的各个协议所注册的状态转换函数指针state_transition。

static inline void ip_vs_set_state(struct ip_vs_conn *cp, int direction, const struct sk_buff *skb, struct ip_vs_proto_data *pd)
{
    if (likely(pd->pp->state_transition))
        pd->pp->state_transition(cp, direction, skb, pd);

对于TCP协议而言,其为tcp_state_transition函数,核心的子函数为set_tcp_state。

static void tcp_state_transition(struct ip_vs_conn *cp, int direction, const struct sk_buff *skb, struct ip_vs_proto_data *pd)
{
    struct tcphdr _tcph, *th;

#ifdef CONFIG_IP_VS_IPV6
    int ihl = cp->af == AF_INET ? ip_hdrlen(skb) : sizeof(struct ipv6hdr);
#else
    int ihl = ip_hdrlen(skb);
#endif

    th = skb_header_pointer(skb, ihl, sizeof(_tcph), &_tcph);

    set_tcp_state(pd, cp, direction, th);

如下的set_tcp_state函数,由TCP协议转换表tcp_state_table推导出当前连接的下一个状态new_state,如果此新状态不是活跃的TCP状态,并且此连接之前为设置inactive标志,将为其设置IP_VS_CONN_F_INACTIVE标志,递减目的服务器的activeconns活跃连接计数,增加inactconns非活跃连接计数。反之亦然。

static inline void set_tcp_state(struct ip_vs_proto_data *pd, struct ip_vs_conn *cp, int direction, struct tcphdr *th)
{
    int new_state = IP_VS_TCP_S_CLOSE;
    int state_off = tcp_state_off[direction];

    if ((state_idx = tcp_state_idx(th)) < 0) {
        IP_VS_DBG(8, "tcp_state_idx=%d!!!\n", state_idx);
        goto tcp_state_out;
    }
    new_state = pd->tcp_state_table[state_off+state_idx].next_state[cp->state];

tcp_state_out:
    if (new_state != cp->state) {
        struct ip_vs_dest *dest = cp->dest;

        if (dest) {
            if (!(cp->flags & IP_VS_CONN_F_INACTIVE) && !tcp_state_active(new_state)) {
                atomic_dec(&dest->activeconns);
                atomic_inc(&dest->inactconns);
                cp->flags |= IP_VS_CONN_F_INACTIVE;
            } else if ((cp->flags & IP_VS_CONN_F_INACTIVE) && tcp_state_active(new_state)) {
                atomic_inc(&dest->activeconns);
                atomic_dec(&dest->inactconns);
                cp->flags &= ~IP_VS_CONN_F_INACTIVE;

以下为正常情况下的TCP状态转换表tcp_states,以及开启secure_tcp之后所使用的tcp_states_dos转换表。例如,在输入方向(TCP_DIR_INPUT),对于正常的tcp_states转换表,如果当前连接状态为sSR(SYN_RECV),那么下一个状态根据报文类型(SYN,FIN,ACK和RST)分别为(sSR,sTW,sES和sSR);而对于tcp_states_dos转换表,其下一个状态分别为(sSR,sTW,sSR和sSR),只有接收到FIN报文,其状态才会转换到sTW(TIME_WAIT),其它类型报文不引起状态变迁。目前还没有搞清楚这两张表的意义(…>_<…)。

static struct tcp_states_t tcp_states [] = {
/*  INPUT */
/*        sNO, sES, sSS, sSR, sFW, sTW, sCL, sCW, sLA, sLI, sSA */
/*syn*/ {{sSR, sES, sES, sSR, sSR, sSR, sSR, sSR, sSR, sSR, sSR }},
/*fin*/ {{sCL, sCW, sSS, sTW, sTW, sTW, sCL, sCW, sLA, sLI, sTW }},
/*ack*/ {{sES, sES, sSS, sES, sFW, sTW, sCL, sCW, sCL, sLI, sES }},
/*rst*/ {{sCL, sCL, sCL, sSR, sCL, sCL, sCL, sCL, sLA, sLI, sSR }},
...
};

static struct tcp_states_t tcp_states_dos [] = {
/*  INPUT */
/*        sNO, sES, sSS, sSR, sFW, sTW, sCL, sCW, sLA, sLI, sSA */
/*syn*/ {{sSR, sES, sES, sSR, sSR, sSR, sSR, sSR, sSR, sSR, sSA }},
/*fin*/ {{sCL, sCW, sSS, sTW, sTW, sTW, sCL, sCW, sLA, sLI, sSA }},
/*ack*/ {{sES, sES, sSS, sSR, sFW, sTW, sCL, sCW, sCL, sLI, sSA }},
/*rst*/ {{sCL, sCL, sCL, sSR, sCL, sCL, sCL, sCL, sLA, sLI, sCL }},

内核版本 4.15

你可能感兴趣的:(负载均衡)