LVS-NAT模式下处理FTP流量

4次阅读

共计 7574 个字符,预计需要花费 19 分钟才能阅读完成。

概念说明

FTP 数据通道的主动模式和被动模式

主动模式 :服务端通过指定的数据传输端口(默认 20),主动连接客户端提交的端口,向客户端发送数据。

​ 客户端主动发送:”PORT xxx,xxx,xxx,xxx,ppp,ppp”。等待服务器端发起数据连接。

​ 服务器回复:“200”表示同意,到此数据通道可以建立了。

被动模式 :服务端采用客户端建议使用被动模式,开启数据传输端口的监听,被动等待客户端的连接然后向客户端发送数据。

​ 客户端主动发送:”PASV”。通知服务器端使用被动模式。

​ 服务器回复:“227 xxx,xxx,xxx,xxx,ppp,ppp”。表示同意,同时将本端监听的端口和 IP 发送给客户端。

一言以蔽之,服务端主动连接客户端就是主动模式,服务端被动等待客户端连接(客户端主动连接服务端)就是被动模式。

ftp 有主动模式被动模式而 ssh 等其他协议没有这种说法的根源是:ftp 要使用别的端口来传输数据。

LVS 中对 FTP 的数据通道的处理

out2in

从上面的概念可以知道,主动模式客户端会通过”PORT“命令将本地监听的端口和 IP 发送给服务器。所以在 out2in 方向可以获取数据通道的信息。目前只有 nat 模式需要支持 ftp 的 alg 处理。在 dnat 函数中会解析 ftp 的内容,找到 PORT 命令信息,添加数据通道的连接跟踪。

假设我们从内容中获取的 ip 为 dataip,端口为 dataport。控制通道的连接跟踪为 cn,则添加的连接跟踪 7 元祖为:

caddr:dataip

cport:dataport

vaddr:cn->vaddr

vport:cn->vport-1(即 20)

daddr:cn->daddr

dport:cn->dport-1(即 20)

创建该连接跟踪后,会将其状态设置为 IP_VS_TCP_S_LISTEN,同时设置其超时定时器的时间为对应的时间。主动模式下不需要进行 seq 的修正,因为报文不会进行 ip 地址的变化。

in2out

从上面的概念可以知道,被动模式服务器端会将本地监听的端口和 IP 发送给客户端。所以在 in2out 方向可以获取数据通道的信息。目前只有 nat 模式需要支持 ftp 的 alg 处理。因为是在 in2out 方向,所以在 snat 的反向动作 dnat 中会进行数据通道的连接跟踪处理。

假设我们从内容中获取的 ip 为 dataip,端口为 dataport。控制通道的连接跟踪为 cn,则添加的连接跟踪 7 元祖为:

caddr:cn->addr

cport:0

vaddr:cn->vaddr

vport:dataport

daddr:dataip

dport:dataport

从上面的连接跟踪信息可以知道,LVS 希望客户端发送的数据通道的目的 IP 也是 VIP,这与实服务器指定的 dataip 是不一样的。所以需要修改 ftp 报文中的端口 IP 信息,让客户端发起数据通道连接时能命中上面的连接跟踪内容。同时还不知道客户端会以哪个端口来连接服务器的数据通道,所以连接跟踪中的 cport 设置为 0,并且设置了标志 IP_VS_CONN_F_NO_CPORT。表示这个连接跟踪需要在确定的时候设置该 cport(这个时候就是客户端发送连接数据通道的 syn 包的时候,命中了该连接跟踪)。

由于需要修改应用层信息,那么涉及到 tcp 的序列号的变化,LVS 想借用 netfilter 的 adjseq 机制进行处理,设置了标志 IP_VS_CONN_F_NFCT,表示不要删除 conntrack。

关键函数分析

out2in

/*
 * Look at incoming ftp packets to catch the PASV/PORT command
 * (outside-to-inside).
 *
 * The incoming packet having the PORT command should be something like
 *      "PORT xxx,xxx,xxx,xxx,ppp,ppp\n".
 * xxx,xxx,xxx,xxx is the client address, ppp,ppp is the client port number.
 * In this case, we create a connection entry using the client address and
 * port, so that the active ftp data connection from the server can reach
 * the client.
 */
static int ip_vs_ftp_in(struct ip_vs_app *app, struct ip_vs_conn *cp,
            struct sk_buff *skb, int *diff)
{
    struct iphdr *iph;
    struct tcphdr *th;
    char *data, *data_start, *data_limit;
    char *start, *end;
    union nf_inet_addr to;
    __be16 port;
    struct ip_vs_conn *n_cp;

    /* no diff required for incoming packets */
    *diff = 0;

#ifdef CONFIG_IP_VS_IPV6
    /* This application helper doesn't work with IPv6 yet,
     * so turn this into a no-op for IPv6 packets
     */
    if (cp->af == AF_INET6)
        return 1;
#endif

    /* Only useful for established sessions */
    if (cp->state != IP_VS_TCP_S_ESTABLISHED)
        return 1;

    /* Linear packets are much easier to deal with. */
    if (!skb_make_writable(skb, skb->len))
        return 0;

    /*
     * Detecting whether it is passive
     */
    iph = ip_hdr(skb);
    th = (struct tcphdr *)&(((char *)iph)[iph->ihl*4]);

    /* Since there may be OPTIONS in the TCP packet and the HLEN is
       the length of the header in 32-bit multiples, it is accurate
       to calculate data address by th+HLEN*4 */
    data = data_start = (char *)th + (th->doff << 2);
    data_limit = skb_tail_pointer(skb);
    // 家产是否为从模式,6 表示 "PASV\r\n" 的长度,这里采用了暴力匹配
    while (data <= data_limit - 6) {if (strncasecmp(data, "PASV\r\n", 6) == 0) {
            /* Passive mode on */
            IP_VS_DBG(7, "got PASV at %td of %td\n",
                  data - data_start,
                  data_limit - data_start);
            cp->app_data = &ip_vs_ftp_pasv;
            return 1;
        }
        data++;
    }

    /*
     * To support virtual FTP server, the scenerio is as follows:
     *       FTP client ----> Load Balancer ----> FTP server
     * First detect the port number in the application data,
     * then create a new connection entry for the coming data
     * connection.
     * 这种情况为主动模式。*/
    if (ip_vs_ftp_get_addrport(data_start, data_limit,
                   CLIENT_STRING, sizeof(CLIENT_STRING)-1,
                   '','\r', &to.ip, &port,
                   &start, &end) != 1)
        return 1;

    IP_VS_DBG(7, "PORT %pI4:%d detected\n", &to.ip, ntohs(port));

    /* Passive mode off */
    cp->app_data = NULL;

    /*
     * Now update or create a connection entry for it
     */
    IP_VS_DBG(7, "protocol %s %pI4:%d %pI4:%d\n",
          ip_vs_proto_name(iph->protocol),
          &to.ip, ntohs(port), &cp->vaddr.ip, 0);

    {
        struct ip_vs_conn_param p;
        // 为主动模式创建请求方向的连接跟踪,只记录了
        // 主动模式端口号为 20。ip_vs_conn_fill_param(cp->ipvs, AF_INET,
                      iph->protocol, &to, port, &cp->vaddr,
                      htons(ntohs(cp->vport)-1), &p);//vport==(vportcp->vport)-1
        n_cp = ip_vs_conn_in_get(&p);
        if (!n_cp) {
            /* This is ipv4 only 使用同一个服务器。*/
            n_cp = ip_vs_conn_new(&p, AF_INET, &cp->daddr,
                          htons(ntohs(cp->dport)-1),//dport==(vportcp->dport)-1
                          IP_VS_CONN_F_NFCT, cp->dest,
                          skb->mark);
            if (!n_cp)
                return 0;

            /* add its controller */
            ip_vs_control_add(n_cp, cp);
        }
    }

    /*
     *    Move tunnel to listen state
     *  设置连接跟踪的状态为 listen。*/
    ip_vs_tcp_conn_listen(n_cp);
    ip_vs_conn_put(n_cp);

    return 1;
}

in2out

/*
 * Look at outgoing ftp packets to catch the response to a PASV command
 * from the server (inside-to-outside).
 * When we see one, we build a connection entry with the client address,
 * client port 0 (unknown at the moment), the server address and the
 * server port.  Mark the current connection entry as a control channel
 * of the new entry. All this work is just to make the data connection
 * can be scheduled to the right server later.
 *
 * The outgoing packet should be something like
 *   "227 Entering Passive Mode (xxx,xxx,xxx,xxx,ppp,ppp)".
 * xxx,xxx,xxx,xxx is the server address, ppp,ppp is the server port number.
 */
static int ip_vs_ftp_out(struct ip_vs_app *app, struct ip_vs_conn *cp,
             struct sk_buff *skb, int *diff)
{
    struct iphdr *iph;
    struct tcphdr *th;
    char *data, *data_limit;
    char *start, *end;
    union nf_inet_addr from;
    __be16 port;
    struct ip_vs_conn *n_cp;
    char buf[24];        /* xxx.xxx.xxx.xxx,ppp,ppp\000 */
    unsigned int buf_len;
    int ret = 0;
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct;

    *diff = 0;

#ifdef CONFIG_IP_VS_IPV6
    /* This application helper doesn't work with IPv6 yet,
     * so turn this into a no-op for IPv6 packets
     */
    if (cp->af == AF_INET6)
        return 1;
#endif

    /* Only useful for established sessions */
    if (cp->state != IP_VS_TCP_S_ESTABLISHED)
        return 1;

    /* Linear packets are much easier to deal with. */
    if (!skb_make_writable(skb, skb->len))
        return 0;
    // 被动模式,说明是客户端发起连接,服务器会发送端口和地址
    if (cp->app_data == &ip_vs_ftp_pasv) {// 被动模式,端口来自服务器端,需要在 out 方向获取端口。iph = ip_hdr(skb);
        th = (struct tcphdr *)&(((char *)iph)[iph->ihl*4]);
        data = (char *)th + (th->doff << 2);
        data_limit = skb_tail_pointer(skb);

        if (ip_vs_ftp_get_addrport(data, data_limit,
                       SERVER_STRING,
                       sizeof(SERVER_STRING)-1,
                       '(', ')',
                       &from.ip, &port,
                       &start, &end) != 1)
            return 1;

        IP_VS_DBG(7, "PASV response (%pI4:%d) -> %pI4:%d detected\n",
              &from.ip, ntohs(port), &cp->caddr.ip, 0);

        /*
         * Now update or create an connection entry for it
         * 获取的服务器端打开的地址和端口
         */
        {
            struct ip_vs_conn_param p;
            ip_vs_conn_fill_param(cp->ipvs, AF_INET,
                          iph->protocol, &from, port,
                          &cp->caddr, 0, &p);// 这里填写了客户端的端口为 0
            // 查看是否存在输出的
            n_cp = ip_vs_conn_out_get(&p);
        }
        if (!n_cp) {
            struct ip_vs_conn_param p;
            ip_vs_conn_fill_param(cp->ipvs,
                          AF_INET, IPPROTO_TCP, &cp->caddr,
                          0, &cp->vaddr, port, &p);
            /* As above, this is ipv4 only */
            /* 设置客户端端口可以为 0,因为没有端口 */
            n_cp = ip_vs_conn_new(&p, AF_INET, &from, port,
                          IP_VS_CONN_F_NO_CPORT |
                          IP_VS_CONN_F_NFCT,
                          cp->dest, skb->mark);
            if (!n_cp)
                return 0;

            /* add its controller */
            ip_vs_control_add(n_cp, cp);
        }

        /*
         * Replace the old passive address with the new one
         * 修改报文内容,使用新的 ip 通知客户端
         */
        from.ip = n_cp->vaddr.ip;
        port = n_cp->vport;
        snprintf(buf, sizeof(buf), "%u,%u,%u,%u,%u,%u",
             ((unsigned char *)&from.ip)[0],
             ((unsigned char *)&from.ip)[1],
             ((unsigned char *)&from.ip)[2],
             ((unsigned char *)&from.ip)[3],
             ntohs(port) >> 8,
             ntohs(port) & 0xFF);

        buf_len = strlen(buf);
        // 使用 nf_ct 机制进行变换
        ct = nf_ct_get(skb, &ctinfo);
        if (ct) {
            bool mangled;

            /* If mangling fails this function will return 0
             * which will cause the packet to be dropped.
             * Mangling can only fail under memory pressure,
             * hopefully it will succeed on the retransmitted
             * packet.
             * 会涉及 seqadjst。*/
            mangled = nf_nat_mangle_tcp_packet(skb, ct, ctinfo,
                               iph->ihl * 4,
                               start - data,
                               end - start,
                               buf, buf_len);
            if (mangled) {
                ip_vs_nfct_expect_related(skb, ct, n_cp,
                              IPPROTO_TCP, 0, 0);
                if (skb->ip_summed == CHECKSUM_COMPLETE)
                    skb->ip_summed = CHECKSUM_UNNECESSARY;
                /* csum is updated */
                ret = 1;
            }
        }

        /*
         * Not setting 'diff' is intentional, otherwise the sequence
         * would be adjusted twice.
         */

        cp->app_data = NULL;
        // 设置连接跟踪的状态为 listen 状态。ip_vs_tcp_conn_listen(n_cp);
        ip_vs_conn_put(n_cp);
        return ret;
    }
    return 1;
}

IP_VS_CONN_F_NO_CPORT

/*
 *    Fill a no_client_port connection with a client port number
 */
void ip_vs_conn_fill_cport(struct ip_vs_conn *cp, __be16 cport)
{if (ip_vs_conn_unhash(cp)) {spin_lock_bh(&cp->lock);
        if (cp->flags & IP_VS_CONN_F_NO_CPORT) {atomic_dec(&ip_vs_conn_no_cport_cnt);
            cp->flags &= ~IP_VS_CONN_F_NO_CPORT;
            cp->cport = cport;
        }
        spin_unlock_bh(&cp->lock);

        /* hash on new dport */
        ip_vs_conn_hash(cp);
    }
}

正文完
 0