IPVS代码阅读笔记(二):NAT模式下的TCP数据包处理 - 我的文章 - langqi...

来源:百度文库 编辑:神马文学网 时间:2024/05/03 11:25:04


NAT模式是IPVS最常用的一种模式。相比于TUN和DR模式,NAT模式更容易部署,仅仅是需要更改真实服务器的默认网关配置。

IPVS是基于Netfilter实现的。它注册了4个Netfilter钩子函数,其中与NAT模式相关的是ip_vs_in和ip_vs_out两个钩子函数。前者处理了客户端-〉服务器的数据包,后者则针对服务器-〉客户端的数据包。

1、ip_vs_in钩子函数

ip_vs_in函数的处理流程非常清晰:

预校验--〉ip_vs_conn对象的查找或生成--〉更新统计信息--〉更新ip_vs_conn对象的状态--〉修改sk_buff并转发数据包--〉IPVS状态同步--〉结束

1.1、预校验

预校验的过程很简单,主要包括:

  • 确保数据包的类型为PACKET_HOST
  • 确保数据包不是loopback设备发出来的
  • 确保数据包的协议类型是IPVS所支持的,目前IPVS支持TCP、UDP、AH和ESP协议
1.2、ip_vs_conn对象的查找或生成

既然有数据包来了,必然有对应的ip_vs_conn对象。首先根据数据包的<源地址-源端口-目的地址-目的端口>,查找当前的ip_vs_conn对象列表。如果没有找到的话,说明这是一个新连接,于是运行调度过程,找到一个合适的真实服务器,然后再生成一个新的ip_vs_conn对象。这里先不对调度过程进行展开描述。

1.3、更新统计信息

这里先看一下ip_vs_stats结构,其各个成员的作用很容易看出来:

/*
 *    IPVS statistics object
 */
struct ip_vs_stats
{
    __u32 conns; /* connections scheduled */
    __u32 inpkts; /* incoming packets */
    __u32 outpkts; /* outgoing packets */
    __u64 inbytes; /* incoming bytes */
    __u64 outbytes; /* outgoing bytes */

    __u32            cps;        /* current connection rate */
    __u32            inpps;        /* current in packet rate */
    __u32            outpps;        /* current out packet rate */
    __u32            inbps;        /* current in byte rate */
    __u32            outbps;        /* current out byte rate */

    spinlock_t lock; /* spin lock */
};

这是一个专门用于统计的数据结构,每个ip_vs_service对象、每个ip_vs_dest对象都包含有这么一个结构,另外还有一个ip_vs_stats全局变量。

函数ip_vs_in_stats和ip_vs_out_stats分别统计两个方向的数据包流量,函数ip_vs_conn_stats用于统计新建连接数。

static inline void
ip_vs_in_stats(struct ip_vs_conn *cp, struct sk_buff *skb)
{
    struct ip_vs_dest *dest = cp->dest;
    if (dest && (dest->flags & IP_VS_DEST_F_AVAILABLE)) {
        spin_lock(&dest->stats.lock);
        dest->stats.inpkts++;
        dest->stats.inbytes += skb->len;
        spin_unlock(&dest->stats.lock);

        spin_lock(&dest->svc->stats.lock);
        dest->svc->stats.inpkts++;
        dest->svc->stats.inbytes += skb->len;
        spin_unlock(&dest->svc->stats.lock);

        spin_lock(&ip_vs_stats.lock);
        ip_vs_stats.inpkts++;
        ip_vs_stats.inbytes += skb->len;
        spin_unlock(&ip_vs_stats.lock);
    }
}

conns、inpkts、outpkts、inbytes和outbytes统计比较容易,只需简单的加1。但cps等统计起来就要复杂一些了,它是通过内核定时器来实现的。每个ip_vs_stats对象都对应有一个ip_vs_estimator结构:

struct ip_vs_estimator
{
    struct ip_vs_estimator    *next;
    struct ip_vs_stats    *stats;

    u32            last_conns;
    u32            last_inpkts;
    u32            last_outpkts;
    u64            last_inbytes;
    u64            last_outbytes;

    u32            cps;
    u32            inpps;
    u32            outpps;
    u32            inbps;
    u32            outbps;
};

所有的ip_vs_estimator结构形成一张链表,通过全局变量est_list可以遍历这个链表。定时器的时间间隔为2秒,对应的触发函数为:

static void estimation_timer(unsigned long arg)
{
    struct ip_vs_estimator *e;
    struct ip_vs_stats *s;
    u32 n_conns;
    u32 n_inpkts, n_outpkts;
    u64 n_inbytes, n_outbytes;
    u32 rate;

    read_lock(&est_lock);
    for (e = est_list; e; e = e->next) {
        s = e->stats;

        spin_lock(&s->lock);
        n_conns = s->conns;
        n_inpkts = s->inpkts;
        n_outpkts = s->outpkts;
        n_inbytes = s->inbytes;
        n_outbytes = s->outbytes;

        /* scaled by 2^10, but divided 2 seconds */
        rate = (n_conns - e->last_conns)<<9;
        e->last_conns = n_conns;
        e->cps += ((long)rate - (long)e->cps)>>2;
        s->cps = (e->cps+0x1FF)>>10;

        rate = (n_inpkts - e->last_inpkts)<<9;
        e->last_inpkts = n_inpkts;
        e->inpps += ((long)rate - (long)e->inpps)>>2;
        s->inpps = (e->inpps+0x1FF)>>10;

        rate = (n_outpkts - e->last_outpkts)<<9;
        e->last_outpkts = n_outpkts;
        e->outpps += ((long)rate - (long)e->outpps)>>2;
        s->outpps = (e->outpps+0x1FF)>>10;

        rate = (n_inbytes - e->last_inbytes)<<4;
        e->last_inbytes = n_inbytes;
        e->inbps += ((long)rate - (long)e->inbps)>>2;
        s->inbps = (e->inbps+0xF)>>5;

        rate = (n_outbytes - e->last_outbytes)<<4;
        e->last_outbytes = n_outbytes;
        e->outbps += ((long)rate - (long)e->outbps)>>2;
        s->outbps = (e->outbps+0xF)>>5;
        spin_unlock(&s->lock);
    }
    read_unlock(&est_lock);
    mod_timer(&est_timer, jiffies + 2*HZ);
}

以cps的统计为例,其计算过程很简单,cps = (rate+cps)/2,其中单位为2^10。

1.4、更新ip_vs_conn对象的状态

在TCP数据包处理过程中,每个ip_vs_conn对象对应于一个TCP连接,因此也必须有一个状态转换过程,才能够引导此TCP连接正常建立和终止。这个状态转换颇为复杂,在后续内容将IN/OUT结合一起,来看TCP连接的状态转换。

1.5、修改sk_buff并转发数据包

NAT模式下的数据包转发由ip_vs_nat_xmit函数完成。对sk_buff数据结构的操作不熟悉,略过。

1.6、IPVS状态同步

先判断此ip_vs_conn对象是否需要进行主备机同步。首先当前IPVS必须是MASTER,并且此ip_vs_conn对象的状态为ESTABLISHED。另外,满足这些条件时,并非每个Packet转发的时候都进行同步,而是每50个Packet,才同步一次。

同步过程由函数ip_vs_sync_conn完成:

/*
 * Add an ip_vs_conn information into the current sync_buff.
 * Called by ip_vs_in.
 */
void ip_vs_sync_conn(struct ip_vs_conn *cp)
{
    struct ip_vs_sync_mesg *m;
    struct ip_vs_sync_conn *s;
    int len;

    spin_lock(&curr_sb_lock);
    if (!curr_sb) {
        if (!(curr_sb=ip_vs_sync_buff_create())) {
            spin_unlock(&curr_sb_lock);
            IP_VS_ERR("ip_vs_sync_buff_create failed.\n");
            return;
        }
    }

    len = (cp->flags & IP_VS_CONN_F_SEQ_MASK) ? FULL_CONN_SIZE :
        SIMPLE_CONN_SIZE;
    m = curr_sb->mesg;
    s = (struct ip_vs_sync_conn *)curr_sb->head;

    /* copy members */
    s->protocol = cp->protocol;
    s->cport = cp->cport;
    s->vport = cp->vport;
    s->dport = cp->dport;
    s->caddr = cp->caddr;
    s->vaddr = cp->vaddr;
    s->daddr = cp->daddr;
    s->flags = htons(cp->flags & ~IP_VS_CONN_F_HASHED);
    s->state = htons(cp->state);
    if (cp->flags & IP_VS_CONN_F_SEQ_MASK) {
        struct ip_vs_sync_conn_options *opt =
            (struct ip_vs_sync_conn_options *)&s[1];
        memcpy(opt, &cp->in_seq, sizeof(*opt));
    }

    m->nr_conns++;
    m->size += len;
    curr_sb->head += len;

    /* check if there is a space for next one */
    if (curr_sb->head+FULL_CONN_SIZE > curr_sb->end) {
        sb_queue_tail(curr_sb);
        curr_sb = NULL;
    }
    spin_unlock(&curr_sb_lock);

    /* synchronize its controller if it has */
    if (cp->control)
        ip_vs_sync_conn(cp->control);
}

  每个ip_vs_conn对象对应的同步数据拷贝到curr_sb中,然后将它放到ip_vs_sync_queue链表中。同步线程主函数sync_master_loop则从ip_vs_sync_queue链表取同步数据对象,然后发送到备机中去。备机中的同步线程主函数sync_backup_loop则从网络中读取同步数据对象,然后由函数ip_vs_process_message将其恢复成ip_vs_conn对象,并保存起来。  
1.7、ip_vs_out钩子函数

ip_vs_out函数处理 服务器-〉客户端 的数据包。相比于ip_vs_in函数,它要简单得多。这里不再描述。

2、TCP状态转换过程

IPVS支持TCP、UDP、AH和ESP四种协议。由于TCP协议的逻辑相对复杂一些,所以IPVS对TCP协议的特殊处理也更多。

IPVS针对TCP协议的处理主要是体现在TCP状态维护上,而TCP状态维护依赖于一个状态转换矩阵:

/*
 *    Timeout table[state]
 */
static int tcp_timeouts[IP_VS_TCP_S_LAST+1] = {
    [IP_VS_TCP_S_NONE]        =    2*HZ,
    [IP_VS_TCP_S_ESTABLISHED]    =    15*60*HZ,
    [IP_VS_TCP_S_SYN_SENT]        =    2*60*HZ,
    [IP_VS_TCP_S_SYN_RECV]        =    1*60*HZ,
    [IP_VS_TCP_S_FIN_WAIT]        =    2*60*HZ,
    [IP_VS_TCP_S_TIME_WAIT]        =    2*60*HZ,
    [IP_VS_TCP_S_CLOSE]        =    10*HZ,
    [IP_VS_TCP_S_CLOSE_WAIT]    =    60*HZ,
    [IP_VS_TCP_S_LAST_ACK]        =    30*HZ,
    [IP_VS_TCP_S_LISTEN]        =    2*60*HZ,
    [IP_VS_TCP_S_SYNACK]        =    120*HZ,
    [IP_VS_TCP_S_LAST]        =    2*HZ,
};

static char * tcp_state_name_table[IP_VS_TCP_S_LAST+1] = {
    [IP_VS_TCP_S_NONE]        =    "NONE",
    [IP_VS_TCP_S_ESTABLISHED]    =    "ESTABLISHED",
    [IP_VS_TCP_S_SYN_SENT]        =    "SYN_SENT",
    [IP_VS_TCP_S_SYN_RECV]        =    "SYN_RECV",
    [IP_VS_TCP_S_FIN_WAIT]        =    "FIN_WAIT",
    [IP_VS_TCP_S_TIME_WAIT]        =    "TIME_WAIT",
    [IP_VS_TCP_S_CLOSE]        =    "CLOSE",
    [IP_VS_TCP_S_CLOSE_WAIT]    =    "CLOSE_WAIT",
    [IP_VS_TCP_S_LAST_ACK]        =    "LAST_ACK",
    [IP_VS_TCP_S_LISTEN]        =    "LISTEN",
    [IP_VS_TCP_S_SYNACK]        =    "SYNACK",
    [IP_VS_TCP_S_LAST]        =    "BUG!",
};

#define sNO IP_VS_TCP_S_NONE
#define sES IP_VS_TCP_S_ESTABLISHED
#define sSS IP_VS_TCP_S_SYN_SENT
#define sSR IP_VS_TCP_S_SYN_RECV
#define sFW IP_VS_TCP_S_FIN_WAIT
#define sTW IP_VS_TCP_S_TIME_WAIT
#define sCL IP_VS_TCP_S_CLOSE
#define sCW IP_VS_TCP_S_CLOSE_WAIT
#define sLA IP_VS_TCP_S_LAST_ACK
#define sLI IP_VS_TCP_S_LISTEN
#define sSA IP_VS_TCP_S_SYNACK

struct tcp_states_t {
    int next_state[IP_VS_TCP_S_LAST];
};

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*/ {{sCL, sES, sSS, sES, sFW, sTW, sCL, sCW, sCL, sLI, sES }},
/*rst*/ {{sCL, sCL, sCL, sSR, sCL, sCL, sCL, sCL, sLA, sLI, sSR }},

/*    OUTPUT */
/* sNO, sES, sSS, sSR, sFW, sTW, sCL, sCW, sLA, sLI, sSA    */
/*syn*/ {{sSS, sES, sSS, sSR, sSS, sSS, sSS, sSS, sSS, sLI, sSR }},
/*fin*/ {{sTW, sFW, sSS, sTW, sFW, sTW, sCL, sTW, sLA, sLI, sTW }},
/*ack*/ {{sES, sES, sSS, sES, sFW, sTW, sCL, sCW, sLA, sES, sES }},
/*rst*/ {{sCL, sCL, sSS, sCL, sCL, sTW, sCL, sCL, sCL, sCL, sCL }},

/*    INPUT-ONLY */
/* 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, sFW, sSS, sTW, sFW, sTW, sCL, sCW, sLA, sLI, sTW }},
/*ack*/ {{sCL, sES, sSS, sES, sFW, sTW, sCL, sCW, sCL, sLI, sES }},
/*rst*/ {{sCL, sCL, sCL, sSR, sCL, sCL, sCL, sCL, sLA, sLI, sCL }},
};

与NAT模式相关的为INPUT和OUTPUT两张表,其意思也较容易理解:

  1. sNO、sES等为TCP状态,tcp_state_name_table为状态名称表,而tcp_timeouts表指明了每个状态的维系时间。此维系时间决定了ip_vs_conn对象的生命期,当维系时间内此连接无任何输入输出,则ip_vs_conn对象自动销毁,它是通过设置ip_vs_conn对象的timeout来实现的。
  2. 当连接处于某状态时,在tcp_states矩阵中查找对应状态列,然后根据当前的输入(INPUT指客户端输入,OUTPUT指真实服务器输出),查找到下一个状态值。

状态转换矩阵也可以用一张状态转换图来表示:

这是一个完整的图,其中有很多状态转换都不太可能出现的。