上一篇:DIY TCP/IP IP模块和ICMP模块的实现1 本节在8.2节的基础上扩展icmp_recv函数,检验接收到的ICMP数据帧的校验和,解析ICMP数据帧头部的type字段,根据ICMP数据帧的类型做相应处理。
int icmp_recv(unsigned char *pkt, unsigned int sz) { int ret = 0; iphdr_t *ippkt = NULL; icmphdr_t *icmppkt = NULL; unsigned short icmppkt_len = 0; unsigned char icmp_type = 0; unsigned char *payload = NULL; if (pkt == NULL || sz == 0) { ret = -1; goto out; } ippkt = (iphdr_t *)pkt; icmppkt_len = NTOHS(ippkt->total_len) - sizeof(iphdr_t); icmppkt = (icmphdr_t *)strip_header(ippkt, sizeof(iphdr_t)); if (cksum(0, icmppkt, icmppkt_len, BENDIAN)) { log_printf(ERROR, "Invalid ICMP checksum\n"); ret = -1; goto out; } icmp_type = icmppkt->type; log_printf(INFO, "Echo (ping) %s, id: %u, seq: %u/%u, ttl=%u\n", icmp_type == ICMP_TYPE_ECHO ? "request" : "reply", NTOHS(icmppkt->id), NTOHS(icmppkt->seq), icmppkt->seq, ippkt->ttl); dump_buf(icmppkt, icmppkt_len); switch(icmp_type) { case ICMP_TYPE_ECHO: ret = process_icmp_echo(ippkt, icmppkt); break; case ICMP_TYPE_ECHO_REPLY: ret = process_icmp_echo_reply(); break; default: log_printf(WARNING, "Unhandled ICMP PKT, type: %u\n", icmp_type); ret = -1; goto out; } out: return ret; }Line 1-13: icmp_recv函数的参数pkt指向IP数据帧的首字节地址,sz是IP数据帧的长度,包括IP头部和IP Payload。pkt的类型是void *,没有定义成iphdr_t *类型,因为icmp_recv函数是ICMP模块暴露给其他模块使用的函数接口,icmp_recv函数声明在icmp.h头文件中,如果定义icmp_recv的入参pkt为iphdr_t *类型,就需要在icmp.h头文件中引入ip.h头文件,造成ICMP模块和IP模块头文件的循环引用。读者还会在其他模块的实现中看到类似的void *类型的函数入参的定义,都是为了避免头文件的循环引用和增加模块之间的独立性。 Line 14-17: 判断入参pkt不为空且sz不为0时继续执行,将pkt强制转换为iphdr_t *类型,根据IP首部的total_len字段计算ICMP数据帧的长度,strip_header将IP数据帧的头部剥去后赋值给icmppkt,strip_header之前已经介绍过,将ippkt指针转换为unsigned char *类型,再加上sizeof(iphdr_t)。 Line 18-22: 检验ICMP数据帧的校验和,8.1节中介绍的ICMP的头部数据和ICMP的payload数据都要参与计算校验和,icmppkt指向的ICMP数据帧的头部中包含发送方的校验和数据,所以此处检验校验和的结果应当为0,否则认为接收到的ICMP数据帧不合法。 Line 23-44: 校验和检验通过后,打印输出接收的到ICMP数据帧的头部信息,dump_buf做为debug使用,输出Echo (Ping) Request数据帧的内容。根据ICMP头部中的type判断,接收到的ICMP数据帧是Echo Ping Rquest或者是Echo Ping Reply,再交给process_icmp_echo和process_icmp_echo_reply函数处理。 8.4 ICMP数据帧的发送 本节在8.3节的基础上扩展process_icmp_echo函数,构造ICMP Echo (Ping) Reply数据帧,复制ICMP Echo Ping Request数据帧的payload数据,计算ICMP数据帧的校验和,将构造好的ICMP Echo Ping Reply数据帧交给IP模块发送。在介绍代码实现之前,先来通过wireshark抓取PING的数据帧交互过程,查看如何构造ICMP Echo (Ping) Reply数据帧。 上图是本人所在局域网中的一台windows主机PING路由器的交互过程,192.168.0.105发出Echo (ping) Request,路由器192.168.0.1回复Echo (ping) Reply。 Echo (ping) Requset数据帧 Echo (Ping) Request数据帧头部结构已经在8.1节详细介绍过,data部分含有32个字节的数据,从wireshark的raw byte数据显示可以看出,32个字节的data是ascii码小写的a到w,再重复小写的a到i。 Echo (ping) Reply数据帧 Echo (ping) Reply的identifier,sequence number与Echo (ping) Request相等,在8.1节已经介绍过,RFC792 ICMP协议中有说明,ICMP头部code为0时,Echo ping Request和Echo Ping Reply的identifier,sequence number字段分别相等。表示Echo Ping Reply回复对应的Echo ping request数据帧。再来看数据部分,32个字节的数据也是相等的,与ICMP Echo数据帧的名字相呼应,接收到Echo (Ping) Request数据帧时,将数据原封不动的Echo回去。Echo (Ping) Reply除了type和校验和部分与Echo (Ping) Request不相等之外,其余部分均相等。弄清楚Echo (Ping) Reply数据帧的回复规则后,再来看process_icmp_echo的代码实现。
static int process_icmp_echo(iphdr_t *ippkt, icmphdr_t *echo_req) { int ret = 0; unsigned short data_len = 0; unsigned char *echo_req_data = NULL; icmphdr_t *echo_reply = NULL; pdbuf_t *pdbuf = NULL; if (ippkt == NULL || echo_req == NULL) { ret = -1; goto out; } echo_req_data = strip_header(echo_req, sizeof(icmphdr_t)); data_len = NTOHS(ippkt->total_len) - sizeof(iphdr_t) - sizeof(icmphdr_t); pdbuf = pdbuf_alloc(data_len, !IGNORE_MTU); if (pdbuf == NULL) { ret = -1; goto out; } /* build icmp reply */ pdbuf_push(pdbuf, data_len); memcpy(pdbuf->payload, echo_req_data, data_len); pdbuf_push(pdbuf, sizeof(icmphdr_t)); echo_reply = (icmphdr_t *)pdbuf->payload; echo_reply->type = ICMP_TYPE_ECHO_REPLY; echo_reply->code = ICMP_CODE; echo_reply->cksum = 0; echo_reply->id = echo_req->id; echo_reply->seq = HSTON(icmp_seq ++); echo_reply->cksum = cksum(0, echo_reply, data_len + sizeof(icmphdr_t), BENDIAN); echo_reply->cksum = HSTON(echo_reply->cksum); dump_buf(echo_reply, data_len + sizeof(icmphdr_t)); out: return ret; } static int process_icmp_echo_reply() { int ret = 0; return ret; }process_icmp_echo函数的入参是ippkt指针和icmppkt指针,分别指向IP数据帧的首字节地址,和ICMP数据帧的首字节地址。之所以需要IP数据帧,是因为ICMP Echo (Ping) Reply数据帧构造完成之后,需要将其交给IP模块的发送函数处理,发送的目标IP地址就是ICMP Request数据帧的IP头部中的源IP地址。 Line 1-16: 判断ippkt和icmppkt指针都不为空时继续执行,虽然icmp_recv到process_ecmp_echo的调用,ippkt和icmppkt指针必然不为空,本人编写C语言习惯先检查指针的合法性,再使用指针。调用strip_header将ICMP数据帧的头部剥去,返回的指针赋值给echo_req_data,echo_req_data指向ICMP Echo (Ping) Request的数据部分的首字节地址。通过IP头部的total_len字段减去IP头部长度和ICMP头部长度,得到Echo (ping) Request数据部分的长度。 Line 17-21: pdbuf_alloc申请长度为data_len的buffer,6.4节已经介绍过pdbuf_alloc申请的内存空间包括data_len,Layer3和Layer4最大的协议头部,以太网头部和pdbuf_t描述符本身占用的全部内存空间。pdbuf_alloc的第二个参数是!IGNORE_MTU,表示最大申请1500字节的内存空间。介绍IP分片的重组时,会再次改写该函数,重组后的IP分片,封装的ICMP数据帧的长度会超过1500字节。 Line 22-26: pdbuf_push调整pdbuf->payload向地址减小的方向移动data_len个字节,首先将Echo Request的数据部分复制到pdbuf->payload指向的内存空间。再将pdbuf->payload向地址减小的方向移动sizeof(icmphdr_t)个字节,用于存放ICMP的头部数据。将pdbuf->payload指针强制转换为icmphdr_t *类型,用于构造ICMP头部数据。 Line 27-38: type为0x0,code为0x0,cksum字段先赋值为0,最后计算校验和的数值。Identifier和sequence number都来自Echo (ping) Request数据帧,已经是大端格式,所以不需要再次转换为大端。计算校验和,参与计算的数据包括Echo (ping) Reply的头部数据和Payload数据。校验和计算完成后再转换为大端格式填入Echo (ping) Reply数据帧的头部cksum字段中。dump_buf做为debug使用,在将Echo (ping) Reply数据帧交给IP模块发送之前,先通过dump_buf查看构造的数据帧是否符合预期。 与8.2的测试方法一致,运行DIY TCP/IP的主机记为A,与主机A在同一个局域网的主机B上Ping主机A,8.3节在完成ICMP数据帧的校验和检查后,dump_buf打印Echo Ping Request数据帧的内容,本节在ICMP数据帧的校验和计算完成后,dump_buf打印回复构造的Echo Ping Reply数据帧的内容。 主机B PING 192.168.0.7,该IP地址是DIY TCP/IP的虚拟IP地址(局域网中不存在),本节只是构造了ICMP Echo Ping Reply,并没有将数据帧交给IP模块发送,所以主机B上PING的结果仍然是失败的。 再来看DIY TCP/IP的运行结果
gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch5/2$ sudo ./tcp_ip_stack -i 192.168.0.7 [sudo] password for gannicus: Network device init filter: ether proto 0x0800 or ether proto 0x0806 Network device RX init Network device TX init Net device ip address: 192.168.0.7 192.168.0.7 is at 00:0c:29:2e:0a:ed ARP Table IP Address MAC Address 192.168.0.105 8c:a9:82:11:d1:de Echo (ping) request, id: 1, seq: 16/4096, ttl=64 08 00 4d 4b 00 01 00 10 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 61 62 63 64 65 66 67 68 69 Echo (ping) reply, id: 1, seq: 16/4096, ttl=64 00 00 55 4b 00 01 00 10 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 61 62 63 64 65 66 67 68 69 Echo (ping) request, id: 1, seq: 17/4352, ttl=64 …从运行结果可以看出,ICMP模块接收到了Echo (PIing) Request,并且校验和检验成功,Echo (ping) request的type为0x08,0x0为code,4d 4b 为大端格式的校验和,00 01是大端格式的identification,00 10也是大端格式的sequence number,从61向后,一直到第40个字节是Echo (ping) Request的数据内容。 再来看本节构造的Echo (Ping) Reply,type为0x00,0x0为code,55 4b是大端格式的校验和,identification和sequence number与Echo (Ping) Request相等,后面的数据部分也相等,运行结果符合预期。 下一篇:DIY TCP/IP IP模块和ICMP模块的实现3
