IPVS之NAT转发模式

负载均衡 专栏收录该内容
42 篇文章 4 订阅

如下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

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值