上一篇:DIY TCP/IP ARP模块0
7.2 接收ARP Request数据帧
本节实现ARP模块的接收函数arp_recv,完成对接收到的ARP数据帧的解析。修改DIY TCP/IP网络设备模块接收线程的处理函数,根据以太网头部类型判断,如果是0x0806,则调用arp_recv函数处理,在arp_recv函数中完成对ARP帧的解析。新增文件arp.c用于实现ARP模块的处理逻辑,ARP模块相关的数据结构定义在arp.h头文件中,该头文件在6.1节已经引入,7.1已经节详细介绍一下ARP数据帧各个字段的含义。
#ifndef _ARP_H_
#define _ARP_H_
#define ARP_HW_TYPE 1 /* Ethernet */
#define ARP_PROTO_TYPE 0x0800 /* IPv4 */
#define ARP_HW_SZ 6 /* Hardware Address Length */
#define ARP_PROTO_SZ 4 /* Protocol Address Length */
/* ARP operation code */
enum {
ARP_REQUEST = 1,
ARP_REPLY = 2,
} arp_op_code;
/* ARP Packet */
typedef struct _arppkt {
unsigned short hw_type;
unsigned short proto_type;
unsigned char hw_sz;
unsigned char proto_sz;
unsigned short op_code;
unsigned char sender_mac[ARP_HW_SZ];
unsigned char sender_ip[ARP_PROTO_SZ];
unsigned char target_mac[ARP_HW_SZ];
unsigned char target_ip[ARP_PROTO_SZ];
} __attribute__ ((packed)) arppkt_t;
int arp_recv(unsigned char *payload, unsigned int sz);
#endif
Line 4-7: 将ARP帧结构中的物理层类型,协议类型,硬件地址长度和协议地址长度定义为宏,便于使用。
Line 10-13: 定义枚举类型arp_op_code,描述ARP帧的类型,用于解析和构建ARP帧。
Line 16-27: 定义ARP帧数据结构,该数据结构根据7.1.1节中介绍的ARP帧结构定义。
Line 28: 声明arp_recv函数,参数为payload,和payload的长度,该函数被网络设备模块的接收线程调用。在介绍arp_recv函数的实现之前,先修改网络设备模块接收线程处理函数。
网络设备模块的接收线程从接收队列中取出数据帧后,调用dev_process_rxpkt处理。5.3.4节的实现仅仅是打印以太网头部信息,本节修改dev_process_rxpkt函数,剥去以太网头部,然后根据以太网头部类型交给ARP模块处理。dev_process_rxpkt是DIT TCP/IP的网络设备模块与上层模块的接口函数,随着DIY TCP/IP后续上层模块的实现,将不断完善该函数。
static void dev_process_rxpkt(net_device_t *ndev, dev_rxpkt_t *rxpkt)
{
ethhdr_t *ethpkt = NULL;
unsigned short ethtype = 0;
void *uplayer_pkt = NULL;
if (ndev == NULL || rxpkt == NULL)
return;
ethpkt = (ethhdr_t *)rxpkt->payload;
log_printf(VERBOSE, "dev rx, ethernet type: %04x, "MACSTR " --> " MACSTR"\n",
NTOHS(ethpkt->type), MAC2STR(ethpkt->src), MAC2STR(ethpkt->dst));
ethtype = NTOHS(ethpkt->type);
switch (ethtype) {
case ETHERNET_IP:
break;
case ETHERNET_ARP:
uplayer_pkt = strip_header(rxpkt->payload, sizeof(ethhdr_t));
arp_recv(uplayer_pkt, (rxpkt->len - sizeof(ethhdr_t)));
break;
default:
break;
}
free(rxpkt);
}
Line 1-12: 新增ethtype和uplayer_pkt变量,ethtype存放以太网头部类型,uplayer_pkt指针指向网络设备层数据帧剥去以太网头部后的有效载荷。
Line 13-22: 根据以太网头部类型判断,如果是IP数据帧,IP模块尚未实现暂不处理。如果是ARP数据帧,先调用strip_header得到以太网头部后面有效载荷的指针uplayer_pkt,再将uplayer_pkt和除去以太网头部后的数据帧长度交给arp_recv处理。
Line 23: 释放网络设备模块的数据帧应由DIY TCP/IP的上层模块处理。目前DIY TCP/IP的实现,网络设备模块与上层模块的接口均为同步调用。以本节的arp_recv为例,arp_recv返回时,ARP模块的处理已经完成,所以可以由网络设备模块释放内存。这种实现是为了简化DIY TCP/IP对数据帧的释放,集中放在网络设备模块,确保每个网络设备模块的数据帧,在交由上层模块接口处理完成后,均被释放,避免分散在各个模块释放,造成内存泄漏。
strip_header是common.h头文件中的新增函数,用于剥去指定的协议头部数据。
static inline void *strip_header(void *payload, unsigned int sz)
{
unsigned char *p = (unsigned char *)payload;
if (p == NULL || sz == 0)
return p;
return (void *)(p + sz);
}
Line 1-7: 入参payoad指针转换成unsigned char指针,确保p ++的步长为1个字节,返回payload + sz处的指针。
arp_recv是ARP模块暴露给网络设备模块的接口,接收并解析网络设备模块剥去以太网头部后的ARP数据帧。根据ARP request数据帧中的目标IP地址判断,是否为本机IP地址,如果是,则回复对应的ARP Reply数据帧。此处的本机IP就是本章开始提到的DIY TCP/IP协议栈的虚拟IP地址。通过DIY TCP/IP的初始化代码init.c,给DIY TCP/IP初始化一个局域网中不存在的IP地址,先来看arp_recv的实现,再来介绍IP地址的初始化。
int arp_recv(unsigned char *payload, unsigned int sz)
{
int ret = 0;
arppkt_t *pkt = NULL;
if (payload == NULL || sz == 0)
return 0;
pkt = (arppkt_t *)payload;
if (NTOHS(pkt->hw_type) != ARP_HW_TYPE ||
NTOHS(pkt->proto_type) != ARP_PROTO_TYPE ||
pkt->hw_sz != ARP_HW_SZ ||
pkt->proto_sz != ARP_PROTO_SZ) {
ret = -1;
log_printf(WARNING, "Invalid ARP Packet:"
"hw_type = %04x,"
"proto_type = %04x,"
"hw_sz = %02x,"
"proto_sz = %02x\n",
NTOHS(pkt->hw_type), NTOHS(pkt->proto_type), pkt->hw_sz, pkt->proto_sz);
goto out;
}
switch (NTOHS(pkt->op_code)) {
case ARP_REQUEST:
ret = process_arp_request(pkt);
break;
case ARP_REPLY:
break;
default:
log_printf(WARNING, "Invalid ARP operation code: %d\n",
NTOHS(pkt->op_code));
ret = -1;
break;
}
out:
return ret;
}
Line 1-21: arp_recv的入参payload是网络设备模块剥去以太网头部后的数据帧的指针,此时指向ARP Request头部数据。sz是剥去以太网头部后的数据帧的大小。将payload强制转换为arppkt_t *类型,检查ARP Request数据帧的合法性:包括ARP Request数据帧的hw type, protocol type, hw address size和protocol address size,丢弃不合法的ARP Request数据帧。
Line 22-35: 根据ARP数据帧的Op Code判断,如果是ARP Request则调用process_arp_request处理,ARP Reply的处理在实现ARP表时引入。
static int process_arp_request(arppkt_t *pkt)
{
int ret = 0;
unsigned char *hwaddr = NULL;
unsigned char *ipaddr = NULL;
log_printf(VERBOSE, "Who has "IPSTR"? TELL "IPSTR"\n",
IP2STR(pkt->target_ip), IP2STR(pkt->sender_ip));
if ((ipaddr = netdev_ipaddr()) == NULL) {
log_printf(ERROR, "Failed to get local ip address\n");
ret = -1;
goto out;
}
if ((hwaddr = netdev_hwaddr()) == NULL) {
log_printf(ERROR, "Failed to get net device hw address\n");
ret = -1;
goto out;
}
if (memcmp(pkt->target_ip, ipaddr, ARP_PROTO_SZ) == 0) {
log_printf(INFO, IPSTR" is at "MACSTR"\n",
IP2STR(ipaddr), MAC2STR(hwaddr));
}
out:
return ret;
}
Line 9-18: 调用网络设备模块的接口netdev_ipaddr获取DIY TCP/IP的IP地址,也就是本章开头提到的虚拟IP地址。netdev_hwaddr获取网络接口(common.h头文件中的DEFAULT_IFNAME)的硬件地址。netdev_ipaddr和netdev_hwaddr是网络设备模块新增的接口函数。IP地址和DEFAULT_IFNAME网络接口的硬件地址都保存在网络设备结构体net_device_t中。硬件地址在网络设备模块的初始化函数中获取,IP地址在DIY TCP/IP初始化时由用户输入,确保给DIY TCP/IP初始化的IP地址是局域网中不存在的IP地址。
Line 19-25: 比较ARP Request中的目标IP地址和DIY TCP/IP的虚拟IP地址是否相等,如果相等,则回复ARP Reply数据帧。使发送ARP Reply的主机能够建立虚拟IP地址和本机DEFAULT_IFNAME网络接口硬件地址的映射。网络设备层的发送队列尚未实现,本节只是简单打印出 x.x.x.x is at “yy:yy:yy:yy:yy:yy”字样,ARP Reply数据帧的发送在下节实现。
先来介绍DIY TCP/IP 的虚拟IP地址的初始化,首先是修改网络设备数据结构net_device_t,在其中添加ip和hwaddr成员。
#ifndef _DEVICE_H_
#define _DEVICE_H_
#include <pcap.h>
#include "utils.h"
…
typedef struct _net_device {
pcap_t *pcap_dev;
/* rx */
pthread_t rx_thread;
pthread_cond_t rxq_cond;
pthread_mutex_t rxq_mutex;
queue_t rxpkt_q;
/* tx */
int tx_sock;
pthread_t tx_thread;
pthread_cond_t txq_cond;
pthread_mutex_t txq_mutex;
queue_t txpkt_q;
/* dev info */
/* ip addr */
unsigned char ip[4];
/* hw addr */
unsigned char hwaddr[6];
} net_device_t;
…
unsigned char *netdev_ipaddr(void);
unsigned char *netdev_hwaddr(void);
…
#endif
Line 22和line 24是网络设备结构体中新增的两个unsigned char类型的数组,长度固定,分别用于保存用户输入的IP地址和DEFAULT_IFNAME网络接口的硬件地址。
Line 27和line 28是网络设备模块新增的函数接口,暴露给其他模块使用,用于获取DIY TCP/IP的IP地址和硬件地址。
目前DIY TCP/IP编译生成的可执行文件tcp_ip_stack运行时没有加任何参数。本节增加IP地址,TCP Port和Help选项做为DIY TCP/IP的初始化参数,修改DIY TCP/IP的初始化的代码如下:
int main(int argc, char *argv[])
{
int c, ret = 0;
unsigned char ip_cfg = 0;
unsigned char fake_ip[4];
net_device_t *ndev = NULL;
if (argc <= 1) {
log_printf(WARNING, "No option configured for DIY User Space TCP/IP stack\n");
goto skip_option;
}
for (;;) {
c = getopt(argc, argv, "i:p:h");
if (c < 0) {
if (optind <= (argc - 1)) {
log_printf(ERROR, "invalid option '%s' found\n", argv[optind]);
ret = -1;
goto out;
}
break;
}
switch (c) {
case 'i':
ip_cfg = 1;
dotip2rawbyte(optarg, fake_ip, sizeof(fake_ip));
break;
case 'p':
log_printf(INFO, "port: %s\n", optarg);
break;
case 'h':
print_help();
goto out;
default:
log_printf(ERROR, "invalid option: %s\n", optarg);
ret = -1;
goto out;
}
}
skip_option:
signal(SIGINT, signal_handler);
ndev = netdev_init(DEFAULT_IFNAME);
if (ndev == NULL) {
ret = -1;
goto out;
}
if (ip_cfg)
netdev_set_ipaddr(ndev, fake_ip, sizeof(fake_ip));
pdbuf_init();
netdev_start_loop(ndev);
out:
if (ndev)
netdev_deinit(ndev);
pdbuf_deinit();
return ret;
}
Line 1-10: 新增变量ip_cfg,判断是否给网络设备设置IP地址。fake_ip存放用户输入的IP地址。如果运行tcp_ip_stack时没加任何参数,即argc <=1,则跳转到skip_option标号处执行,跳过IP地址初始化过程。
Line 11-22: 如果运行时添加-i x.x.x.x 参数,则调用getopt解析命令行参数。getopt是c语言的库函数,argc和argv是main函数的入参,分别代表参数个数和指向参数字符串的指针数组。还有一个格式化的选项字符串”i:p:h”,选项之间通过:分隔,如果一个选项之后紧跟”:”,则该选项需要指定相应的参数值。如果选项之间没有”:”分隔,则选项不需要指定参数值。
getopt解析选项字符串,返回对应的选项字符值。全局变量optarg则指向选项对应的参数值。全局变量optind是argv数组的下标,argv[optind]指向getopt将要解析的下一个字符串,optind的初始值为1。如果运行tcp_ip_stack时输入的选项字符串不包含任何“-i”,”-p”或”-h”选项,例如./tcp_ip_stack 192.168.0.7,这也是本人在实现输入参数解析时碰到的一个错误,只设置了ip地址没有添加-i选项。当getopt返回负值时,optind为argv数组中第一个不是任何选项字符串的下标,判断如果optind <= argc -1,则说明输入了不合法的选项字符串,跳转到out标号,退出执行。感兴趣的朋友可以通过man 3 getopt查阅getopt库函数的更多详细信息。
Line 23-39: 本节main函数实现了对选项i和选项h的处理,选项p用于初始化TCP模块需要监听的端口号,在介绍DIY TCP/IP TCP模块的实现时引入。getopt返回选项i的ascii码时,optarg指向对应的点分十进制的IP地址字符串,将optarg交给dotip2rawbyte处理。 getopt返回选项h字符值时,调用print_help打印帮助信息。pint_help仅仅是调用log_printf打印提示信息,此处不再赘述,感兴趣的朋友可以在本节的代码实现中查看print_help的实现
Line 40-47:新增skip_option标号,处理没有任何选项输入的情况。
Line 48-49:通过网络设备模块的接口netdev_set_ipaddr将用户输入的IP地址设置给网络设备,其实就是存入net_device_t结构体的ip成员中。main函数中的其他代码实现已经在前面章节介绍过。
在介绍网络设备模块的改动之前,先来介绍dotip2rawbyte。用于将点分十进制的IP地址字符串,转换成去掉点后的4个整数值,存放在指定内存中。该函数是一个通用函数,实现在utility.c文件中。
void dotip2rawbyte(unsigned char *dotip, unsigned char *dst, unsigned int sz)
{
unsigned int i = 0;
unsigned char *p, *q;
if (dotip == NULL || dst == NULL || sz == 0)
return;
p = q = dotip;
while (*p && i < sz) {
q = strstr(p, ".");
if (q == NULL) {
dst[i ++] = atoi(p);
break;
}
*q ++ = '\0';
dst[i ++] = atoi(p);
p = q;
}
}
Line 1-19: 遍历dotip,通过strstr查找点分十进制字符串中的”.”,返回指向”.”的指针q,将*q设置为’\0’后,设置q指向点分十进制字符串的下个字符位置。调用atoi将以p为开头,以q - 1为结尾的字符串,转换为整数值,存放在dst指向的数组中。重复上述过程,直到字符串解析完成。
utility.c文件中除了新增dotip2rawbyte函数外,还新增了获取指定网络接口硬件地址的函数get_hw_addr,供ARP模块使用。ARP模块收到ARP Request之后,判断请求的IP地址是DIY TCP/IP的虚拟IP地址时,要构建ARP Reply数据帧,需要获取网络接口的硬件地址。get_hw_addr的调用放在网络设备模块的初始化函数中,将获取到的硬件地址存入网络设备结构体中,并通过网络设备模块的函数将硬件地址暴露给ARP模块,避免重复调用get_hw_addr函数。
int get_hw_addr(char *ifname, unsigned char *buf, unsigned int sz)
{
#define HW_ADDR_LEN 6
int fd = 0, ret = 0;
struct ifreq ifr;
unsigned int ifname_sz;
if (buf == NULL || sz < HW_ADDR_LEN) {
printf("%s: invalid params\n", __func__);
ret = -1;
goto out;
}
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Failed to create socket fd: %s (%d)\n",
strerror(errno), errno);
ret = -1;
goto out;
}
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, MIN(strlen(ifname) + 1, sizeof(ifr.ifr_name)));
if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) {
printf("Failed to get interface: %s hw address: %s (%d)\n",
ifr.ifr_name, strerror(errno), errno);
ret = -1;
goto out;
}
memcpy(buf, ifr.ifr_hwaddr.sa_data, HW_ADDR_LEN);
out:
if (fd)
close(fd);
return ret;
}
Linux为配置网络设备提供了标准的ioctl系统调用。通过任意domain,type,protocol类型的socket文件描述符,均可以向kernel space发出系统调用,请求或配置网络设备的参数。应用程序创建socket,初始化对应的ifreq结构体和请求命令字,将这些参数传给ioctl。ioctl系统调用,根据命令字和ifreq中的网络设备名称,配置或返回应用程序要完成的操作。get_hw_addr就是通过SIOCGIFHWADDR命令字请求指定网络接口的硬件地址。
Line 1-18: 创建socket文件描述符,AF_INET域,SOCK_STREM类型,任意protocol(0)。
Line 19-32: 初始化ifreq结构体之前先清空ifreq变量,然后将网络设备的名称复制给ifreq.ifr_name。把fd,SIOCGIFHWADDR和ifreq结构体指针传给ioctl。系统调用成功返回时,kernel将网络接口对应的硬件地址存放在ifreq.ifr_hwaddr.sa_data中返回给用户空间。最后将硬件地址复制到get_hw_addr的入参buf指向的memory中,返回给调用者。此处用到的MIN取两个无符号整数的最小值,该宏定义实现在utils.h中,实现简单读者可查看本节的utils.h文件,此处不再给出。
utils.c是新增的c文件,朋友们一定还记得网络设备模块链表和队列操作的实现都是作为内联函数放在utils.h文件中。新增的utils.c需要加入Makefile中编译并链接到tcp_ip_stack目标文件。前面介绍过的章节已经循序渐进的向Makefile中添加了debug.c, arp.c等文件的编译和链接,此处不再赘述Makefile的修改,读者可以参考本节的Makefile。
再来看ARP模块在处理收到的ARP Request时,调用到的netdev_ipaddr和netdev_hwaddr,以及网络设备模块的修改。
网络设备模块初始化时调用get_hw_addr获取网络接口的硬件地址,网络设备初始化完成后,netdev_init返回到main函数中,DIY TCP/IP的初始化main函数继续执行,将用户输入的ip地址配置给网络设备。这些操作完成之后网络设备模块的netdev_ipaddr和netdev_hwaddr就可以向其他模块返回DIY TCP/IP的虚拟IP地址和网络接口对应的硬件地址了。
device.c
net_device_t * netdev_init(char *ifname)
{
net_device_t *ndev = NULL;
char pcap_packet_filter[FILTER_BUFFER_SIZE];
char err_buf[PCAP_ERRBUF_SIZE];
struct bpf_program filter_code;
…
/* rx init */
if (dev_rx_init(ndev))
goto out;
/* hw addr init */
if (get_hw_addr(ifname, ndev->hwaddr, sizeof(ndev->hwaddr)))
goto out;
return ndev;
out:
netdev_deinit(ndev);
return NULL;
}
Line 12: 网络设备模块初始化函数netdev_init中,接收队列初始化完成后,调用get_hw_addr获取网络接口对应的硬件地址,保存在ndev->hwaddr中。netdev_init函数的其他代码与5.4.1节一致。
void netdev_set_ipaddr(net_device_t *ndev, unsigned char *ip, unsigned int sz)
{
if (ndev == NULL ||
ip == NULL ||
sz > sizeof(ndev->ip)) {
log_printf(ERROR, "Invalid params for net device ip addr\n");
return;
}
memcpy(ndev->ip, ip, sz);
log_printf(INFO, "Net device ip address: "IPSTR"\n", IP2STR(ndev->ip));
}
unsigned char *netdev_ipaddr(void)
{
if (gndev == NULL)
return NULL;
return gndev->ip;
}
netdev_set_ipaddr, netdev_ipaddr,netdev_hwaddr实现都很简单,这样做的目的是将gndev封装起来,通过网络设备模块的接口设置或获取相应的信息。避免其他模块直接操作网络设备结构体。到这里arp_recv中用到的新增函数都已经全部介绍完成,ARP模块收到ARP Request后,解析ARP数据帧,判断ARP Request请求的目标IP地址是否与DIY TCP/IP的虚拟IP地址相等,如果相等,则打印 is at 信息,来看本节代码的运行结果。
gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch4/0$ sudo ./tcp_ip_stack -i 192.168.0.7
Network device init
filter: ether proto 0x0800 or ether proto 0x0806
Network device RX init
Net device ip address: 192.168.0.7
192.168.0.7 is at 00:0c:29:2e:0a:ed
192.168.0.7 is at 00:0c:29:2e:0a:ed
192.168.0.7 is at 00:0c:29:2e:0a:ed
192.168.0.7 is at 00:0c:29:2e:0a:ed
192.168.0.7 is at 00:0c:29:2e:0a:ed
192.168.0.7 is at 00:0c:29:2e:0a:ed
^Cpcap_loop ended
Network device deinit
Network device RX deinit
Dev rx routine exited
dev flushed 0 packets
运行tcp_ip_stack的参数为-i 192.168.0.7,该ip地址在本人所处的局域网中不存在。按照本章开头时介绍的测试方法,在局域网中另外一台主机B上先清除ARP表,确保主机B ping 192.168.0.7时,会发出ARP Request广播数据帧。运行tcp_ip_stack的主机A收到ARP Request帧后,DIY TCP/IP 的网络设备模块将ARP Request数据帧传给本节实现的ARP模块处理,正确打印出该IP地址和网络接口硬件地址的映射。
下一篇:DIY TCP/IP ARP模块2
本文围绕DIY TCP/IP的ARP模块展开,实现了ARP模块接收函数arp_recv,用于解析ARP Request数据帧。介绍了网络设备模块接收线程处理函数的修改,以及IP地址初始化、命令行参数解析、点分十进制IP字符串转换等内容。通过运行测试,验证了ARP模块对ARP Request的处理。

335

被折叠的 条评论
为什么被折叠?



