IPVS系统的路由和发送

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

IPVS不管是配置的NAT/Masq、Direct Route或者隧道Tunnel转发模式,在执行发送时,都需要进行出口路由的查找。

路由查找

在出口路由查找函数__ip_vs_get_out_rt中,通常根据目的服务器的地址信息进行查找。如下,首先检查此目的服务器结构中是否缓存了路由信息,以及是否还是有效的,如果成立,直接使用缓存的路由表项。否则,就需要进行路由的查找。路由查找函数do_output_route4主要是根据目的地址dest->addr.ip查找路由,并且填充路由表项中的源地址dest_dst->dst_saddr.ip。路由查找成功之后,将表项缓存到目的服务器结构中,后续的相同报文就可省去路由查找,直接使用路由缓存表项了。

static int __ip_vs_get_out_rt(struct netns_ipvs *ipvs, int skb_af, struct sk_buff *skb, struct ip_vs_dest *dest,
           __be32 daddr, int rt_mode, __be32 *ret_saddr, struct ip_vs_iphdr *ipvsh)
{
    struct net *net = ipvs->net;
    struct ip_vs_dest_dst *dest_dst;
    struct rtable *rt;          /* Route to the other host */
    int local, noref = 1;

    if (dest) {
        dest_dst = __ip_vs_dst_check(dest);
        if (likely(dest_dst))
            rt = (struct rtable *) dest_dst->dst_cache;
        else {
            dest_dst = ip_vs_dest_dst_alloc();
            spin_lock_bh(&dest->dst_lock);
            if (!dest_dst)
                goto err_unreach;
            
            rt = do_output_route4(net, dest->addr.ip, rt_mode, &dest_dst->dst_saddr.ip);
            if (!rt) 
                goto err_unreach;
            
            __ip_vs_dst_set(dest, dest_dst, &rt->dst, 0);
        }

如果目的服务器结构为空,这种情况下,直接使用参数中的目的地址daddr进行路由查找,即没有可用的目的服务器的情况,使用报文IP头部中的目的IP进行路由查找。清空IP_VS_RT_MODE_CONNECT标志,将导致路由查找函数do_output_route4仅执行一次查找。对于这种没有目的服务器的连接,为性能考虑尽量减少查找次数。稍后将会看到隧道Tunnel转发模式,将使用此标志绑定源IP地址。

    } else {
        __be32 saddr = htonl(INADDR_ANY);

        noref = 0;

        /* For such unconfigured boxes avoid many route lookups for performance reasons because we do not remember saddr
         */
        rt_mode &= ~IP_VS_RT_MODE_CONNECT;
        rt = do_output_route4(net, daddr, rt_mode, &saddr);
        if (!rt)
            goto err_unreach;
        if (ret_saddr)
            *ret_saddr = saddr;
    }

函数crosses_local_route_boundary根据路由信息判断当前的路由是否合法,主要是否超出了本地路由的边界。稍后看此函数具体实现。如果检查通过,并且之前查找到的路由为本地路由,返回local值,函数结束,保持skb中的入口缓存路由。

随后的IPVS发送流程中,对于本地路由的报文(local为真),将不执行发送操作,交由内核协议栈处理。

    local = (rt->rt_flags & RTCF_LOCAL) ? 1 : 0;
    if (unlikely(crosses_local_route_boundary(skb_af, skb, rt_mode, local))) {
        IP_VS_DBG_RL("We are crossing local and non-local addresses daddr=%pI4\n", &daddr);
        goto err_put;
    }
    if (unlikely(local)) {
        /* skb to local stack, preserve old route */
        if (!noref) ip_rt_put(rt);
        return local;
    }

之后,对于非本地路由报文,递减报文的TTL计数。对于Tunnel转发模式(IPIP隧道),需要留出一个IP头部结构iphdr的空间。

    if (!decrement_ttl(ipvs, skb_af, skb))
        goto err_put;

    if (likely(!(rt_mode & IP_VS_RT_MODE_TUNNEL))) {
        mtu = dst_mtu(&rt->dst);
    } else {
        mtu = dst_mtu(&rt->dst) - sizeof(struct iphdr);
        if (mtu < 68) {
            IP_VS_DBG_RL("%s(): mtu less than 68\n", __func__);
            goto err_put;
        }
        maybe_update_pmtu(skb_af, skb, mtu);
    }

    if (!ensure_mtu_is_adequate(ipvs, skb_af, rt_mode, ipvsh, skb, mtu))
        goto err_put;

函数最后,丢弃SKB中的入口路由缓存,更新为出口缓存。感觉这段代码由问题,因为对于目的地址为本地的报文(local为真),已经在之前返回,不可能走到这里,所以这里的local总是为零。变量noref表示是否在IPVS的目的服务器结构中缓存了出口路由,其为真表明已缓存,继而在skb中使用skb_dst_set_noref函数进行缓存。反之,如果没有在目的服务器结构中缓存,使用函数skb_dst_set进行缓存,用完之后即释放。

    skb_dst_drop(skb);
    if (noref) {
        if (!local)
            skb_dst_set_noref(skb, &rt->dst);
        else
            skb_dst_set(skb, dst_clone(&rt->dst));
    } else
        skb_dst_set(skb, &rt->dst);

    return local;

出口路由查找

其调用内核路由系统的ip_route_output_key函数完成。需要注意的是对于Tunnel隧道转发模式,其在进行路由查找时,将为rt_mode变量设置IP_VS_RT_MODE_CONNECT标志。对于Tunnel我们不仅需要目的地址,还需要源地址,才能进行正确的封装,所以将进行带有源saddr的路由查找。

static struct rtable *do_output_route4(struct net *net, __be32 daddr, int rt_mode, __be32 *saddr)
{   
    struct flowi4 fl4;
    struct rtable *rt;
    int loop = 0;

    memset(&fl4, 0, sizeof(fl4));
    fl4.daddr = daddr;
    fl4.flowi4_flags = (rt_mode & IP_VS_RT_MODE_KNOWN_NH) ? FLOWI_FLAG_KNOWN_NH : 0;

retry: 
    rt = ip_route_output_key(net, &fl4);
    if (IS_ERR(rt)) {
        /* Invalid saddr ? */
        if (PTR_ERR(rt) == -EINVAL && *saddr && rt_mode & IP_VS_RT_MODE_CONNECT && !loop) {
            *saddr = 0;
            flowi4_update_output(&fl4, 0, 0, daddr, 0);
            goto retry;
        }
        IP_VS_DBG_RL("ip_route_output error, dest: %pI4\n", &daddr);
        return NULL;
    } else if (!*saddr && rt_mode & IP_VS_RT_MODE_CONNECT && fl4.saddr) {
        ip_rt_put(rt);
        *saddr = fl4.saddr;
        flowi4_update_output(&fl4, 0, 0, daddr, fl4.saddr);
        loop++;
        goto retry;
    }
    *saddr = fl4.saddr;
    return rt;

本地目的地址路由检查

在查看路由检查函数之前,首先看一下IPVS各个转发模式下,进行路由查询时的传递参数。对于NAT转发模式,可支持三种标志:IP_VS_RT_MODE_LOCAL、IP_VS_RT_MODE_NON_LOCAL和IP_VS_RT_MODE_RDR。

int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp, struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
    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);

对于Direct-Route转发模式,其支持IP_VS_RT_MODE_LOCAL、IP_VS_RT_MODE_NON_LOCAL和IP_VS_RT_MODE_KNOWN_NH三种路由标志。

int ip_vs_dr_xmit(struct sk_buff *skb, struct ip_vs_conn *cp, struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
    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_KNOWN_NH, NULL, ipvsh);

对于隧道Tunnel转发模式,其支持四种标志IP_VS_RT_MODE_LOCAL、IP_VS_RT_MODE_NON_LOCAL、IP_VS_RT_MODE_CONNECT和IP_VS_RT_MODE_TUNNEL。在上一节已经看到了IP_VS_RT_MODE_CONNECT标志在路由查找中的作用。

int ip_vs_tunnel_xmit(struct sk_buff *skb, struct ip_vs_conn *cp, struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
    local = __ip_vs_get_out_rt(ipvs, cp->af, skb, cp->dest, cp->daddr.ip,
                   IP_VS_RT_MODE_LOCAL |
                   IP_VS_RT_MODE_NON_LOCAL |
                   IP_VS_RT_MODE_CONNECT |
                   IP_VS_RT_MODE_TUNNEL, &saddr, ipvsh);

另外,IPVS内核代码中,还存在一种bypass转发模式,其仅支持一种路由标志:IP_VS_RT_MODE_NON_LOCAL。当IPVS连接调度不到合适的目的服务器时,连接工作在此模式下,这时使用IP报头中的目的IP进行路由查找,而不是目的服务器IP地址。

int ip_vs_bypass_xmit(struct sk_buff *skb, struct ip_vs_conn *cp, struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
    struct iphdr  *iph = ip_hdr(skb);
    if (__ip_vs_get_out_rt(cp->ipvs, cp->af, skb, NULL, iph->daddr,
                   IP_VS_RT_MODE_NON_LOCAL, NULL, ipvsh) < 0)
        goto tx_error;

对于4.15版本的内核,此函数crosses_local_route_boundary中的变量rt_mode_allow_non_local的赋值存在错误,其应当使用!!(rt_mode & IP_VS_RT_MODE_NON_LOCAL)进行赋值,看了一下5.0版本,以及修正了这个问题。

static inline bool crosses_local_route_boundary(int skb_af, struct sk_buff *skb, int rt_mode, bool new_rt_is_local)
{
    bool rt_mode_allow_local = !!(rt_mode & IP_VS_RT_MODE_LOCAL);
    bool rt_mode_allow_non_local = !!(rt_mode & IP_VS_RT_MODE_LOCAL);
    bool rt_mode_allow_redirect = !!(rt_mode & IP_VS_RT_MODE_RDR);
    bool source_is_loopback;
    bool old_rt_is_local;

#ifdef CONFIG_IP_VS_IPV6
    if (skb_af == AF_INET6) {
        int addr_type = ipv6_addr_type(&ipv6_hdr(skb)->saddr);

        source_is_loopback = (!skb->dev || skb->dev->flags & IFF_LOOPBACK) && (addr_type & IPV6_ADDR_LOOPBACK);
        old_rt_is_local = __ip_vs_is_local_route6( (struct rt6_info *)skb_dst(skb));
    } else
#endif
    {
        source_is_loopback = ipv4_is_loopback(ip_hdr(skb)->saddr);
        old_rt_is_local = skb_rtable(skb)->rt_flags & RTCF_LOCAL;
    }

路由合法性检查如下,对于bypass转发模式,参见函数ip_vs_bypass_xmit,其仅允许路由目的地为本机(IP_VS_RT_MODE_NON_LOCAL),即如果new_rt_is_local为真,将因为rt_mode_allow_local为假而出错返回。 其它的转发模式都由设置允许本地路由标志IP_VS_RT_MODE_LOCAL。

新路由目的为本机的第二个判断为,如果旧的路由目的也是本地,但是当前的转发模式的路由设置并不允许重定向(rt_mode_allow_redirect为假),返回错误。

最后如果新查找到的出口路由目的非本机,但是当前转发模式的路由设置不允许到非本地地址,返回错误;否则,如果报文报文的源地址为回环地址loopback,也返回错误。

    if (unlikely(new_rt_is_local)) {
        if (!rt_mode_allow_local)
            return true;
        if (!rt_mode_allow_redirect && !old_rt_is_local)
            return true;
    } else {
        if (!rt_mode_allow_non_local)
            return true;
        if (source_is_loopback)
            return true;
    }
    return false;

重路由

以上几节讲述的都是与客户端请求处理相关的路由操作。以下的重路由部分是用于处理真实服务器回复报文相关的路由操作,重路由完全是对于NAT/Masq转发模式而言的。如下函数handle_response,其在修改报文的源IP地址后,使用ip_vs_route_me_harder函数进行重新路由。

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;

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

#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));
    }

    /*
     * nf_iterate does not expect change in the skb->dst->dev. It looks like it is not fatal to enable this code for hooks
     * where our handlers are at the end of the chain list and when all next handlers use skb->dst->dev and not outdev.
     * It will definitely route properly the inout NAT traffic when multiple paths are used.
     */

    /* For policy routing, packets originating from this machine itself may be routed differently to packets
     * passing through.  We want this packet to be routed as if it came from this machine itself.  So re-compute the routing information.
     */
    if (ip_vs_route_me_harder(cp->ipvs, af, skb, hooknum))
        goto drop;
}

参见函数中的注释部分。对于策略路由,可指定以及报文的源接口进行路由,所以可导致由本机发送的报文与经过本机的报文的路由不相同。此处的重路由,将为转发的报文重新选择路由源地址,这样其与由本机发起的报文将一致。

可通过PROC系统的文件/proc/sys/net/ipv4/vs/snat_reroute开启和关闭此重路由功能,默认情况下是开启状态(sysctl_snat_reroute)。

如下的重路由函数ip_vs_route_me_harder,如果原路由的目的地为本机,将不进行重路由。否则,交由netfilter系统的ip_route_me_harder函数进行路由选择,最后一个参数RTN_LOCAL表明源地址可使用本机的任意地址。

static int ip_vs_route_me_harder(struct netns_ipvs *ipvs, int af, struct sk_buff *skb, unsigned int hooknum)
{
    if (!sysctl_snat_reroute(ipvs))
        return 0;
    /* Reroute replies only to remote clients (FORWARD and LOCAL_OUT) */
    if (NF_INET_LOCAL_IN == hooknum)
        return 0;
#ifdef CONFIG_IP_VS_IPV6
    if (af == AF_INET6) {
        struct dst_entry *dst = skb_dst(skb);

        if (dst->dev && !(dst->dev->flags & IFF_LOOPBACK) && ip6_route_me_harder(ipvs->net, skb) != 0)
            return 1;
    } else
#endif
        if (!(skb_rtable(skb)->rt_flags & RTCF_LOCAL) && ip_route_me_harder(ipvs->net, skb, RTN_LOCAL) != 0)
            return 1;

内核版本 4.15

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

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

抵扣说明:

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

余额充值