TCP/IP协议栈之LwIP(四)---网络诊断与状态查询(ICMPv4 + ICMPv6)

一、ICMP协议简介

架构IP网络时需要特别注意两点:确认网络是否正常工作;遇到异常时进行问题诊断。例如,一个刚刚搭建好的网络,需要验证该网络的设置是否正确,为了确保网络能够按照预期正常工作,一旦遇到什么问题需要立即制止问题的蔓延。IP协议虽然完成了数据报在各个主机之间的递交,但它只提供了一种无连接不可靠的数据报交付服务,协议本身并不提供任何错误检验与恢复机制,这就需要另一种协议ICMP(Internet Control Message Protocol)提供相应的错误检验与状态查询机制。

ICMP协议的主要功能包括,确认IP包是否成功送达目标地址,通知在发送过程当中IP包被废弃的具体原因,改善网络设置等。有了这些功能后,就可以获得网络是否正常、设置是否有误以及设备有何异常等信息,从而便于进行网络上的问题诊断。

在IP通信中如果某个IP包因为某种原因未能到达目标地址,那么这个具体的原因将由ICMP负责通知。ICMP的这种通知消息会使用IP数据报进行发送,从这点看ICMP有点像上层传输层协议,但由于ICMP并不为应用程序提供传输服务,所以仍算作网络层协议。ICMP报文封装位置与格式如下图示:
ICMP报文两次封装
ICMP报文格式

1.1 ICMPv4报文功能

从功能上划分,ICMP消息报文大致可以分为两类:一类是通知出错原因的差错报告报文;另一类是用于诊断的查询消息报文。差错报告报文主要用来向IP数据报源主机返回一个差错报告信息,这个错误报告信息产生的原因是路由器或主机不能对当前数据报进行正常的处理,例如无法将数据报递交给有效的上层协议、数据报因为生存时间TTL减为0而被删除等。查询报文用于一台主机向另一台主机查询特定的信息,通常查询报文都是成对出现的,即源主机发起一个查询报文,在目的主机收到该报文后,会按照查询报文约定的格式为源主机返回一个应答报文。两大种类的ICMPv4报文及其常见类型如下表示:
ICMPv4报文分类
目的站不可达:IP路由器无法将UO数据报发送到目的地址时,会给发送端主机返回一个目的不可达的ICMP消息报文,并在这个消息报文中显示不可达的具体原因(前篇介绍的路径MTU发现就是根据代码4的分片位实现的),如下表示:
目的站不可达代码
数据报超时:数据报超时可以用来防止数据报在网络中被循环的路由,在IP包中有一个字段叫TTL(Time To Live,生存时间),它的值随着每经过一次路由器就会减1,直到减到0时该IP包会被丢弃,IP路由器将会发送一个ICMP超时消息报文给发送端主机以通知该包已被丢弃(网络上常用的traceroute命令就是充分利用ICMP超时消息实现的),超时原因主要有以下两类:
ICMP超时代码
源站抑制:为了给IP协议增加一种流量控制而设计,当路由器或主机因拥塞而丢弃数据报时,它可以向源站发送ICMP源站抑制报文,这个报文将告诉源站两个消息:第一,你的数据报发得太快,我已经丢弃了;第二,路径中出现了拥塞,请放慢你的数据报发送频率。

重定向:如果路由器发现发送端主机使用了次优的路径发送数据包,那么它会返回一个ICMP重定向消息报文告诉源主机改变它的路由表,这个消息报文中包含了最合适的路由信息和源数据,以提高数据报的递交效率。

数据报参数错误:数据报在网络中传输时,其首部中出现的任何二义性都可能会产生严重的问题,如果路由器或主机发现了这种二义性或者数据报中的某个字段丢失,路由器会直接丢弃数据报,并向源主机返回一个数据报参数错误报文。

回送请求或应答:用于进行通信的主机或路由器之间,判断所发送的数据包是否已经成功到达对端的一种消息报文。可以向对端主机发送回送请求消息,也可以接收对端主机发回来的回送应答消息,网络上最常用的ping命令就是利用这个消息报文实现的。

路由器询问和通告:主要用于发现与自己相连网络中的路由器,当一台主机发出ICMP路由器请求时,路由器则返回相应的通告报文。

时间戳请求或回答:在互联网中的两台主机能够使用时间戳请求或回答报文来确定数据报在彼此之间往返所需要的时间。

地址掩码请求或回答:主要用于主机或路由器想要了解子网掩码的情况,可以向那些目标主机或路由器发送ICMP地址掩码请求报文,然后通过接收ICMP地址掩码应答报文获取子网掩码的信息。

1.2 ICMPv6报文功能

IPv4中ICMP仅作为一个辅助作用支持IPv4,即使没有ICMP仍可以实现IP通信。然而在IPv6中,ICMP的作用被扩大了,如果没有ICMPv6,IPv6就无法进行正常通信。比如在IPv6中从IP地址定位MAC地址的协议从ARP转为ICMP的邻居探索消息(Neighbor Discovery),这种邻居探索消息融合了IPv4的ARP、ICMP重定向以及ICMP路由器选择消息等功能于一体,甚至还提供自动设置IP地址的功能。

ICMPv6中将ICMP也大致分为两类:一类是错误报告消息(类型0–127);另一类是信息查询消息(类型128–255)。常用的消息类型如下表示:
ICMPv6错误报告消息
上面的错误报告消息含义跟ICMPv4类似,其中的数据包过大是指数据包在传递过程中,其大小超过了链路的MTU值,路由器会向源节点发送此消息,此消息也被用于链路MTU发现协议。
ICMPv6信息查询消息
上面的信息查询报文多数也跟ICMPv4类似,下面主要介绍下多播监听发现消息(Multicast Listener Discovery)、邻居探索消息(Neighbor Discovery Protocol)、反邻居探索消息等,更多信息可以参考博客:IPv6重臣之ICMPv6

多播监听发现:包括从类型130至类型132的消息,在多播通信中,用于确认是否有接收端,这里的MLD(Multicast Listener Discovery)可以实现IPv4中IGMP(Internet Group Management Protocol)的功能。其中多播监听报告与结束消息用于通知多播路由器(需要支持多播路由协议,便于将组关系转发给互联网上其它的多播路由器)加入与退出多播组,多播监听查询消息用于周期性探寻本地局域网上主机是否还为多播组成员。

邻居探索消息:ICMPv6中从类型133至类型137的消息叫做邻居探索消息,其中邻居请求消息用于查询IPv6的地址与MAC地址的对应关系(与IPv4中的ARP协议功能类似),并由邻居宣告消息得知MAC地址,邻居请求消息利用IPv6的多播地址实现传输。

反邻居探索消息:反向邻居探索请求消息用于查询MAC地址与IPv6地址的对应关系(与IPv4中的RARP协议功能类似),并由反向邻居探索宣告消息得知IPv6地址,反向邻居探索消息也利用了IPv6的多播地址实现传输。

由于在IPv6中实现了即插即用的功能,所以在没有DHCP服务器的环境下也能实现IP地址的自动获取。如果是一个没有路由器的网络,就使用MAC地址作为链路本地单播地址(前篇介绍过IPv6地址中的一种,网络标识为FE80::/10,主机标识为64比特版的MAC地址EUI-64)。而在一个有路由器的网络环境中,可以从路由器获得IPv6地址的前面部分的网络标识,后面部分的主机标识则由MAC地址进行设置(需要转换为EUI-64),此时可以利用路由器请求与宣告消息进行设置。

二、PC常用网络命令

前面介绍ICMP时已经提到了两个常用的网络命令ping与traceroute:ping命令利用ICMP的回送请求/应答消息报文检查网络的连通性与往返估计时间;traceroute命令利用ICMP超时消息报文显示出由执行程序的源主机到达目的主机之前历经多少路由器。下面分别看下这两个命令的使用示例:
ping命令示例
tracert命令示例
上面是在windows系统上运行命令截的图,主要是考虑到windows上命令支持的参数排版解释更详细。这两个命令也是在进行网络错误监测时最常用到的命令,前面介绍的查看ARP缓存表,查看路由表,查看网卡接口信息,甚至后面将要介绍的查询网络端口连接信息都有相关的命令提供功能支持,下面列举出linux常用的网络命令如下:

Linux常用网络命令命令功能描述
ifconfig可以手动启动、查看、修改网络接口的相关参数,可以修改的参数包括IP地址、子网掩码、默认网关、MTU等;
iwlist
iwconfig
iwlist可以利用无线网卡进行无线AP的检测并获得相关数据;
iwconfig可以设置无线网卡的相关参数;
ip网络参数综合命令,除了可以设置一些基本的网络参数外,还能执行额外的IP协议,包括多IP的设置,功能很强大;
arp查看IP地址与MAC地址对的缓存表信息;
route查看目的IP地址、子网掩码、默认网关等路由状态信息;
nslookup
host
查看主机名与IP地址的对应关系信息;
ping查看目的主机是否可以访问到,并可获知往返时间等信息;
traceroute跟踪源主机到目的主机所通过的各个路由节点信息;
netstat查看网络传输层各端口的连接状态信息,比如目前有多少连接已建立或出现问题等;
telnet可用于远程登录并访问目的主机;
ftp
lftp
可与远程主机间进行文件传送;
tcpdump
wireshark
可捕获网络数据包,用于分析数据包流向甚至监听数据包内容;
其中tcpdump是命令接口式数据包分析软件,wireshark是图形接口数据包分析软件;

上面的命令使用时可以直接查看命令帮助,如果只查看简略的命令参数信息,可以使用–help(或-h)获得简略命令帮助信息,如果想查看详细的帮助信息可以使用man,即在命令名前加man(全称manual使用手册的意思)。下面看看windows系统的常用网络命令:

windows常用网络命令命令功能描述
ipconfig查询网络接口信息,包括各网卡的MAC地址、IP地址/子网掩码/默认网关,甚至DHCP/DNS服务器地址等信息;
netshNetwork Shell是一个 Windows 系统本身提供的网络配置命令行工具;
arp查看或修改IP地址与MAC地址对的缓存表信息;
route查看或修改目的IP地址、子网掩码、默认网关等路由状态信息;
nslookup查看主机名与IP地址的对应关系信息;
ping查看目的主机是否可以访问到,并可获知往返时间等信息;
tracert跟踪源主机到目的主机所通过的各个路由节点信息;
netstat查看网络传输层各端口的连接状态信息,比如目前有多少连接已建立或出现问题等;
net可以查看我们的管理网络环境、服务、用户、登陆等信息内容;
telnet可用于远程登录并访问目的主机;
ftp可与远程主机间进行文件传送;
wireshark可捕获网络数据包,用于分析数据包流向甚至监听数据包内容;

上面的命令依然可以直接查看帮助信息获得所支持的参数及用法,在命令后加上"/?"即可获得该命令的帮助信息。也可以在命令名前加help查询该命令的用法,但这种方式支持的命令相对较少,如果想获得更强大的命令交互支持,可以使用powershell。

三、ICMP协议实现

总结下LwIP中实现了ICMP协议的哪些功能?在数据报处理过程中,根据差错情况的不同,能够发送两种类型的差错报文:目的站不可达差错报文和数据报超时差错报文。此外,LwIP能够响应一种查询报文,即回送请求报文,协议栈会根据收到的回送请求报文产生一个回送应答报文。

3.1 ICMPv4数据报描述

ICMP的数据报格式前面介绍过,其中的首部剩余字节在差错报文与查询报文中有些不同,两种报文的结构分别如下图示:
ICMPv4差错报文
ICMPv4查询报文
ICMP报文相比IP报文简单些,在LwIP中描述ICMP报文的数据结构如下:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\icmp.h

#define ICMP_ER   0    /* echo reply */
#define ICMP_DUR  3    /* destination unreachable */
#define ICMP_SQ   4    /* source quench */
#define ICMP_RD   5    /* redirect */
#define ICMP_ECHO 8    /* echo */
#define ICMP_TE  11    /* time exceeded */
#define ICMP_PP  12    /* parameter problem */
#define ICMP_TS  13    /* timestamp */
#define ICMP_TSR 14    /* timestamp reply */
#define ICMP_IRQ 15    /* information request */
#define ICMP_IR  16    /* information reply */

enum icmp_dur_type {
  ICMP_DUR_NET   = 0,  /* net unreachable */
  ICMP_DUR_HOST  = 1,  /* host unreachable */
  ICMP_DUR_PROTO = 2,  /* protocol unreachable */
  ICMP_DUR_PORT  = 3,  /* port unreachable */
  ICMP_DUR_FRAG  = 4,  /* fragmentation needed and DF set */
  ICMP_DUR_SR    = 5   /* source route failed */
};

enum icmp_te_type {
  ICMP_TE_TTL  = 0,    /* time to live exceeded in transit */
  ICMP_TE_FRAG = 1     /* fragment reassembly time exceeded */
};

PACK_STRUCT_BEGIN
struct icmp_echo_hdr {
  PACK_STRUCT_FIELD(u8_t type);
  PACK_STRUCT_FIELD(u8_t code);
  PACK_STRUCT_FIELD(u16_t chksum);
  PACK_STRUCT_FIELD(u16_t id);
  PACK_STRUCT_FIELD(u16_t seqno);
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

#define ICMPH_TYPE(hdr) ((hdr)->type)
#define ICMPH_CODE(hdr) ((hdr)->code)

/** Combines type and code to an u16_t */
#define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t))
#define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c))

上面这些宏及数据结构的定义相对简单,前面的宏定义主要定义了ICMP的报文类型,接下来的枚举类型定义了目的不可达和数据报超时的报文代码。后面的结构体定义了ICMP回送报文首部(PACK_STRUCT_FIELD禁止编译器自对齐),这个结构体也可以拿来描述其他类型的首部;最后的宏定义分别用于查询、设置ICMP首部中的部分字段,其中宏变量hdr指向ICMP首部结构体指针。

3.2 ICMPv4数据报操作函数

在数据报不能递交给任何一个上层协议时,函数icmp_dest_unreach会被调用,以发送一个目的不可达ICMP差错报文给源主机,引起目的不可达的原因是协议不可达;在UDP层处理时还将看到如果UDP数据不能被递交给任何一个应用程序,函数icmp_dest_unreach也会被调用,这里引起目的不可达的具体原因是端口不可达。另一种差错报文是超时报文,发送超时报文的函数叫icmp_time_exceeded,在数据报转发和分片重装过程中,都可能调用该函数,引发超时的具体原因可能有两种:一种是数据报TTL为0;另一种是分片重装时间超时。下面来看看两种差错报文具体是怎么被发送的:

// rt-thread\components\net\lwip-1.4.1\src\core\ipv4\icmp.c

/* The amount of data from the original packet to return in a dest-unreachable */
#define ICMP_DEST_UNREACH_DATASIZE 8

/**
 * Send an icmp 'destination unreachable' packet, called from ip_input() if
 * the transport layer protocol is unknown and from udp_input() if the local
 * port is not bound.
 * @param p the input packet for which the 'unreachable' should be sent,
 *          p->payload pointing to the IP header
 * @param t type of the 'unreachable' packet
 */
void icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
{
  icmp_send_response(p, ICMP_DUR, t);
}

#if IP_FORWARD || IP_REASSEMBLY
/**
 * Send a 'time exceeded' packet, called from ip_forward() if TTL is 0.
 * @param p the input packet for which the 'time exceeded' should be sent,
 *          p->payload pointing to the IP header
 * @param t type of the 'time exceeded' packet
 */
void icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
{
  icmp_send_response(p, ICMP_TE, t);
}
#endif /* IP_FORWARD || IP_REASSEMBLY */

/**
 * Send an icmp packet in response to an incoming packet.
 *
 * @param p the input packet for which the 'unreachable' should be sent,
 *          p->payload pointing to the IP header
 * @param type Type of the ICMP header
 * @param code Code of the ICMP header
 */
static void icmp_send_response(struct pbuf *p, u8_t type, u8_t code)
{
  struct pbuf *q;
  struct ip_hdr *iphdr;
  /* we can use the echo header here */
  struct icmp_echo_hdr *icmphdr;
  ip_addr_t iphdr_src;

  /* ICMP header + IP header + 8 bytes of data */
  q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + IP_HLEN + ICMP_DEST_UNREACH_DATASIZE, PBUF_RAM);
  if (q == NULL) {
    return;
  }

  iphdr = (struct ip_hdr *)p->payload;
  icmphdr = (struct icmp_echo_hdr *)q->payload;
  icmphdr->type = type;
  icmphdr->code = code;
  icmphdr->id = 0;
  icmphdr->seqno = 0;

  /* copy fields from original packet */
  SMEMCPY((u8_t *)q->payload + sizeof(struct icmp_echo_hdr), (u8_t *)p->payload,
          IP_HLEN + ICMP_DEST_UNREACH_DATASIZE);

  /* calculate checksum */
  icmphdr->chksum = 0;
  icmphdr->chksum = inet_chksum(icmphdr, q->len);
  
  ip_addr_copy(iphdr_src, iphdr->src);
  ip_output(q, NULL, &iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP);
  pbuf_free(q);
}

这里的重点是icmp_send_response函数,它为报文申请空间,然后根据报文类型和代码字段值填写数据,然后计算校验和,最后通过函数ip_output将数据报发送出去。

IP层收到ICMP报文会调用icmp_input函数处理,该函数根据报文的不同类型做出不同处理。目前LwIP只支持ICMP回送请求报文的处理,而对其他类型的ICMP报文直接丢弃,不做任何响应,这在嵌入式产品中也够用了。对于ICMP回送请求,icmp_input生成回送应答报文并返回源主机的处理过程如下:

// rt-thread\components\net\lwip-1.4.1\src\core\ipv4\icmp.c

/**
 * Processes ICMP input packets, called from ip_input().
 *
 * Currently only processes icmp echo requests and sends
 * out the echo response.
 * @param p the icmp echo request packet, p->payload pointing to the ip header
 * @param inp the netif on which this packet was received
 */
void icmp_input(struct pbuf *p, struct netif *inp)
{
  u8_t type;
  struct icmp_echo_hdr *iecho;
  struct ip_hdr *iphdr;
  s16_t hlen;
  
  iphdr = (struct ip_hdr *)p->payload;
  hlen = IPH_HL(iphdr) * 4;
  if (pbuf_header(p, -hlen) || (p->tot_len < sizeof(u16_t)*2)) {
    goto lenerr;
  }

  type = *((u8_t *)p->payload);
  switch (type) {
  case ICMP_ER:
    /* This is OK, echo reply might have been parsed by a raw PCB
       (as obviously, an echo request has been sent, too). */
    break; 
  case ICMP_ECHO:
    {
      int accepted = 1;
      /* multicast destination address? */
      if (ip_addr_ismulticast(&current_iphdr_dest)) {
        accepted = 0;
      }
      /* broadcast destination address? */
      if (ip_addr_isbroadcast(&current_iphdr_dest, inp)) {
        accepted = 0;
      }
      /* broadcast or multicast destination address not acceptd? */
      if (!accepted) {
        pbuf_free(p);
        return;
      }
    }
    if (p->tot_len < sizeof(struct icmp_echo_hdr)) {
      goto lenerr;
    }
    if (inet_chksum_pbuf(p) != 0) {
      pbuf_free(p);
      return;
    }
    /* At this point, all checks are OK. */
    /* We generate an answer by switching the dest and src ip addresses,
     * setting the icmp type to ECHO_RESPONSE and updating the checksum. */
    iecho = (struct icmp_echo_hdr *)p->payload;
    ip_addr_copy(iphdr->src, *ip_current_dest_addr());
    ip_addr_copy(iphdr->dest, *ip_current_src_addr());
    ICMPH_TYPE_SET(iecho, ICMP_ER);
    
    /* adjust the checksum */
    if (iecho->chksum >= PP_HTONS(0xffffU - (ICMP_ECHO << 8))) {
      iecho->chksum += PP_HTONS(ICMP_ECHO << 8) + 1;
    } else {
      iecho->chksum += PP_HTONS(ICMP_ECHO << 8);
    }
    /* Set the correct TTL and recalculate the header checksum. */
    IPH_TTL_SET(iphdr, ICMP_TTL);
    IPH_CHKSUM_SET(iphdr, 0);
    IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN));

    if(pbuf_header(p, hlen)) {
      LWIP_ASSERT("Can't move over header in packet", 0);
    } else {
      err_t ret;
      /* send an ICMP packet, src addr is the dest addr of the curren packet */
      ret = ip_output_if(p, ip_current_dest_addr(), IP_HDRINCL,
                   ICMP_TTL, 0, IP_PROTO_ICMP, inp);
    }
    break;
  default:
    break;
  }
  pbuf_free(p);
  return;
lenerr:
  pbuf_free(p);
  return;
}

上面的函数为了便于理解,去掉了部分不重要的编译选项,对于传进来的数据报pbuf,首先检查将payload指针调整到ICMP首部并判断首部长度是否不小于4字节,若满足最小长度要求继续处理。接下来根据ICMP首部不同类型做出不同处理,若为回送应答则可直接忽略,对于回送请求报文则需判断目的地址是否合法(目的地址为多播与广播地址的请求报文不做处理),再检查报文长度和校验和是否正确。当所有校验工作成功后,就可以产生一个应答报文,应答报文不需要另开辟新的内存空间,直接重复利用请求报文空间即可,它们二者只有报文类型字段有差异,修改ICMP报文类型字段并重新计算校验和(因只变更一个字段,有简易算式减少计算量),交换目的IP与源IP后就可以将生成的回送应答报文通过ip_output_if函数发送出去了。

3.3 ICMPv6数据报描述与操作

LwIP 1.4.1版本中对IPv6的支持有限,并没有实现邻居发现和多播监听发现功能。在最新版的LwIP 2.1.2版本中增强了对IPv6的支持,实现了ICMPv6的邻居发现和多播监听发现功能。这里主要以LwIP 1.4.1版本为例分析TCP/IP协议栈原理与实现代码,邻居发现与多播监听发现的实现代码在这里就暂略了,报文跟ICMPv4类似,LwIP也只实现了目的不可达、超时、回送请求/应答报文,不同的是ICMPv6三种报文分别用三个数据结构来描述了,代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv6\lwip\icmp.h

#define ICMP6_DUR  1
#define ICMP6_TE   3
#define ICMP6_ECHO 128    /* echo */
#define ICMP6_ER   129      /* echo reply */

enum icmp_dur_type {
  ICMP_DUR_NET = 0,    /* net unreachable */
  ICMP_DUR_HOST = 1,   /* host unreachable */
  ICMP_DUR_PROTO = 2,  /* protocol unreachable */
  ICMP_DUR_PORT = 3,   /* port unreachable */
  ICMP_DUR_FRAG = 4,   /* fragmentation needed and DF set */
  ICMP_DUR_SR = 5      /* source route failed */
};

enum icmp_te_type {
  ICMP_TE_TTL = 0,     /* time to live exceeded in transit */
  ICMP_TE_FRAG = 1     /* fragment reassembly time exceeded */
};

struct icmp_echo_hdr {
  u8_t type;
  u8_t icode;
  u16_t chksum;
  u16_t id;
  u16_t seqno;
};

struct icmp_dur_hdr {
  u8_t type;
  u8_t icode;
  u16_t chksum;
  u32_t unused;
};

struct icmp_te_hdr {
  u8_t type;
  u8_t icode;
  u16_t chksum;
  u32_t unused;
};

该版本协议栈在ICMPv6中实现的三种报文操作函数代码如下:

// rt-thread\components\net\lwip-1.4.1\src\core\ipv6\icmp6.c

#define SMEMCPY(dst,src,len)            memcpy(dst,src,len)

void icmp_input(struct pbuf *p, struct netif *inp)
{
  u8_t type;
  struct icmp_echo_hdr *iecho;
  struct ip_hdr *iphdr;
  struct ip_addr tmpaddr;

  /* TODO: check length before accessing payload! */
  type = ((u8_t *)p->payload)[0];

  switch (type) {
  case ICMP6_ECHO:
    if (p->tot_len < sizeof(struct icmp_echo_hdr)) {
      pbuf_free(p);
      return;
    }
    iecho = p->payload;
    iphdr = (struct ip_hdr *)((u8_t *)p->payload - IP_HLEN);
    ip_addr_set(&tmpaddr, &(iphdr->src));
    ip_addr_set(&(iphdr->src), &(iphdr->dest));
    ip_addr_set(&(iphdr->dest), &tmpaddr);
    iecho->type = ICMP6_ER;
    /* adjust the checksum */
    if (iecho->chksum >= htons(0xffff - (ICMP6_ECHO << 8))) {
      iecho->chksum += htons(ICMP6_ECHO << 8) + 1;
    } else {
      iecho->chksum += htons(ICMP6_ECHO << 8);
    }
    ip_output_if (p, &(iphdr->src), IP_HDRINCL, iphdr->hoplim, IP_PROTO_ICMP, inp);
    break;
  default:
    break;
  }
  pbuf_free(p);
}

void icmp_dest_unreach(struct pbuf *p, enum icmp_dur_type t)
{
  struct pbuf *q;
  struct ip_hdr *iphdr;
  struct icmp_dur_hdr *idur;

  /* @todo: can this be PBUF_LINK instead of PBUF_IP? */
  q = pbuf_alloc(PBUF_IP, 8 + IP_HLEN + 8, PBUF_RAM);
  /* ICMP header + IP header + 8 bytes of data */
  if (q == NULL) {
    pbuf_free(p);
    return;
  }
  iphdr = p->payload;
  idur = q->payload;
  idur->type = (u8_t)ICMP6_DUR;
  idur->icode = (u8_t)t;
  
  SMEMCPY((u8_t *)q->payload + 8, p->payload, IP_HLEN + 8);

  /* calculate checksum */
  idur->chksum = 0;
  idur->chksum = inet_chksum(idur, q->len);

  ip_output(q, NULL, (struct ip_addr *)&(iphdr->src), ICMP_TTL, IP_PROTO_ICMP);
  pbuf_free(q);
}

void icmp_time_exceeded(struct pbuf *p, enum icmp_te_type t)
{
  struct pbuf *q;
  struct ip_hdr *iphdr;
  struct icmp_te_hdr *tehdr;

  /* @todo: can this be PBUF_LINK instead of PBUF_IP? */
  q = pbuf_alloc(PBUF_IP, 8 + IP_HLEN + 8, PBUF_RAM);
  /* ICMP header + IP header + 8 bytes of data */
  if (q == NULL) {
    pbuf_free(p);
    return;
  }
  iphdr = p->payload;
  tehdr = q->payload;
  tehdr->type = (u8_t)ICMP6_TE;
  tehdr->icode = (u8_t)t;

  /* copy fields from original packet */
  SMEMCPY((u8_t *)q->payload + 8, (u8_t *)p->payload, IP_HLEN + 8);

  /* calculate checksum */
  tehdr->chksum = 0;
  tehdr->chksum = inet_chksum(tehdr, q->len);
  
  ip_output(q, NULL, (struct ip_addr *)&(iphdr->src), ICMP_TTL, IP_PROTO_ICMP);
  pbuf_free(q);
}

3.4 如何发送ping命令

前面介绍了如何发送ICMP目的不可达报文与超时报文,也介绍了对于接收到的ICMP回送请求报文如何回送应答报文,但ICMP回送请求报文是如何发送的呢?前面介绍的ping命令是根据ICMP回送请求/应答报文实现的,下面就以ping命令的实现过程为例,介绍ICMP回送请求报文如何发送。

在前篇介绍IP协议时,IP层输入函数ip_input对于每个输入的数据包都会调用raw_input进行处理,这是IP层为应用程序直接获取IP数据包提供的一种机制,Socket编程中将这种机制称为原始套接字,而在LwIP内核中,我们可以把它称为原始协议控制块raw_pcb,对raw_pcb的描述如下:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\ip.h

/* This is the common part of all PCB types. It needs to be at the
   beginning of a PCB type definition. It is located here so that
   changes to this common part are made in one location instead of
   having to change all PCB structs. */
#define IP_PCB \
  /* ip addresses in network byte order */ \
  ip_addr_t local_ip; \
  ip_addr_t remote_ip; \
   /* Socket options */  \
  u8_t so_options;      \
   /* Type Of Service */ \
  u8_t tos;              \
  /* Time To Live */     \
  u8_t ttl

struct ip_pcb {
/* Common members of all PCB types */
  IP_PCB;
};

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\raw.h

/** Function prototype for raw pcb receive callback functions.
 * @param arg user supplied argument (raw_pcb.recv_arg)
 * @param pcb the raw_pcb which received data
 * @param p the packet buffer that was received
 * @param addr the remote IP address from which the packet was received
 * @return 1 if the packet was 'eaten' (aka. deleted),
 *         0 if the packet lives on
 * If returning 1, the callback is responsible for freeing the pbuf
 * if it's not used any more.
 */
typedef u8_t (*raw_recv_fn)(void *arg, struct raw_pcb *pcb, struct pbuf *p,
    ip_addr_t *addr);

struct raw_pcb {
  /* Common members of all PCB types */
  IP_PCB;

  struct raw_pcb *next;

  u8_t protocol;

  /** receive callback function */
  raw_recv_fn recv;
  /* user-supplied argument for the recv callback */
  void *recv_arg;
};

原始协议控制块raw_pcb如上所示,每一个控制块raw_pcb可定制一个特定协议类型的IP数据包,如ICMP包、TCP包、UDP包等。当IP层收到一个数据包后,如果该包首部中的IP地址和协议字段与某个raw_pcb吻合,则数据包会被递交给这个raw_pcb处理。raw_pcb对数据包的处理过程很简单,直接调用raw_pcb上注册的recv回调函数,用户根据自己的需要编写这个回调函数,从而完成对该IP包的特定处理。内核中可能同时存在多个raw_pcb,它们各自定制不同连接上的不同协议包,因此内核利用next字段将所有raw_pcb组织在一个名为raw_pcbs的链表上,方便对各个原始协议控制块进行遍历操作。下面看看前篇提到的ip_input函数内调用的raw_input函数是如何工作的:

// rt-thread\components\net\lwip-1.4.1\src\core\raw.c

/**
 * Determine if in incoming IP packet is covered by a RAW PCB
 * and if so, pass it to a user-provided receive callback function.
 * Given an incoming IP datagram (as a chain of pbufs) this function
 * finds a corresponding RAW PCB and calls the corresponding receive
 * callback function.
 * @param p pbuf to be demultiplexed to a RAW PCB.
 * @param inp network interface on which the datagram was received.
 * @return - 1 if the packet has been eaten by a RAW PCB receive
 *           callback function. The caller MAY NOT not reference the
 *           packet any longer, and MAY NOT call pbuf_free().
 * @return - 0 if packet is not eaten (pbuf is still referenced by the
 *           caller).
 */
u8_t raw_input(struct pbuf *p, struct netif *inp)
{
  struct raw_pcb *pcb, *prev;
  struct ip_hdr *iphdr;
  s16_t proto;
  u8_t eaten = 0;
  
  iphdr = (struct ip_hdr *)p->payload;
  proto = IPH_PROTO(iphdr);

  prev = NULL;
  pcb = raw_pcbs;
  /* loop through all raw pcbs until the packet is eaten by one */
  /* this allows multiple pcbs to match against the packet by design */
  while ((eaten == 0) && (pcb != NULL)) {
    if ((pcb->protocol == proto) &&
        (ip_addr_isany(&pcb->local_ip) ||
         ip_addr_cmp(&(pcb->local_ip), &current_iphdr_dest))) 
      {
        /* receive callback function available? */
        if (pcb->recv != NULL) {
          /* the receive callback function did not eat the packet? */
          if (pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr()) != 0) {
            /* receive function ate the packet */
            p = NULL;
            eaten = 1;
            if (prev != NULL) {
            /* move the pcb to the front of raw_pcbs so that is
               found faster next time */
              prev->next = pcb->next;
              pcb->next = raw_pcbs;
              raw_pcbs = pcb;
            }
          }
        }
        /* no receive callback function was set for this raw PCB */
      }
    /* drop the packet */
    prev = pcb;
    pcb = pcb->next;
  }
  return eaten;
}

可见,raw_input的处理过程就是一个循环查找的过程,它为IP数据包查找一个协议字段和IP地址都吻合的原始协议控制块,并调用该控制块注册的回调函数recv处理数据包。其余的raw_pcb的操作函数见下表:

raw_pcb操作函数函数功能描述
struct raw_pcb * raw_new(u8_t proto)创建一个raw_pcb并插入raw_pcbs链表首部,
以proto作为协议类型初始化该控制块;
void raw_remove(struct raw_pcb *pcb)从raw_pcbs链表中移除某raw_pcb并释放其
内存空间;
err_t raw_bind(struct raw_pcb *pcb,
ip_addr_t *ipaddr)
将本地IP地址绑定到raw_pcb上(设置IP_PCB
中的local_ip);
err_t raw_connect(struct raw_pcb *pcb,
ip_addr_t *ipaddr)
将对端IP地址绑定到raw_pcb上(设置IP_PCB
中的remote_ip);
void raw_recv(struct raw_pcb *pcb,
raw_recv_fn recv, void *recv_arg)
向raw_pcb中注册回调函数recv及其参数recv_arg;
err_t raw_sendto(struct raw_pcb *pcb,
struct pbuf *p, ip_addr_t *ipaddr)
将一个raw IP数据包发送到目的IP地址对应的主机,
raw IP数据包首部字段值由raw_pcb提供;
err_t raw_send(struct raw_pcb *pcb,
struct pbuf *p)
实际调用raw_sendto;

利用raw_pcb的结构体及其操作函数,我们可以注册一个ICMP协议的原始协议控制块,用来接收IP层的ping响应包,同时利用内核的定时机制,周期性地往对端IP地址构造并发送ping请求包,而在原始协议控制块的recv回调函数中接收并处理ping响应。按照上述原理发送ping请求的实现代码如下:

#define PING_DELAY     1000
#define PING_ID        0xAFAF
#define PING_DATA_SIZE 32
/* ping variables */
static u16_t ping_seq_num;
static u32_t ping_time;
static struct raw_pcb *ping_pcb = NULL;
static ip_addr_t ping_dst;

/** Prepare a echo ICMP request */
static void ping_prepare_echo( struct icmp_echo_hdr *iecho, u16_t len)
{
  size_t i;
  size_t data_len = len - sizeof(struct icmp_echo_hdr);

  ICMPH_TYPE_SET(iecho, ICMP_ECHO);
  ICMPH_CODE_SET(iecho, 0);
  iecho->chksum = 0;
  iecho->id     = PING_ID;
  iecho->seqno  = htons(++ping_seq_num);

  /* fill the additional data buffer with some data */
  for(i = 0; i < data_len; i++) {
    ((char*)iecho)[sizeof(struct icmp_echo_hdr) + i] = (char)i;
  }
  iecho->chksum = inet_chksum(iecho, len);
}

/* Ping using the raw ip */
static u8_t ping_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *addr)
{
  struct icmp_echo_hdr *iecho;
  //we can also check src ip here, but just egnore it
  if ((p->tot_len >= (PBUF_IP_HLEN + sizeof(struct icmp_echo_hdr))))
  {
      iecho = (struct icmp_echo_hdr *)((u8_t*)p->payload + PBUF_IP_HLEN);
      if ((iecho->type == ICMP_ER) && (iecho->id == PING_ID) && (iecho->seqno == htons(ping_seq_num))) {  
          LWIP_DEBUGF( PING_DEBUG, ("ping: recv "));
          ip_addr_debug_print(PING_DEBUG, addr);
          LWIP_DEBUGF( PING_DEBUG, (" time=%"U32_F" ms\n", (sys_now()-ping_time)));

          pbuf_free(p);
          return 1; /* eat the packet */
      }
  }
  return 0; /* don't eat the packet */
}

static void ping_send(struct raw_pcb *raw, ip_addr_t *addr)
{
  struct pbuf *p;
  struct icmp_echo_hdr *iecho;
  size_t ping_size = sizeof(struct icmp_echo_hdr) + PING_DATA_SIZE;

  p = pbuf_alloc(PBUF_IP, (u16_t)ping_size, PBUF_RAM);
  if (!p) {
    return;
  }
  if ((p->len == p->tot_len) && (p->next == NULL)) {
    iecho = (struct icmp_echo_hdr *)p->payload;
    ping_prepare_echo(iecho, (u16_t)ping_size);
    raw_sendto(raw, p, addr);
    ping_time = sys_now();

	LWIP_DEBUGF(PING_DEBUG, ("ping:[%"U32_F"] send ", ping_seq_num));
	ip_addr_debug_print(PING_DEBUG, addr);
    LWIP_DEBUGF( PING_DEBUG, ("\n"));
  }
  pbuf_free(p);
}

static void ping_timeout(void *arg)
{
  struct raw_pcb *pcb = (struct raw_pcb*)arg;
  ping_send(pcb, &ping_dst);
  sys_timeout(PING_DELAY, ping_timeout, pcb);
}

static void ping_raw_init(void)
{
  ping_pcb = raw_new(IP_PROTO_ICMP);
  raw_recv(ping_pcb, ping_recv, NULL);
  raw_bind(ping_pcb, IP_ADDR_ANY);
  sys_timeout(PING_DELAY, ping_timeout, ping_pcb);
}

void ping_init(void)
{
  IP4_ADDR(&ping_dst, 192,168,1,103);
  ping_raw_init();
}

上面的代码主要功能有两个:一是通过注册到新建raw_pcb上的ping_recv函数,当接收到IP层的ping回送请求包时,通过串口打印ping响应信息;二是通过周期性函数ping_timeout不断调用ping_send向对端IP地址发送ping请求包(由函数ping_prepare_echo构造),并通过串口打印ping请求包发送信息。

在contrib-1.4.1中有针对lwip-1.4.1版本协议栈的ping命令实现,该实现方式没有使用raw接口,而是采用更上层的socket接口实现,其实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\apps\ping\ping.c

/* using the lwIP custom ping */
rt_err_t ping(char* target_name, rt_uint32_t times, rt_size_t size)
{
#if LWIP_VERSION_MAJOR >= 2U
    struct timeval timeout = { PING_RCV_TIMEO / RT_TICK_PER_SECOND, PING_RCV_TIMEO % RT_TICK_PER_SECOND };
#else
    int timeout = PING_RCV_TIMEO * 1000UL / RT_TICK_PER_SECOND;
#endif

    int s, ttl, recv_len;
    ip_addr_t target_addr;
    rt_uint32_t send_times;
    rt_tick_t recv_start_tick;
    struct addrinfo hint, *res = NULL;
    struct sockaddr_in *h = NULL;
    struct in_addr ina;

    send_times = 0;
    ping_seq_num = 0;

    if (size == 0)
    {
        size = PING_DATA_SIZE;
    }

    memset(&hint, 0, sizeof(hint));
    /* convert URL to IP */
    if (lwip_getaddrinfo(target_name, NULL, &hint, &res) != 0)
    {
        rt_kprintf("ping: unknown host %s\n", target_name);
        return -RT_ERROR;
    }
    memcpy(&h, &res->ai_addr, sizeof(struct sockaddr_in *));
    memcpy(&ina, &h->sin_addr, sizeof(ina));
    lwip_freeaddrinfo(res);
    if (inet_aton(inet_ntoa(ina), &target_addr) == 0)
    {
        rt_kprintf("ping: unknown host %s\n", target_name);
        return -RT_ERROR;
    }
    /* new a socket */
    if ((s = lwip_socket(AF_INET, SOCK_RAW, IP_PROTO_ICMP)) < 0)
    {
        rt_kprintf("ping: create socket failed\n");
        return -RT_ERROR;
    }

    lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

    while (1)
    {
        int elapsed_time;

        if (lwip_ping_send(s, &target_addr, size) == ERR_OK)
        {
            recv_start_tick = rt_tick_get();
            if ((recv_len = lwip_ping_recv(s, &ttl)) >= 0)
            {
                elapsed_time = (rt_tick_get() - recv_start_tick) * 1000UL / RT_TICK_PER_SECOND;
                rt_kprintf("%d bytes from %s icmp_seq=%d ttl=%d time=%d ms\n", recv_len, inet_ntoa(ina), send_times,
                        ttl, elapsed_time);
            }
            else
            {
                rt_kprintf("From %s icmp_seq=%d timeout\n", inet_ntoa(ina), send_times);
            }
        }
        else
        {
            rt_kprintf("Send %s - error\n", inet_ntoa(ina));
        }

        send_times++;
        if (send_times >= times)
        {
            /* send ping times reached, stop */
            break;
        }

        rt_thread_delay(PING_DELAY); /* take a delay */
    }

    lwip_close(s);

    return RT_EOK;
}

/* Ping using the socket ip */
err_t lwip_ping_send(int s, ip_addr_t *addr, int size)
{
    int err;
    struct icmp_echo_hdr *iecho;
    struct sockaddr_in to;
    int ping_size = sizeof(struct icmp_echo_hdr) + size;
    LWIP_ASSERT("ping_size is too big", ping_size <= 0xffff);

    iecho = rt_malloc(ping_size);
    if (iecho == RT_NULL)
    {
        return ERR_MEM;
    }

    ping_prepare_echo(iecho, (u16_t) ping_size);

    to.sin_len = sizeof(to);
    to.sin_family = AF_INET;
#if LWIP_IPV4 && LWIP_IPV6
    to.sin_addr.s_addr = addr->u_addr.ip4.addr;
#elif LWIP_IPV4
    to.sin_addr.s_addr = addr->addr;
#elif LWIP_IPV6
#error Not supported IPv6.
#endif

    err = lwip_sendto(s, iecho, ping_size, 0, (struct sockaddr*) &to, sizeof(to));
    rt_free(iecho);

    return (err == ping_size ? ERR_OK : ERR_VAL);
}

int lwip_ping_recv(int s, int *ttl)
{
    char buf[64];
    int fromlen = sizeof(struct sockaddr_in), len;
    struct sockaddr_in from;
    struct ip_hdr *iphdr;
    struct icmp_echo_hdr *iecho;

    while ((len = lwip_recvfrom(s, buf, sizeof(buf), 0, (struct sockaddr*) &from, (socklen_t*) &fromlen)) > 0)
    {
        if (len >= (int)(sizeof(struct ip_hdr) + sizeof(struct icmp_echo_hdr)))
        {
            iphdr = (struct ip_hdr *) buf;
            iecho = (struct icmp_echo_hdr *) (buf + (IPH_HL(iphdr) * 4));
            if ((iecho->id == PING_ID) && (iecho->seqno == htons(ping_seq_num)))
            {
                *ttl = iphdr->_ttl;
                return len;
            }
        }
    }

    return len;
}

/** Prepare a echo ICMP request */
static void ping_prepare_echo( struct icmp_echo_hdr *iecho, u16_t len)
{
    size_t i;
    size_t data_len = len - sizeof(struct icmp_echo_hdr);

    ICMPH_TYPE_SET(iecho, ICMP_ECHO);
    ICMPH_CODE_SET(iecho, 0);
    iecho->chksum = 0;
    iecho->id     = PING_ID;
    iecho->seqno  = htons(++ping_seq_num);

    /* fill the additional data buffer with some data */
    for (i = 0; i < data_len; i++)
    {
        ((char*) iecho)[sizeof(struct icmp_echo_hdr) + i] = (char) i;
    }

#ifdef RT_LWIP_USING_HW_CHECKSUM
      iecho->chksum = 0;
#else
      iecho->chksum = inet_chksum(iecho, len);
#endif

}

3.5 ICMP洪水攻击

如果在执行ping命令时,加大ping数据包的大小(以太网MTU即1500 - IP首部大小20 - ICMP首部大小8 = ping数据包大小1472字节),我们的板子就ping不通了,如下图所示:
ping数据包过大
由于在协议栈内部为数据报接收而预留的空间并不能放下如此大的一个ICMP数据报文,或者如此大的一个ICMP报文在分片重装过程中超出了重装条件的限制(如LwIP中的pbuf使用个数限制),协议栈会直接将报文丢弃,不做任何回应,将导致主机一直接收不到ICMP回送应答,这就是ICMP洪水攻击的雏形。

ICMP洪水攻击的原理可以看成是网络黑客利用其能控制的多台中间人计算机(傀儡主机,也称之为肉鸡)一起向目标主机发送大量看似合法的ICMP回送请求数据包,造成目标主机网络阻塞或服务器资源耗尽而导致拒绝服务产生,无法对正常用户提供网络服务。另一方面,合法的网络数据报被虚假的数据报淹没而无法在网络中被转发,合法的用户不能正常使用网络。

对于ICMP洪水攻击,可以采取两种方法进行防范:第一种方法是在路由器上对ICMP数据包进行带宽限制,将ICMP占用的带宽控制在一定范围内,这样即使有ICMP攻击,它所占用的带宽也是非常有限的,对整个网络的影响将会非常小;第二种方法是在主机上设置ICMP数据包的处理规则,如果允许,可以拒绝向所有的ICMP数据包服务。

更多文章

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页