IPVS的NAT转发模式下对TCP序号的修正

由于在NAT转发模式下,ipvs的app模块对报文的修改,有可能改变数据包的长度,对于TCP来说,就需要动态的调整TCP头部的序号和确认序号的数值。

输入处理

ipvs的应用app模块中函数app_tcp_pkt_in用于处理输入方向的数据流。这里使用两个标志IP_VS_CONN_F_IN_SEQ和IP_VS_CONN_F_OUT_SEQ分别表示是否要修正报文的序号和确认序号。

注意这两个标志的判断位于对NAT应用处理(app->pkt_in)调用之前,但是这两个标志的设置是在其之后的vs_seq_update函数中进行的。也就是说,只有在(app->pkt_in)改变了报文的长度之后,才会设置这两个标志位。对于输入方向,将设置IP_VS_CONN_F_IN_SEQ标志位。

static inline int app_tcp_pkt_in(struct ip_vs_conn *cp, struct sk_buff *skb, struct ip_vs_app *app)
{
    const unsigned int tcp_offset = ip_hdrlen(skb);
    struct tcphdr *th;
	
    th = (struct tcphdr *)(skb_network_header(skb) + tcp_offset);

    /* Remember seq number in case this pkt gets resized */
    seq = ntohl(th->seq);

    /* Fix seq stuff if flagged as so. */
    if (cp->flags & IP_VS_CONN_F_IN_SEQ)
        vs_fix_seq(&cp->in_seq, th);
    if (cp->flags & IP_VS_CONN_F_OUT_SEQ)
        vs_fix_ack_seq(&cp->out_seq, th);

    if (!app->pkt_in(app, cp, skb, &diff))
        return 0;

    /* Update ip_vs seq stuff if len has changed. */
    if (diff != 0)
        vs_seq_update(cp, &cp->in_seq, IP_VS_CONN_F_IN_SEQ, seq, diff);

    return 1;
}

函数vs_seq_update负责在报文长度改变之后,更新相应的记录。对于输入方向,如果连接标志flag还没有置位IP_VS_CONN_F_IN_SEQ(即报文长度还没有被NAT应用改变过),或者报文中TCP的序号在记录的init_seq序号之后,这种情况意味着TCP报文经过了重传。以上两种情况都需要更新ipvs的序号记录。

其中delta表示TCP报文的改动长度,而previous_delta表示上一次的改动长度,init_seq保存TCP原始报文头中的序号值。

static inline void vs_seq_update(struct ip_vs_conn *cp, struct ip_vs_seq *vseq, unsigned int flag, __u32 seq, int diff)
{
    spin_lock_bh(&cp->lock);
    if (!(cp->flags & flag) || after(seq, vseq->init_seq)) {
        vseq->previous_delta = vseq->delta;
        vseq->delta += diff;
        vseq->init_seq = seq;
        cp->flags |= flag;
    }
    spin_unlock_bh(&cp->lock);
}

如上所述,如果TCP数据包长度改变了,当随后的TCP数据包到来时,就需要对其进行序号修正。如下函数vs_fix_seq。这里存在两种情况,第一,随后到来的数据包是一个TCP重传报文,即报文的TCP序号在ipvs中记录的序号init_seq之前,此时应当使用之前的previous_delta值修正报文序号;否则,就是第二种情况了,到来的数据包为保序的报文,即报文的TCP序号在init_seq记录的序号之后,此时使用当前的delta值修正报文序号。

static inline void vs_fix_seq(const struct ip_vs_seq *vseq, struct tcphdr *th)
{
    __u32 seq = ntohl(th->seq);

    if (vseq->delta || vseq->previous_delta) {
        if(after(seq, vseq->init_seq))
            th->seq = htonl(seq + vseq->delta);
        else
            th->seq = htonl(seq + vseq->previous_delta);
    }
}

输出处理

ipvs的应用app模块中函数app_tcp_pkt_out用于处理输出方向的数据流。其处理逻辑与以上介绍的pp_tcp_pkt_in函数类似。但是有两点需要注意,一是vs_seq_update函数这里设置的标志换成了IP_VS_CONN_F_OUT_SEQ。

另外重要的一点是,对于输出标志IP_VS_CONN_F_OUT_SEQ,这里需要修正的是out_seq中保存的序号。

static inline int app_tcp_pkt_out(struct ip_vs_conn *cp, struct sk_buff *skb, struct ip_vs_app *app)
{
    const unsigned int tcp_offset = ip_hdrlen(skb);
    struct tcphdr *th;

    th = (struct tcphdr *)(skb_network_header(skb) + tcp_offset);

    /*  Remember seq number in case this pkt gets resized */
    seq = ntohl(th->seq);

    /* Fix seq stuff if flagged as so. */
    if (cp->flags & IP_VS_CONN_F_OUT_SEQ)
        vs_fix_seq(&cp->out_seq, th);
    if (cp->flags & IP_VS_CONN_F_IN_SEQ)
        vs_fix_ack_seq(&cp->in_seq, th);

    if (!app->pkt_out(app, cp, skb, &diff))
        return 0;

    /* Update ip_vs seq stuff if len has changed. */
    if (diff != 0)
        vs_seq_update(cp, &cp->out_seq, IP_VS_CONN_F_OUT_SEQ, seq, diff);

    return 1;
}

确认序号的修正

在以上介绍的两个函数app_tcp_pkt_in和app_tcp_pkt_out中,只是介绍了TCP头部序号的修正,以下我们看一看确认序号的修正。由以下代码的对比可见,在输入函数中,使用记录的输出序号out_seq修正报文的确认序号;而在输出函数中,使用记录的输入序号in_seq修正报文的确认序号。

static inline int app_tcp_pkt_in(struct ip_vs_conn *cp, struct sk_buff *skb, struct ip_vs_app *app)
{
    if (cp->flags & IP_VS_CONN_F_OUT_SEQ)
        vs_fix_ack_seq(&cp->out_seq, th);
}
static inline int app_tcp_pkt_out(struct ip_vs_conn *cp, struct sk_buff *skb, struct ip_vs_app *app)
{
    if (cp->flags & IP_VS_CONN_F_IN_SEQ)
        vs_fix_ack_seq(&cp->in_seq, th);
}

逻辑是这样的。由于在报文输入时(来自客户端)修改了其TCP序号,在最终目的地(服务端)接收到此报文之后,将进行回复,而此回复报文的TCP的确认序号是按照IPVS修改之后的序号生成的(报文长度修改导致确认序号变化),如果直接将此报文回送给客户端,将导致确认序号出错。所以在回送时,由app_tcp_pkt_out函数将回复报文中的TCP确认序号进行修正(消除报文长度变化带来的问题)。

反之,对于客户端回复服务端的报文,在函数app_tcp_pkt_in中进行TCP报文确认序号的修正。

TCP确认序号的修正与其之前的序号修正是正相反的,如下函数vs_fix_ack_seq。

static inline void vs_fix_ack_seq(const struct ip_vs_seq *vseq, struct tcphdr *th)
{
    __u32 ack_seq = ntohl(th->ack_seq);

    /* Adjust ack_seq with delta-offset for
     * the packets AFTER most recent resized pkt has caused a shift for packets before most recent resized pkt, use previous_delta
     */
    if (vseq->delta || vseq->previous_delta) {
        /* since ack_seq is the number of octet that is expected to receive next, so compare it with init_seq+delta */
        if(after(ack_seq, vseq->init_seq+vseq->delta)) {
            th->ack_seq = htonl(ack_seq - vseq->delta);
        } else {
            th->ack_seq = htonl(ack_seq - vseq->previous_delta);
        }
    }
}

内核版本 4.15

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页