Linux对转发报文究竟做了哪些修改

本文为作者原创,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:youThinker
博客:www.cnblogs.com/youthinker
======================================================================================================

  • 1 介绍

      前面的文章(http://blog.chinaunix.net/uid-20706239-id-5788939.html)主要讲解Linux作为路由器,对收到的报文是如何处理的。这次关注的是转发报文,而且重点关注Linux作为路由器对转发报文的修改。
    注意:
        1)本文分析的是路由器,而不是防火墙等安全设备;
        2)本文涉及到的代码对应内核版本为:Linux-2.6.32.11;

  • 2 路由器基本功能

     路由器要想正常转发报文必须具有路由功能,也就是在收到报文后,路由器可将报文从某一物理口转发出去。换句话说就是,可以将收到的报文交给下一跳设备,再由下一跳设备继续路由,直到目的地。
     但是,即使报文能正确路由到目的地(服务器),客户端也未必能收到服务器的响应报文。因为,若是客户端处于私网内,处于公网的服务器的响应报文无法被路由到私网。这就需要NAT功能。
     这些就是一个路由器能正常转发报文所具备的基本功能。当然,ARP功能,无论是路由器还是主机,都是必备功能。

  • 2.1 路由功能
  • 2.1.1 路由查找

      对于收到的报文,Linux通过路由查找来决定走向:或上送本地(4层),或转发,或丢弃(可能发送ICMP差错报文)。

        ip_route_input_slow函数代码片段    --路由查找
        /*
         *    Now we are ready to route packet.
         */
        if ((err = fib_lookup(net, &fl, &res)) != 0) {
            if (!IN_DEV_FORWARD(in_dev))
                goto e_hostunreach;
            goto no_route;
        }
        free_res = 1;
  • 2.1.2 转发

       对于转发报文,前文已经提到,报文(skb)会被ip_forward函数处理。

        ip_forward函数代码片段        --TTL递减
        /* We are about to mangle packet. Copy */
        if (skb_cow(skb, LL_RESERVED_SPACE(rt->u.dst.dev)+rt->u.dst.header_len))
            goto drop;
        iph = ip_hdr(skb);

        /* Decrease ttl after skb cow done */
        ip_decrease_ttl(iph);

    如上所示,此处涉及到报文的第一次修改:IP头TTL字段减1。

    因为IP头被修改,因此IP头的校验和也要随之被修改,也即重新计算。但这里的重新计算并非要将整个IP头从头算一遍校验和,而是巧妙的采用差值计算法(当然,这是我自己起的名字),代码如下所示:
 

    /* The function in 2.2 was invalid, producing wrong result for
     * check=0xFEFF. It was noticed by Arthur Skawina _year_ ago. --ANK(000625) */
    static inline
    int ip_decrease_ttl(struct iphdr *iph)
    {
        u32 check = (__force u32)iph->check;
        check += (__force u32)htons(0x0100);
        iph->check = (__force __sum16)(check + (check>=0xFFFF));
        return --iph->ttl;
    }

     注:在TTL减1前,要对其做合法性校验,即其不能小于等于1,否则报文被丢弃。
     另外,思考一下,TTL的作用是什么?

     答:在存在环路的情况下,可以避免报文无限循环。

     笔者在工作中做性能测试时,曾经遇到过环路。由于存在环路,导致即使流量不太大,设备的CPU利用率仍然很高。因为一个报文被循环发送几十次。最简单的配置就是,两个设备或者主机直连,然后互相将对方设置为下一跳,这样一个正常转发的报文就会在两台设备/主机间来回发送,直到TTL被减为0,被丢弃。因此,TTL可以避免一个报文被无限循环发送。
     经过ip_forward处理后,后面依次调用ip_forward_finish -> ip_output -> ip_finish_output -> ip_finish_output2。
     在ip_finish_output中又会涉及到对报文的修改:若报文超过出接口的MTU,需要对报文分片处理。这涉及到IP头的长度字段及校验和字段等内容的修改。

    static int ip_finish_output(struct sk_buff *skb)
    {
    #if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
        /* Policy lookup after SNAT yielded a new policy */
        if (skb_dst(skb)->xfrm != NULL) {
            IPCB(skb)->flags |= IPSKB_REROUTED;
            return dst_output(skb);
        }
    #endif
        if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
            return ip_fragment(skb, ip_finish_output2);    --若大于出接口MTU,则进行分片处理,并且每个分片报文均调用ip_finish_output2
        else
            return ip_finish_output2(skb);
    }
static inline int ip_finish_output2(struct sk_buff *skb)
{
    struct dst_entry *dst = skb_dst(skb);
    struct rtable *rt = (struct rtable *)dst;
    struct net_device *dev = dst->dev;
    unsigned int hh_len = LL_RESERVED_SPACE(dev);

    。。。。。。。。

    if (dst->hh)
        return neigh_hh_output(dst->hh, skb);   --交给ARP模块处理,若有缓存,则走快速路径
    else if (dst->neighbour)
        return dst->neighbour->output(skb);    --交给ARP模块处理,若无缓存,则走慢速路径

    if (net_ratelimit())
        printk(KERN_DEBUG "ip_finish_output2: No header cache and no neighbour!\n");
    kfree_skb(skb);
    return -EINVAL;
}
  • 2.1.3 ARP学习

    3层处理完后,将报文交给2层(neighbour层,对于IPv4来说就是ARP层) 。
       ARP模块首先将报文缓存在neigh->arp_queue队列中,然后启动定时器(neigh_timer_handler)进行ARP状态机的处理,定时器除了维护ARP表项的状态切换外,一个重要的任务就是调用arp_solicit发送ARP请求。

        neigh_timer_handler函数代码片段        --发送ARP请求
        if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
            struct sk_buff *skb = skb_peek(&neigh->arp_queue);
            /* keep skb alive even if arp_queue overflows */
            if (skb)
                skb = skb_copy(skb, GFP_ATOMIC);
            write_unlock(&neigh->lock);
            neigh->ops->solicit(neigh, skb);
            atomic_inc(&neigh->probes);
            kfree_skb(skb);
        } else {
    out:
            write_unlock(&neigh->lock);
        }

    在收到ARP响应后,会依次执行下列函数:arp_process -> neigh_update -> neigh_resolve_output -> neigh_event_send -> dev_hard_header

    static inline int dev_hard_header(struct sk_buff *skb, struct net_device *dev,
                      unsigned short type,
                      const void *daddr, const void *saddr,
                      unsigned len)
    {
        if (!dev->header_ops || !dev->header_ops->create)
            return 0;

        return dev->header_ops->create(skb, dev, type, daddr, saddr, len);
    }

   其中,dev->header_ops->create指向eth_header。

    int eth_header(struct sk_buff *skb, struct net_device *dev,
         unsigned short type,
         const void *daddr, const void *saddr, unsigned len)
    {
        struct ethhdr *eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);

        if (type != ETH_P_802_3)
            eth->h_proto = htons(type);
        else
            eth->h_proto = htons(len);

        /*
         * Set the source hardware address.
         */

        if (!saddr)
            saddr = dev->dev_addr;
        memcpy(eth->h_source, saddr, ETH_ALEN);

        if (daddr) {
            memcpy(eth->h_dest, daddr, ETH_ALEN);
            return ETH_HLEN;
        }

        /*
         * Anyway, the loopback-device should never use this function...
         */

        if (dev->flags & (IFF_LOOPBACK | IFF_NOARP)) {
            memset(eth->h_dest, 0, ETH_ALEN);
            return ETH_HLEN;
        }

        return -ETH_HLEN;
    }

   eth_header函数负责将目的MAC替换为下一跳MAC。然后后面的流程是,将skb交给驱动处理,最终驱动通过硬件将报文发送出去。只有填写了正确的MAC,下一跳才不会丢弃报文。

  • 2.2 NAT功能

      前面已经提到,为了保证回来的报文能被正确转发到客户端,路由器需要做NAT(SNAT)处理。
      NAT模块是基于链接跟踪及netfilter的,也就是NAT是在HOOK点上实现的,对SNAT来说,则是在POSTROUTING上实现的。
      此处又涉及到了对报文的修改。但SNAT模块都对报文哪些字段做修改呢?
    1)源IP是必须的;
    2)源端口可能也会修改,因为可能涉及到会话冲突;
    3)即使源端口不做修改,IP地址改变了,4层(TCP/UDP)校验和也是要重新计算的;
    注:UDP校验和是可选项,即,可以不做计算,填0即可。对端收到后,看到是0,就不会做校验和的验证。

     那问题是,对端怎么知道校验和字段为0,是发送方未做校验和计算还是校验和本身即为0

     这里,有个窍门就是,如果校验和计算后为0,则将0替换为0xffff,这样可以保证对端会对报文做校验和验证,并且验证通过

        udp_manip_pkt函数代码片段
        if (hdr->check || skb->ip_summed == CHECKSUM_PARTIAL) {
            inet_proto_csum_replace4(&hdr->check, skb, oldip, newip, 1);
            inet_proto_csum_replace2(&hdr->check, skb, *portptr, newport,
                         0);
            if (!hdr->check)
                hdr->check = CSUM_MANGLED_0; //CSUM_MANGLED_0等于0xffff
        }

   4)因为IP地址变了,因此3层校验和也跟着改变;
    同上面TTL的改变一样,3层校验和的计算也不用重新计算所有数据(4层校验和也一样)

        manip_pkt函数代码片段
        if (maniptype == IP_NAT_MANIP_SRC) {
            csum_replace4(&iph->check, iph->saddr, target->src.u3.ip);
            iph->saddr = target->src.u3.ip;
        } else {
            csum_replace4(&iph->check, iph->daddr, target->dst.u3.ip);
            iph->daddr = target->dst.u3.ip;
        }
  • 3 小结
  • 3.1 初步总结

      上一章节,跟着报文的转发流程分析哪些模块可能会对报文做修改及修改了哪修部分:
   1、转发模块
    1)修改TTL及3层校验和;
    2)分片:修改报文长度、校验和等字段;
   2、ARP模块
    修改目的MAC;
   3、NAT
    修改源IP、源端口(可能)及3、4层校验和;
  这就是根据上一章节得出的结论。不过,貌似忽略了一点,那就是ARP模块还会修改转发报文的源MAC,将其修改为出接口的MAC

  这是被很多人所忽略的,因为貌似源MAC对报文的转发毫无影响。但这就是事实,不信你可以看一下前面贴出的eth_header函数。
  笔者最开始也觉得的修改源MAC很奇怪,但当看了源代码,并亲自抓包后,才相信这是事实。但这样做得目的是什么呢?笔者想了一下,原因可能有以下几个:
    1)MAC是2层属性,作为3层设备的路由器不应该将一侧的2层属性告诉另一侧;
    2)对于下一跳来说,只有当前设备与其2层可达,下一跳设备不应该学到其他2层网络的MAC;
    3)当前设备与下一跳间若连接交换机,交换机因为收到不断变化源MAC的报文而学习大量的MAC表项,这不合理;
    4)下一跳可能会对MAC进行认证;
    5)其他;

  • 3.2 最后的小结

    对于经路由器转发的报文,除了修改TTL、源IP、源端口及3/4层校验和外,还必须要修改目的MAC,以及源MAC。

posted @ 2018-08-25 13:31  youThinker  阅读(1825)  评论(0)    收藏  举报