Chinaunix首页 | 论坛 | 博客
  • 博客访问: 59609
  • 博文数量: 7
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 90
  • 用 户 组: 普通用户
  • 注册时间: 2015-05-10 20:54
个人简介

多学习,多分享!

文章分类
文章存档

2018年(7)

我的朋友

分类: LINUX

2018-08-20 01:16:27

本文为作者原创,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。

作者:misteryoung

博客:http://blog.chinaunix.net/uid/20706239.html
============================================================

1 前言

    本文旨在介绍Linux作为3层设备(路由器)对收到的报文是如何处理的。
    说明:本文的代码对应的内核版本为:Linux-2.6.32.11

2 分析

    我们都知道,路由器根据报文的目的IP(不考虑策略路由的情况)的路由结果来决定报文的走向:
        1)上送本地;
        2)转发出去;
        3)路由失败,报文被丢弃(回应ICMP差错报文)
    下面针对这几种情况一次分析。

2.1 本地处理

    对于上送本地的报文,3层(路由)处理完以后,需要交给4层处理,根据协议的不同,报文会被TCP/UDP/ICMP等模块处理。

2.1.1 TCP报文

    对于TCP报文来说,内核首先会查找socket,若查找失败,会回复TCP reset报文;否则,若是协议报文(例如SYN或者FIN包),内核进行TCP状态机的处理,若是数据报文,则会将报文交给用户进程处理(放入收包队列,并唤醒相关进程)。

点击(此处)折叠或打开

  1.         tcp_v4_rcv函数代码片段 --查找socket
  2.     sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
  3.     if (!sk)
  4.         goto no_tcp_socket;

  5. process:
  6.     if (sk->sk_state == TCP_TIME_WAIT)
  7.         goto do_time_wait

点击(此处)折叠或打开

  1.     tcp_v4_rcv函数代码片段 --无相应的socket,则回复TCP reset
  2.         if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
  3.     bad_packet:
  4.             TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
  5.         } else {
  6.             tcp_v4_send_reset(NULL, skb);
  7.         }

2.1.2 UDP报文

    对于UDP报文来说,内核依然会查找socket,若查找失败,则回复ICMP的端口不可达报文;否则,或者将报文交给用户进程处理(放入收包队列,并唤醒相关进程),或者内核继续处理。

点击(此处)折叠或打开

  1.     udp_rcv -> __udp4_lib_rcv函数代码片段 -- 查找socket
  2.     if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
  3.         return __udp4_lib_mcast_deliver(net, skb, uh,
  4.                 saddr, daddr, udptable);

  5.     sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

  6.     if (sk != NULL) {
  7.         int ret = udp_queue_rcv_skb(sk, skb);
  8.         sock_put(sk);

  9.         /* a return value > 0 means to resubmit the input, but
  10.          * it wants the return to be -protocol, or 0
  11.          */
  12.         if (ret > 0)
  13.             return -ret;
  14.         return 0;
  15.     }
  1.     udp_rcv -> __udp4_lib_rcv函数代码片段 -- 无相应的socket,则回复端口不可达差错报文
  2.     UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
  3.     icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

  4.     /*
  5.      * Hmm. We got an UDP packet to a port to which we
  6.      * don't wanna listen. Ignore it.
  7.      */
  8.     kfree_skb(skb);
  9.     return 0;
    下面说一下,哪些UDP报文到了4层,查找完socket后还需要内核进一步处理。
    下面的代码展示了,udp_queue_rcv_skb函数确实会进行特殊处理,那么哪些报文需要特殊处理呢?

点击(此处)折叠或打开

  1.     udp_rcv -> __udp4_lib_rcv -> udp_queue_rcv_skb函数代码片段
  2.     if (up->encap_type) {
  3.         /*
  4.          * This is an encapsulation socket so pass the skb to
  5.          * the socket's udp_encap_rcv() hook. Otherwise, just
  6.          * fall through and pass this up the UDP socket.
  7.          * up->encap_rcv() returns the following value:
  8.          * =0 if skb was successfully passed to the encap
  9.          * handler or was discarded by it.
  10.          * >0 if skb should be passed on to UDP.
  11.          * <0 if skb should be resubmitted as proto -N
  12.          */

  13.         /* if we're overly short, let UDP handle it */
  14.         if (skb->len > sizeof(struct udphdr) &&
  15.          up->encap_rcv != NULL) {
  16.             int ret;

  17.             ret = (*up->encap_rcv)(sk, skb);
  18.             if (ret <= 0) {
  19.                 UDP_INC_STATS_BH(sock_net(sk),
  20.                          UDP_MIB_INDATAGRAMS,
  21.                          is_udplite);
  22.                 return -ret;
  23.             }
  24.         }

  25.         /* FALLTHROUGH -- it's a UDP Packet */
  26.     }
    1)L2TP报文
    对应的处理函数为pppol2tp_udp_encap_recv,该函数处理UDP报文内部的L2TP及PPP协议,然后根据PPP内部的数据决定是通过/dev/ppp
将报文上送用户态,还是解封装后将报文重新入队列(调用netif_rx函数)。
    2)IPSec
    对应的处理函数为xfrm4_udp_encap_rcv,该函数在IPSec穿NAT时被注册。该函数将UDP剥掉以后,将内层数据交由ESP模块进一步处理。

2.1.3 ICMP报文

    不同于TCP/UDP,ICMP报文不会查找socket或者不会首先查找socket。而是根据不同的type,内核调用不同的函数做不同的处理。
     简单来说,ICMP共分为两类:
     1、PING Request和Reply
          1)对于Request报文,内核直接回复;而对于Reply会直接丢弃。
          注:对高版本内核(3.0及以上)来说,Request报文,内核依然直接回复,而对于Reply,内核会查找socket,若成功,则将报文交给用户进程处理。因为高版本内核支持ping socket的创建,其流程大致如下是:
               socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)
               bind              指定报文的ID
               send/recv     收发包
          而对于低版本内核,只能通过socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)实现,并通过raw socket收包流程,将报文上送用户进程。当然,这与ICMP协议无关,无论4层是哪种协议(包括TCP/UDP),均可以将报文(保留IP头)通过raw socket机制上送用户进程。
    2、ICMP差错报文
          对差错报文,内核会解析内层协议,并根据内层协议调用相应的差错处理函数,后者根据原始报文(内层报文)查找socket,若查找失败,则不做任何处理,若成功则进一步处理。

点击(此处)折叠或打开

  1.     icmp_rcv -> icmp_unreach函数代码片段
  2.     rcu_read_lock();
  3.     ipprot = rcu_dereference(inet_protos[hash]);
  4.     if (ipprot && ipprot->err_handler)
  5.         ipprot->err_handler(skb, info);
  6.     rcu_read_unlock();

    TCP、UDP对应的差错处理函数(err_handler)分别为:tcp_v4_err,udp_err。这里不做介绍,感兴趣的同学可自行研究。而ICMP对应的err_handler指向空。

2.1.4 其他报文

    内核注册基于IPv4(3层)的4层处理函数的函数是inet_add_protocol,搜索inet_add_protocol,你会发现内核注册了大量的处理函数,除了常见的TCP、UDP、ICMP,还有IGMP、GRE、AH、ESP等等。这里对这些报文的处理就不做介绍了,各位感兴趣的同学可以自行研究代码。

2.2 转发处理

    这里要提一下,路由查找是如何影响报文的处理流程:

    若是本地报文,则skb_dst(skb)->input被赋值为ip_local_deliver;
    若是转发报文,则skb_dst(skb)->input被赋值为ip_forward;
    若是路由失败,则skb_dst(skb)->input被赋值为ip_error;
    在ip_rcv_finish最后调用dst_input(skb),因此上面的函数会被调用。

点击(此处)折叠或打开

  1. static inline int dst_input(struct sk_buff *skb)
  2. {
  3.     return skb_dst(skb)->input(skb);
  4. }

2.2.1 转发流程

    这里简单列出来函数的调用关系,感兴趣的同学,可以根据该线索分析代码。
    ip_rcv -> ip_rcv_finish -> ip_forward -> ip_forward_finish -> ip_output -> ip_finish_output -> ip_finish_output2 -> 交给ARP模块处理

2.2.2 ARP模块

     ARP模块收到上层传递过来的报文(skb)后,会将报文缓存在neigh->arp_queueskb_dst(skb)->neighbour指向neigh)中,并调用arp_solicit(neigh->ops->solicit指向该函数)发送ARP请求,并等待答复。收到回应报文后,会将学习到的MAC替换缓存skb的目的MAC,并发送出去。这样一个报文就被成功发送了出去。

2.3 路由失败处理

    2.2章节中提到,skb_dst(skb)->input被赋值为ip_error,因此ip_error被调用,让我们看看该函数的具体实现:

点击(此处)折叠或打开

  1. static int ip_error(struct sk_buff *skb)
  2. {
  3.     struct rtable *rt = skb_rtable(skb);
  4.     unsigned long now;
  5.     int code;

  6.     switch (rt->u.dst.error) {
  7.         case EINVAL:
  8.         default:
  9.             goto out;
  10.         case EHOSTUNREACH:
  11.             code = ICMP_HOST_UNREACH;
  12.             break;
  13.         case ENETUNREACH:
  14.             code = ICMP_NET_UNREACH;
  15.             IP_INC_STATS_BH(dev_net(rt->u.dst.dev),
  16.                     IPSTATS_MIB_INNOROUTES);
  17.             break;
  18.         case EACCES:
  19.             code = ICMP_PKT_FILTERED;
  20.             break;
  21.     }

  22.     now = jiffies;
  23.     rt->u.dst.rate_tokens += now - rt->u.dst.rate_last;
  24.     if (rt->u.dst.rate_tokens > ip_rt_error_burst)
  25.         rt->u.dst.rate_tokens = ip_rt_error_burst;
  26.     rt->u.dst.rate_last = now;
  27.     if (rt->u.dst.rate_tokens >= ip_rt_error_cost) {
  28.         rt->u.dst.rate_tokens -= ip_rt_error_cost;
  29.         icmp_send(skb, ICMP_DEST_UNREACH, code, 0);
  30.     }

  31. out:    kfree_skb(skb);
  32.     return 0;
  33. }
    可以清楚的看到,ip_error回复了一个
ICMP 网络不可达的差错报文

3 总结

    本文分析了,Linux作为路由器在收到报文后,可能存在的3种处理流程,分别是:
    1)上送本地;
    2)转发出去;
    3)路由失败,报文被丢弃(回应ICMP差错报文)
    这3种处理流程是由路由结果中的skb_dst(skb)->input所决定的。这里再强调一下skb_dst(skb)->input对应3个函数:
    1)ip_local_deliver;
    2)ip_forward;
    3)ip_error;
阅读(7288) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~