欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

IPVS之NAT转发模式

程序员文章站 2024-03-12 23:56:03
...

如下ipvsadm配置命令:

$ ipvsadm -A -t 207.175.44.110:80 -s rr
$ ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.1:80 -m

选项-m(–masquerading)即指定使用NAT/Masq转发模式。由ipvsadm-1.29源码中的选项解析函数parse_options可知,-m对应着NAT/Masq模式,使用标志IP_VS_CONN_F_MASQ。

static int parse_options(int argc, char **argv, struct ipvs_command_entry *ce, unsigned int *options, unsigned int *format)
{
    while ((c=poptGetNextOpt(context)) >= 0){
        switch (c) {
        case 'm':
            set_option(options, OPT_FORWARD);
            ce->dest.conn_flags = IP_VS_CONN_F_MASQ;
            break;

连接绑定转发函数

在连接新建函数ip_vs_conn_new中,对于新创建的连接,使用函数ip_vs_bind_xmit为其绑定发送函数。

struct ip_vs_conn *ip_vs_conn_new(const struct ip_vs_conn_param *p, int dest_af, const union nf_inet_addr *daddr, 
			__be16 dport, unsigned int flags, struct ip_vs_dest *dest, __u32 fwmark)
{
    struct ip_vs_conn *cp;
    cp = kmem_cache_alloc(ip_vs_conn_cachep, GFP_ATOMIC);

#ifdef CONFIG_IP_VS_IPV6
    if (p->af == AF_INET6)
        ip_vs_bind_xmit_v6(cp);
    else
#endif
        ip_vs_bind_xmit(cp);

对于转发模式为NAT/Masq的连接,其传输函数设置为ip_vs_nat_xmit。

/* Bind a connection entry with the corresponding packet_xmit. Called by ip_vs_conn_new. */
static inline void ip_vs_bind_xmit(struct ip_vs_conn *cp)
{ 
    switch (IP_VS_FWD_METHOD(cp)) {
    case IP_VS_CONN_F_MASQ:        
        cp->packet_xmit = ip_vs_nat_xmit;        
        break;

此外对于接收到的同步而来的连接,IPVS使用函数ip_vs_try_bind_dest尝试为其绑定目的服务器时,同时会绑定传输函数。

void ip_vs_try_bind_dest(struct ip_vs_conn *cp)
{
    dest = ip_vs_find_dest(cp->ipvs, cp->af, cp->af, &cp->daddr, cp->dport, &cp->vaddr, cp->vport, cp->protocol, cp->fwmark, cp->flags);
    if (dest) { 
        /* Update its packet transmitter */
        cp->packet_xmit = NULL;
#ifdef CONFIG_IP_VS_IPV6
        if (cp->af == AF_INET6)
            ip_vs_bind_xmit_v6(cp);
        else
#endif
            ip_vs_bind_xmit(cp);

请求报文(NAT发送处理)

在netfilter的hook点NF_INET_LOCAL_IN或者NF_INET_LOCAL_OUT处理客户端请求报文时,函数ip_vs_in在进行完相应的处理之后,使用连接(如果连接不存在,将新建连接)的packet_xmit函数指针执行发送操作。对于NAT/Masq转发模式,其为函数ip_vs_nat_xmit。

static unsigned int ip_vs_in(struct netns_ipvs *ipvs, unsigned int hooknum, struct sk_buff *skb, int af)
{

    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 */

以下看以下NAT/Masq发送函数ip_vs_nat_xmit,仅当连接是由IPVS的FTP应用模块预先创建时,才会有表示客户端端口未设置的标志IP_VS_CONN_F_NO_CPORT。如果是此情况,由函数ip_vs_conn_fill_cport添加客户端端口,并清空此标志。

/* NAT transmitter (only for outside-to-inside nat forwarding) Not used for related ICMP
 */
int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp, struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
    struct rtable *rt;      /* Route to the other host */

    /* check if it is a connection of no-client-port */
    if (unlikely(cp->flags & IP_VS_CONN_F_NO_CPORT)) {

        p = skb_header_pointer(skb, ipvsh->len, sizeof(_pt), &_pt);
        if (p == NULL)
            goto tx_error;
        ip_vs_conn_fill_cport(cp, *p);
        IP_VS_DBG(10, "filled cport=%d\n", ntohs(*p));
    }

通过函数__ip_vs_get_out_rt获取到出口路由,并返回路由目的是否为本机。对于同步而来的连接,如果报文发往本地地址,并且netfilter系统已经记录了报文的tuple信息的情况下,不再进行DNAT处理。

    was_input = rt_is_input_route(skb_rtable(skb));
    local = __ip_vs_get_out_rt(cp->ipvs, cp->af, skb, cp->dest, cp->daddr.ip, IP_VS_RT_MODE_LOCAL |
                   IP_VS_RT_MODE_NON_LOCAL | IP_VS_RT_MODE_RDR, NULL, ipvsh);
    if (local < 0)
        goto tx_error;
    rt = skb_rtable(skb);
    /* Avoid duplicate tuple in reply direction for NAT traffic to local address when connection is sync-ed
     */
#if IS_ENABLED(CONFIG_NF_CONNTRACK)
    if (cp->flags & IP_VS_CONN_F_SYNC && local) {
        enum ip_conntrack_info ctinfo;
        struct nf_conn *ct = nf_ct_get(skb, &ctinfo);

        if (ct) {
            IP_VS_DBG_RL_PKT(10, AF_INET, pp, skb, ipvsh->off, "ip_vs_nat_xmit(): stopping DNAT to local address");
            goto tx_error;
        }
    }
#endif

对于出口和入口路由都是到本地环回地址的报文,不进行处理。

    /* From world but DNAT to loopback address? */
    if (local && ipv4_is_loopback(cp->daddr.ip) && was_input) {
        IP_VS_DBG_RL_PKT(1, AF_INET, pp, skb, ipvsh->off, "ip_vs_nat_xmit(): stopping DNAT to loopback address");
        goto tx_error;
    }

    /* copy-on-write the packet before mangling it */
    if (!skb_make_writable(skb, sizeof(struct iphdr)))
        goto tx_error;

    if (skb_cow(skb, rt->dst.dev->hard_header_len))
        goto tx_error;

此处调用协议结构注册的dnat_handler指针函数,对于TCP协议,其为tcp_dnat_handler;对于UDP协议,其为udp_dnat_handler;SCTP协议的DNAT函数为sctp_dnat_handler。这些函数实现特定于协议的DNAT操作,如更改4层协议的目的端口、重新计算校验和等,稍后再来看具体内容。另外,对于一些应用,例如FTP,也需要进行NAT处理,提前建立数据通道。

之后,更改报文三层头部的目的IP地址,重新计算IP头部的校验和字段。

    /* mangle the packet */
    if (pp->dnat_handler && !pp->dnat_handler(skb, pp, cp, ipvsh))
        goto tx_error;
    ip_hdr(skb)->daddr = cp->daddr.ip;
    ip_send_check(ip_hdr(skb));

以上准备好了要发送的报文,最后由函数ip_vs_nat_send_or_cont执行发送操作。

    /* FIXME: when application helper enlarges the packet and the length
       is larger than the MTU of outgoing device, there will be still MTU problem. */

    /* Another hack: avoid icmp_send in ip_fragment */
    skb->ignore_df = 1;

    rc = ip_vs_nat_send_or_cont(NFPROTO_IPV4, skb, cp, local);
    return rc;

传输处理

函数ip_vs_nat_send_or_cont首先将skb结构的成员ipvs_property设置为1,表明IPVS系统已经处理完此报文,如果在之后的netfilter的hook接口上再次进入IPVS系统,将不再进行处理。如果连接启用了连接跟踪功能,此处将更新连接跟踪中的tuple信息,因为DNAT操作修改了目的地址相关的字段。

/* return NF_STOLEN (sent) or NF_ACCEPT if local=1 (not sent) */
static inline int ip_vs_nat_send_or_cont(int pf, struct sk_buff *skb, struct ip_vs_conn *cp, int local)
{
    int ret = NF_STOLEN;

    skb->ipvs_property = 1;
    if (likely(!(cp->flags & IP_VS_CONN_F_NFCT)))
        ip_vs_notrack(skb);
    else
        ip_vs_update_conntrack(skb, cp, 1);

如果报文目的地址不是到本机,并且虚拟地址和调度的真实服务器地址不相等,丢弃在early_demux阶段获取到的本机sock结构。最后调用NF_INET_LOCAL_OUT点的hook函数,dst_output负责根据skb的出口路由进行报文发送。

    /* Remove the early_demux association unless it's bound for the exact same port and address on this host after translation.
     */
    if (!local || cp->vport != cp->dport || !ip_vs_addr_equal(cp->af, &cp->vaddr, &cp->daddr))
        ip_vs_drop_early_demux_sk(skb);

    if (!local) {
        skb_forward_csum(skb);
        NF_HOOK(pf, NF_INET_LOCAL_OUT, cp->ipvs->net, NULL, skb, NULL, skb_dst(skb)->dev, dst_output);
    } else
        ret = NF_ACCEPT;

    return ret;

在上节的ip_vs_in函数调用的packet_xmit指针函数中,对于转发的报文,在发送之前,将调用NF_INET_LOCAL_OUT点上挂载的函数。最后由函数dst_output发送报文。

报文回复

IPVS在netfilter的NF_INET_FORWARD 和 NF_INET_LOCAL_IN两个hook点处理真实服务器的回复报文。需要注意的是仅在NAT/Masq转发模式下,进行此回复处理。对于回复报文,在函数ip_vs_out中必定可找到对应的连接结构,除非此报文是由真实服务器主动发起的。

static unsigned int ip_vs_out(struct netns_ipvs *ipvs, unsigned int hooknum, struct sk_buff *skb, int af)
{
    /* Check if the packet belongs to an existing entry
     */
    cp = pp->conn_out_get(ipvs, af, skb, &iph);
    if (likely(cp)) {
        if (IP_VS_FWD_METHOD(cp) != IP_VS_CONN_F_MASQ)
            goto ignore_cp;
        return handle_response(af, skb, pd, cp, &iph, hooknum);

函数handle_response处理报文回复。与之前介绍的连接请求相反,此时需对报文进行SNAT处理(对应于之前的DNAT)。对于UDP/TCP/SCTP协议而言,其处理函数分别为udp_snat_handler/tcp_snat_handler/sctp_snat_handler。与之前DNAT的处理相反,此处需要还原报文的4层报头信息,例如源端口号等,并更新校验和。

static unsigned int handle_response(int af, struct sk_buff *skb, struct ip_vs_proto_data *pd, 
        struct ip_vs_conn *cp, struct ip_vs_iphdr *iph, unsigned int hooknum)
{
    struct ip_vs_protocol *pp = pd->pp;

    if (!skb_make_writable(skb, iph->len))
        goto drop;

    /* mangle the packet */
    if (pp->snat_handler && !pp->snat_handler(skb, pp, cp, iph))
        goto drop;

之后的处理就是,还原报文3层信息,如源地址,并重新计算IP层的校验和。

#ifdef CONFIG_IP_VS_IPV6
    if (af == AF_INET6)
        ipv6_hdr(skb)->saddr = cp->vaddr.in6;
    else
#endif
    {   
        ip_hdr(skb)->saddr = cp->vaddr.ip;
        ip_send_check(ip_hdr(skb));
    }

由于策略路由而言,本机发送的数据包和转发的数据包是不同的(indev等字段),IPVS在修改了报文的源地址之后,使用函数ip_vs_route_me_harder进行重新路由,这样对于策略路由,报文和从本机发出的将一致。

    if (ip_vs_route_me_harder(cp->ipvs, af, skb, hooknum))
        goto drop;

    ip_vs_out_stats(cp, skb);
    ip_vs_set_state(cp, IP_VS_DIR_OUTPUT, skb, pd);
    skb->ipvs_property = 1;
    if (!(cp->flags & IP_VS_CONN_F_NFCT))
        ip_vs_notrack(skb);
    else
        ip_vs_update_conntrack(skb, cp, 0);

与IPVS中的DNAT不同,在处理完SNAT之后,不进行直接的发送,而是将报文交付给内核协议栈进行后续处理。

NAT转发模式UDP协议处理

UDP协议的NAT处理函数分别为udp_dnat_handler和udp_snat_handler,两个相对应的函数,前者处理请求报文,后者处理响应报文。在函数udp_dnat_handler中,首先处理UDP协议相关的应用app,但是由于目前IPVS仅有FTP一个应用,其使用TCP协议,所以此处的cp->app为空。

static int udp_dnat_handler(struct sk_buff *skb, struct ip_vs_protocol *pp, struct ip_vs_conn *cp, struct ip_vs_iphdr *iph)
{
    struct udphdr *udph;
    unsigned int udphoff = iph->len;
    int payload_csum = 0;

    if (unlikely(cp->app != NULL)) {
    }

其后,就是将报文的UDP目的端口修改为连接中保存的目的服务器的端口。由于对端口的修改,需要重新计算UDP的校验和。

    udph = (void *)skb_network_header(skb) + udphoff;
    udph->dest = cp->dport;

    /* Adjust UDP checksums
     */
    if (skb->ip_summed == CHECKSUM_PARTIAL) {
        udp_partial_csum_update(cp->af, udph, &cp->vaddr, &cp->daddr, htons(oldlen), htons(skb->len - udphoff));
    } else if (!payload_csum && (udph->check != 0)) {
        /* Only port and addr are changed, do fast csum update */
        udp_fast_csum_update(cp->af, udph, &cp->vaddr, &cp->daddr, cp->vport, cp->dport);
        if (skb->ip_summed == CHECKSUM_COMPLETE)
            skb->ip_summed = (cp->app && pp->csum_check) ? CHECKSUM_UNNECESSARY : CHECKSUM_NONE;
    } else {
        /* full checksum calculation */
        udph->check = 0;
        skb->csum = skb_checksum(skb, udphoff, skb->len - udphoff, 0);
#ifdef CONFIG_IP_VS_IPV6
        if (cp->af == AF_INET6)
            udph->check = csum_ipv6_magic(&cp->caddr.in6, &cp->daddr.in6, skb->len - udphoff, cp->protocol, skb->csum);
        else
#endif
            udph->check = csum_tcpudp_magic(cp->caddr.ip, cp->daddr.ip, skb->len - udphoff, cp->protocol, skb->csum);
        if (udph->check == 0)
            udph->check = CSUM_MANGLED_0;
        skb->ip_summed = CHECKSUM_UNNECESSARY;

对于回复的响应报文,UDP协议的函数udp_snat_handler负责执行SNAT处理。参见以下代码,将报文UDP头部的源端口,修改为连接结构中保存的虚拟服务器的端口,并执行响应的校验和计算(省略其实现代码)。

static int udp_snat_handler(struct sk_buff *skb, struct ip_vs_protocol *pp, struct ip_vs_conn *cp, struct ip_vs_iphdr *iph)
{
    udph = (void *)skb_network_header(skb) + udphoff;
    udph->source = cp->vport;

NAT转发模式TCP协议处理

TCP协议的NAT处理函数分别为tcp_dnat_handler和tcp_snat_handler,两个相对应的函数,前者处理请求报文,后者处理响应报文。在函数tcp_dnat_handler中,对于FTP应用类型的连接结构,其cp->app字段不为空。IPVS将首先计算报文的校验和,再调用app应用相关的NAT处理,包括修正TCP的***。对于FTP而言,在函数ip_vs_app_pkt_in调用其处理函数ip_vs_ftp_in,抓取FTP主动模式下的PASS命令,获得数据通路的端口号,并为其新建一个连接,以保证随后的FTP数据连接调度到同一个目的服务器进行处理。

static int tcp_dnat_handler(struct sk_buff *skb, struct ip_vs_protocol *pp, struct ip_vs_conn *cp, struct ip_vs_iphdr *iph)
{
    struct tcphdr *tcph;
    unsigned int tcphoff = iph->len;
    int payload_csum = 0;

    if (unlikely(cp->app != NULL)) {
        int ret;

        /* Some checks before mangling */
        if (pp->csum_check && !pp->csum_check(cp->af, skb, pp))
            return 0;

        /* Attempt ip_vs_app call.  It will fix ip_vs_conn and iph ack_seq stuff
         */
        if (!(ret = ip_vs_app_pkt_in(cp, skb)))
            return 0;
        /* ret=2: csum update is needed after payload mangling */
        if (ret == 1)
            oldlen = skb->len - tcphoff;
        else
            payload_csum = 1;
    }

注意以上的函数ip_vs_app_pkt_in返回值,其返回2表明对报文的内容进行了修改,就需要对其payload字段进行校验和计算。否则,仅更新报文头部的校验和(省略校验和计算代码)。DNAT操作将报文TCP的目的端口号修改为连接结构中的真实服务器端口号。

    tcph = (void *)skb_network_header(skb) + tcphoff;
    tcph->dest = cp->dport;

    /*  Adjust TCP checksums
     */

对于回复报文,由TCP协议的函数tcp_snat_handler执行SNAT操作。对于FTP应用类型的连接结构,其cp->app字段不为空。IPVS将首先计算报文的校验和,再调用app应用相关的NAT处理,包括修正TCP的确认***。对于FTP而言,在函数ip_vs_app_pkt_out调用其处理函数ip_vs_ftp_out,抓取FTP被动模式下的PASS控制命令,根据目的服务器的地址和端口号,以及客户端的地址等信息新建一个连接。因为此时还不知晓客户端的端口号,为新连接设置IP_VS_CONN_F_NO_CPORT标志,稍后在接收到客户端的数据报文时再填充客户端端口号。FTP新建的数据连接可保证随后的FTP数据报文调度到同一个目的服务器进行处理。

static int tcp_snat_handler(struct sk_buff *skb, struct ip_vs_protocol *pp,  struct ip_vs_conn *cp, struct ip_vs_iphdr *iph) 
{  
    struct tcphdr *tcph;            
    unsigned int tcphoff = iph->len;
    int payload_csum = 0;           

    if (unlikely(cp->app != NULL)) {
        /* Some checks before mangling */
        if (pp->csum_check && !pp->csum_check(cp->af, skb, pp))
            return 0;

        /* Call application helper if needed */
        if (!(ret = ip_vs_app_pkt_out(cp, skb)))
            return 0;
        /* ret=2: csum update is needed after payload mangling */
        if (ret == 1)
            oldlen = skb->len - tcphoff;
        else
            payload_csum = 1;
    }

根据以上函数ip_vs_app_pkt_out的返回值,确定进行校验和计算的数据范围(省略校验和计算代码)。SNAT操作将报文TCP的源端口号修改为连接结构中的虚拟服务的端口号。

    tcph = (void *)skb_network_header(skb) + tcphoff;
    tcph->source = cp->vport;

内核版本 4.15