从Linux5.9看Icmp的处理流程

系统 Linux
昨天有个同学碰到发送udp包时收到destination unreachable的icmp包问题,本文简单介绍一下linux5.9中icmp包的处理流程。

[[397621]]

 本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。

昨天有个同学碰到发送udp包时收到destination unreachable的icmp包问题,本文简单介绍一下linux5.9中icmp包的处理流程。

发送icmp包的流程

下面以udp为例看看什么时候会发送destination unreachable包。我们从收到一个udp包开始分析,具体函数是udp_rcv。

  1. int udp_rcv(struct sk_buff *skb){ 
  2.     return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP); 
  3.  
  4. int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, 
  5.            int proto){ 
  6.     struct sock *sk; 
  7.     struct udphdr *uh; 
  8.     unsigned short ulen; 
  9.     struct rtable *rt = skb_rtable(skb); 
  10.     __be32 saddr, daddr; 
  11.     struct net *net = dev_net(skb->dev); 
  12.     bool refcounted; 
  13.     // udp头 
  14.     uh   = udp_hdr(skb); 
  15.     ulen = ntohs(uh->len); 
  16.     // 源目的ip 
  17.     saddr = ip_hdr(skb)->saddr; 
  18.     daddr = ip_hdr(skb)->daddr; 
  19.     // 头部指示大小比实际数据小 
  20.     if (ulen > skb->len) 
  21.         goto short_packet; 
  22.  
  23.     if (proto == IPPROTO_UDP) { 
  24.         uh = udp_hdr(skb); 
  25.     } 
  26.  
  27.     sk = skb_steal_sock(skb, &refcounted); 
  28.  
  29.     // 广播或多播 
  30.     if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST)) 
  31.         return __udp4_lib_mcast_deliver(net, skb, uh, 
  32.                         saddr, daddr, udptable, proto); 
  33.     // 单播,根据地址信息找到对应的socket 
  34.     sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable); 
  35.     // 找到则挂到socket下 
  36.     if (sk) 
  37.         return udp_unicast_rcv_skb(sk, skb, uh); 
  38.  
  39.     // 找不到socket则回复一个ICMP_DEST_UNREACH icmp包 
  40.     icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); 
  41.  
  42.     kfree_skb(skb); 
  43.     return 0; 

我们看到当通过ip包信息找不到对应socket的时候,就会发送一个icmp包给发送端。icmp包结构如下。

收到icmp包的处理流程

我们从收到ip包开始分析。

  1. int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, 
  2.        struct net_device *orig_dev){ 
  3.     struct net *net = dev_net(dev); 
  4.  
  5.     skb = ip_rcv_core(skb, net); 
  6.     if (skb == NULL
  7.         return NET_RX_DROP; 
  8.  
  9.     return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, 
  10.                net, NULL, skb, dev, NULL
  11.                ip_rcv_finish); 

ip层收到包后会继续执行ip_rcv_finish。

  1. static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb){ 
  2.     struct net_device *dev = skb->dev; 
  3.     int ret; 
  4.  
  5.     ret = ip_rcv_finish_core(net, sk, skb, dev, NULL); 
  6.     if (ret != NET_RX_DROP) 
  7.         ret = dst_input(skb); 
  8.     return ret; 

接着执行dst_input

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

input对应的是ip_local_deliver。

  1. int ip_local_deliver(struct sk_buff *skb){ 
  2.     struct net *net = dev_net(skb->dev); 
  3.     return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, 
  4.                net, NULL, skb, skb->dev, NULL
  5.                ip_local_deliver_finish); 

接着执行ip_local_deliver_finish。

  1. static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb){ 
  2.     __skb_pull(skb, skb_network_header_len(skb)); 
  3.  
  4.     rcu_read_lock(); 
  5.     ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol); 
  6.     rcu_read_unlock(); 
  7.  
  8.     return 0; 

ip_local_deliver_finish会执行ip_protocol_deliver_rcu进一步处理,ip_protocol_deliver_rcu的最后一个入参是ip包里的协议字段(上层协议)。

  1. void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol){ 
  2.     const struct net_protocol *ipprot; 
  3.     int raw, ret; 
  4.  
  5. resubmit: 
  6.     // 根据协议找到对应的处理函数,这里是icmp 
  7.     ipprot = rcu_dereference(inet_protos[protocol]); 
  8.     if (ipprot) { 
  9.         ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, 
  10.                       skb); 
  11.         if (ret < 0) { 
  12.             protocol = -ret; 
  13.             goto resubmit; 
  14.         } 
  15.         __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); 
  16.     } 

INDIRECT_CALL_2是一个宏。

  1. #define INDIRECT_CALL_1(f, f1, ...)                 \ 
  2.     ({                              \ 
  3.         likely(f == f1) ? f1(__VA_ARGS__) : f(__VA_ARGS__); \ 
  4.     })#define INDIRECT_CALL_2(f, f2, f1, ...)                 \ 
  5.     ({                              \ 
  6.         likely(f == f2) ? f2(__VA_ARGS__) :         \ 
  7.                   INDIRECT_CALL_1(f, f1, __VA_ARGS__);  \ 
  8.     }) 

因为这里的protocol是icmp协议。所以会执行icmp对应的handler。那么对应的是哪个函数呢?我们看看inet_protos是什么。

  1. struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly; 
  2. int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){ 
  3.     return !cmpxchg((const struct net_protocol **)&inet_protos[protocol], 
  4.             NULL, prot) ? 0 : -1; 

我们看到inet_add_protocol函数是注册协议和对应处理函数的。我们再来看看哪里会调用这个函数。

  1. static int __init inet_init(void) { 
  2.     inet_add_protocol(&icmp_protocol, IPPROTO_ICMP); 
  3.     inet_add_protocol(&udp_protocol, IPPROTO_UDP); 
  4.     ... 

在内核初始化的时候会注册一系列的协议和处理函数。下面我们看看icmp的函数集。

  1. static const struct net_protocol icmp_protocol = { 
  2.     .handler =  icmp_rcv, 
  3.     .err_handler =  icmp_err, 
  4.     .no_policy =    1, 
  5.     .netns_ok = 1, 
  6. }; 

我们看到handler是icmp_rcv。

  1. int icmp_rcv(struct sk_buff *skb){ 
  2.     struct icmphdr *icmph; 
  3.     struct rtable *rt = skb_rtable(skb); 
  4.     struct net *net = dev_net(rt->dst.dev); 
  5.     bool success; 
  6.     // icmp头 
  7.     icmph = icmp_hdr(skb); 
  8.     success = icmp_pointers[icmph->type].handler(skb); 

icmp_rcv根据icmp包的信息做进一步处理。我看看icmp_pointers的定义。

  1. static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = { 
  2.     ... 
  3.     [ICMP_DEST_UNREACH] = { 
  4.         .handler = icmp_unreach, 
  5.         .error = 1, 
  6.     }, 
  7. }; 

这里我们只关注ICMP_DEST_UNREACH的处理。

  1. static bool icmp_unreach(struct sk_buff *skb){ 
  2.     ... 
  3.     icmp_socket_deliver(skb, info); 

继续看icmp_socket_deliver

  1. static void icmp_socket_deliver(struct sk_buff *skb, u32 info){ 
  2.     const struct iphdr *iph = (const struct iphdr *) skb->data; 
  3.     const struct net_protocol *ipprot; 
  4.     int protocol = iph->protocol; 
  5.     // 根据ip头的协议字段找到对应协议处理,这里的iph是触发错误的原始ip头,不是收到icmp包的ip头,所以protocol是udp 
  6.     ipprot = rcu_dereference(inet_protos[protocol]); 
  7.     if (ipprot && ipprot->err_handler) 
  8.         ipprot->err_handler(skb, info); 

接着执行udp的err_handler,是udp_err

  1. int udp_err(struct sk_buff *skb, u32 info){ 
  2.     return __udp4_lib_err(skb, info, &udp_table);}int __udp4_lib_err(struct sk_buff *skb, u32 info, struct udp_table *udptable){ 
  3.     struct inet_sock *inet; 
  4.     const struct iphdr *iph = (const struct iphdr *)skb->data; 
  5.     struct udphdr *uh = (struct udphdr *)(skb->data+(iph->ihl<<2)); 
  6.     const int type = icmp_hdr(skb)->type; 
  7.     const int code = icmp_hdr(skb)->code; 
  8.     bool tunnel = false
  9.     struct sock *sk; 
  10.     int harderr; 
  11.     int err; 
  12.     struct net *net = dev_net(skb->dev); 
  13.     // 根据报文信息找到对应socket 
  14.     sk = __udp4_lib_lookup(net, iph->daddr, uh->dest, 
  15.                    iph->saddr, uh->source, skb->dev->ifindex, 
  16.                    inet_sdif(skb), udptable, NULL); 
  17.     err = 0; 
  18.     harderr = 0; 
  19.     inet = inet_sk(sk); 
  20.  
  21.     switch (type) { 
  22.     case ICMP_DEST_UNREACH: 
  23.         err = EHOSTUNREACH; 
  24.         if (code <= NR_ICMP_UNREACH) { 
  25.             harderr = icmp_err_convert[code].fatal; 
  26.             err = icmp_err_convert[code].errno; 
  27.         } 
  28.         break; 
  29.         ... 
  30.     } 
  31.  
  32.     // 设置错误信息到socket 
  33.     sk->sk_err = err; 
  34.     sk->sk_error_report(sk); 
  35. out
  36.     return 0; 

__udp4_lib_err设置了错误信息,然后调用sk_error_report。sk_error_report是在调用socket函数时赋值的(具体在sock_init_data函数)。

  1. sk->sk_error_report =   sock_def_error_report; 

接着看sock_def_error_report

  1. static void sock_def_error_report(struct sock *sk){ 
  2.     struct socket_wq *wq; 
  3.  
  4.     rcu_read_lock(); 
  5.     wq = rcu_dereference(sk->sk_wq); 
  6.     if (skwq_has_sleeper(wq)) 
  7.         wake_up_interruptible_poll(&wq->wait, EPOLLERR); 
  8.     sk_wake_async(sk, SOCK_WAKE_IO, POLL_ERR); 
  9.     rcu_read_unlock();}static inline void sk_wake_async(const struct sock *sk, int how, int band){ 
  10.     if (sock_flag(sk, SOCK_FASYNC)) { 
  11.         rcu_read_lock(); 
  12.         sock_wake_async(rcu_dereference(sk->sk_wq), how, band); 
  13.         rcu_read_unlock(); 
  14.     } 

我们看到如果进程阻塞在socket则会被唤醒,或者设置了SOCK_FASYNC标记则收到信号。

后记:本文简单介绍了icmp的产生和处理过程,后面有时间再细化一下。

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-01-12 09:05:11

Linus Torva进程页面锁

2010-06-24 14:58:06

ICMP协议消息

2021-07-14 09:48:15

Linux源码Epoll

2021-07-15 14:27:47

LinuxSocketClose

2010-07-28 17:19:28

ICMP协议

2020-10-10 07:00:16

LinuxSocketTCP

2021-06-10 09:52:33

LinuxTCPAccept

2010-08-02 14:29:46

LinuxPingICMP

2010-08-02 14:36:52

ICMPLinux

2021-10-21 05:41:27

QueenSono安全工具提取数据

2012-09-28 09:27:50

LinuxRed Hat

2022-03-25 10:16:40

CentOSLinux开源

2010-04-02 17:44:45

MiniPCI无线网卡

2010-07-28 17:36:17

2010-07-13 22:16:30

INBOUND ICM

2020-10-15 10:10:31

Linux数据中心IT

2020-10-14 14:31:37

LinuxTCP连接

2009-06-14 18:43:57

LinuxWindows对比

2019-04-28 16:10:50

设计Redux前端

2021-06-26 07:04:24

Epoll服务器机制
点赞
收藏

51CTO技术栈公众号