DIY TCP/IP ARP模块3
上一篇:DIY TCP/IP ARP模块2
7.4 ARP表的实现
先来概括介绍下ARP表,和ARP表的查找过程。ARP表存放局域网中IP地址和硬件地址的映射,构造以太网头部时需要根据目标IP地址查找ARP表获取目标硬件地址。如果目标IP地址与网络接口的IP地址在同一个局域网中,则查找ARP表,获取对应的目标硬件地址,如果查找失败则发送ARP Request,获取目标IP地址对应的硬件地址。收到ARP Reply后更新ARP表项。如果目标IP地址与网络接口的IP地址不在同一个局域网中,则查找路由表,得到下一跳网络接口IP地址,再查找下一跳的IP地址对应的硬件地址,构造相应的以太网头部。
DIY TCP/IP的ARP表的实现相对简化,由于DIY TCP/IP只运行在局域网中,所以暂不实现目标IP地址与网络接口IP地址不在同一网段的处理。另外ARP表的更新,不是通过先发送ARP Request,在得到ARP Reply之后,更新ARP表。而是根据ARP Requset更新ARP表,当DIY TCP/IP的ARP模块收到ARP Request数据帧后,判断请求的IP地址和虚拟IP地址相等时,就将ARP Request数据帧中的源IP地址和源硬件地址做为ARP表项,更新到ARP表中,简化查表失败时先发送ARP Reuqest再根据ARP Reply更新ARP表的过程。
本节的目标是改写7.1.3节中的build_ethernet_header函数,该函数暴露给其他模块使用,构造以太网头部。7.1.3节是在已知源硬件地址和目标硬件地址的情况下调用该函数,所以没有涉及ARP表的查找操作。围绕该函数的改写,引入ARP表项数据结构的定义,以及在该数据结构的基础上实现的ARP表,表项的新增,删除和查找操作。
先在arp.c中增加ARP表项数据结构的定义
typedef struct _arp_entry {
unsigned char ip[ARP_PROTO_SZ];
unsigned char mac[ARP_HW_SZ];
} arp_entry_t;
static arp_entry_t *arp_table = NULL;
static unsigned int num_arp_entry = 0;
static arp_entry_t null_entry = {
.ip = {0, 0, 0, 0},
.mac = {0, 0, 0, 0, 0, 0},
};
Line 1-4: arp_entry_t结构体包括两个固定长度的unsigned char数组,ip长度为4个字节,mac长度为6个字节,分别用于存放ARP表项的IP地址和对应的硬件地址。
Line 6-7: arp_entry_t类型的指针,arp_table指向的内存存放ARP表,num_arp_entry是ARP表项数目。
Line 8-11: null_entry是arp_entry_t类型,ip和mac均初始化为0,用于匹配ARP表中的空闲表项。
围绕arp_entry_t数据结构的定义,实现ARP表的初始化,销毁,表项的新增和删除函数。
static int init_arp_table()
{
int ret = 0;
if (arp_table != NULL)
goto out;
arp_table = (arp_entry_t *)malloc(sizeof(arp_entry_t));
if (arp_table == NULL) {
log_printf(ERROR, "No memory for arp table, %s (%d\n",
strerror(errno), errno);
ret = -1;
goto out;
}
memset(arp_table, 0, sizeof(arp_entry_t));
num_arp_entry = 1;
out:
return ret;
}
static void deinit_arp_table()
{
if (arp_table == NULL)
return;
log_printf(INFO, "Destroy ARP table, %u %s\n",
num_arp_entry, num_arp_entry > 1 ? "entries" : "entry");
free(arp_table);
}
Line 1-17: init_arp_table函数初始化ARP表, arp_table指针为空时,通过malloc申请能够存放一个ARP表项的内存空间,arp_table指向该内存空间的首地址。malloc执行失败时,打印出错信息,结束执行。malloc成功时,初始化arp_table指向的内存空间为全0,num_arp_entry为1。
Line 19-26: deinit_arp_table,销毁ARP表,打印ARP表中的表项数目,然后调用free释放arp_table占用的内存空间。
init_arp_table和deinit_arp_table是ARP模块的静态函数,分别在ARP模块的初始化函数arp_init和销毁函数arp_deinit中被调用。
static void dump_arp_table()
{
unsigned int i = 0;
log_printf(INFO, "ARP Table\n");
log_printf(INFO, "IP Address\t\tMAC Address\n");
for (i = 0; i < num_arp_entry; i ++) {
if (memcmp(&arp_table[i], &null_entry, sizeof(arp_entry_t)) == 0)
continue;
log_printf(INFO, IPSTR"\t\t"MACSTR"\n",
IP2STR(arp_table[i].ip),
MAC2STR(arp_table[i].mac));
}
}
Line 1-13: dump_arp_table遍历ARP表,打印ARP表项的内容,跳过内容为全0的null_entry表项。
static int add_arp_entry(unsigned char *ip, unsigned char *mac)
{
int ret = 0;
unsigned int i = 0;
arp_entry_t tmp_entry;
memcpy(tmp_entry.ip, ip, ARP_PROTO_SZ);
memcpy(tmp_entry.mac, mac, ARP_HW_SZ);
for (i = 0; i < num_arp_entry; i ++) {
if (memcmp(&tmp_entry, &arp_table[i], sizeof(arp_entry_t)) == 0) {
log_printf(INFO, "ARP entry: "IPSTR" "MACSTR" already exists\n",
IP2STR(ip), MAC2STR(mac));
goto out;
}
}
/* look for null entry */
for (i = 0; i < num_arp_entry; i ++)
if (memcmp(&arp_table[i], &null_entry, sizeof(arp_entry_t)) == 0)
break;
/* extend arp table */
if (i >= num_arp_entry) {
arp_table = realloc(arp_table, (num_arp_entry + 1) * sizeof(arp_entry_t));
if (arp_table == NULL) {
log_printf(ERROR, "No memory for new arp entry, %s (%d)\n",
strerror(errno), errno);
ret = -1;
goto out;
}
memset(&arp_table[i], 0, sizeof(arp_entry_t));
num_arp_entry += 1;
}
memcpy(&arp_tabe[i], &tmp_entry, sizeof(arp_entry_t));
out:
return ret;
}
add_arp_entry新增ARP表项,入参为IP地址和硬件地址的指针。定义局部变量tmp_entry,通过入参ip和mac初始化tmp_entry,这样在查找ARP表做比较时可以用ARP表项为单位比较。遍历ARP表,如果tmp_entry已经在ARP表中,返回ret=0。如果没有找到tmp_entry,则tmp_entry做为新增ARP表项加入ARP表中,首先在ARP表中查找null_entry表项,如果找到,则将temp_entry复制到null_entry的位置。如果没有找到null_entry表项,则通过realloc扩展arp_table的内存空间。realloc扩展arp_table失败时,打印提示信息,返回ret=-1,成功时扩展的内存空间大小为一个ARP表项。将扩展的内存空间清0后,累加num_arp_entry,最后将temp_entry复制到新扩展的表项位置,完成ARP表项的添加。
static int del_arp_entry(unsigned char *ip, unsigned char *mac)
{
int ret = -1;
unsigned int i = 0;
for (i = 0; i < num_arp_entry; i ++) {
if (memcmp(ip, arp_table[i].ip, ARP_PROTO_SZ) == 0 &&
memcmp(mac, arp_table[i].mac, ARP_HW_SZ) == 0) {
log_printf(INFO, "Delete ARP entry: "IPSTR" "MACSTR"\n",
IP2STR(ip), MAC2STR(mac));
memset(&arp_table[i], 0, sizeof(arp_entry_t));
ret = 0;
break;
}
}
return ret;
}
del_arp_entry,删除ARP表项,入参为IP地址和硬件地址的指针。遍历arp_table的每个表项,比较ip地址和硬件地址和入参IP和硬件地址相等的表项,找到对应表项时将其置为null_entry。
static int update_arp_table(unsigned char *ip, unsigned char *mac, unsigned int add)
{
int ret = 0;
if (ip == NULL || mac == NULL) {
log_printf(ERROR, "Invalid params to update ARP table\n");
return -1;
}
if (add)
ret = add_arp_entry(ip, mac);
else
ret = del_arp_entry(ip, mac);
dump_arp_table();
return ret;
}
update_arp_table,更新ARP表,入参为IP地址和硬件地址的指针,add标记更新操作是添加还是删除操作。检查ip和mac入参均不为空时,如果add不为0,则将调用arp_add_entry添加新的ARP表项,如果add为0,则调用add_del_entry删除表项。函数末尾调用dump_arp_table打印更新后的arp表,做为debug使用。
static unsigned char *lookup_arp_table(unsigned char *ip)
{
unsigned int i = 0;
unsigned char *mac = NULL;
unsigned char *local_ip = NULL;
/* hardcode subnet mask 255.255.255.0 */
unsigned char subnet_mask[4] = {0xff, 0xff, 0xff, 0x0};
if (ip == NULL)
goto out;
local_ip = netdev_ipaddr();
if (local_ip == NULL)
goto out;
/* within same subnet */
if (((ip[0] & subnet_mask[0]) == local_ip[0]) &&
((ip[1] & subnet_mask[1]) == local_ip[1]) &&
((ip[2] & subnet_mask[2]) == local_ip[2])) {
for (i = 0; i < num_arp_entry; i ++) {
if (memcmp(ip, arp_table[i].ip, ARP_PROTO_SZ) == 0) {
mac = arp_table[i].mac;
break;
}
}
/* no arp entry found, send out arp request*/
if (i >= num_arp_entry) {
/* todo send out arp request */
}
} else {
/* lookup route table for next hop ip address */
log_printf(WARNING, "Need to lookup route table\n");
}
out:
return mac;
}
lookup_arp_table查找ARP表,本节开始时概括介绍过ARP的查表过程,现在来看其具体实现。
lookup_arp_table的入参为IP地址的指针,返回值是unsigned char *指针,返回null时查表失败,成功时返回值指向ARP表中与入参IP地址对应的硬件地址。
Line 3-7: 定于局部变量i,mac初始值为null,local_ip是DIY TCP/IP中pcap_t设备的网络接口ip地址,subnet_mask子网掩码,被硬编码为255.255.255.0。用于比较入参IP地址和local_ip地址的网络前缀是否相等。
Line 9-13: 判断入参IP是否为空,以及调用网络设备模块的netdev_ipaddr获取网络接口的IP地址的指针,赋值给local_ip。
Line 14-34: 首先比较入参IP和local_ip的24位的网络前缀是否相等,判断入参IP地址和local_ip是否在相同局域网中。如果不再同一局域网中,则需要查找路由表,获取下一跳网络接口的IP地址,该部分功能暂不实现。如果在同一局域网中,则遍历arp_table,查找与入参IP相等的ARP表项,查找成功时将ARP表项中的硬件地址的指针赋值给mac,查找失败时需要发送ARP Request获取与入参IP对应的硬件地址,该部分内存简化实现为通过接收ARP Request更新ARP表项。暂时不实现的内容和简化实现的内容本节开始已经介绍过原因,该函数最后返回mac指针的值。
以上内容介绍了围绕arp_entry_t数据结构的定义,实现的init_arp_table,deinit_arp_table,dump_arp_table,add_arp_entry,del_arp_entry,update_arp_table和lookup_arp_table。这些函数都是ARP模块内部的静态函数,仅在ARP模块内部使用。
先来看update_arp_table在ARP模块内部的使用,本节开头部分已经介绍了ARP表的更新不仅仅依赖于收到的ARP Reply数据帧,当收到ARP Request数据帧,判断需要回复ARP Reply数据帧时,同样更新ARP表,简化ARP表失查找败时,发送ARP Request的处理。所以要修改proces_arp_request函数,加入update_arp_table的调用,更新ARP表。
static int process_arp_request(arphdr_t *pkt)
{
…
if (memcmp(pkt->target_ip, ipaddr, ARP_PROTO_SZ))
goto out;
log_printf(INFO, IPSTR " is at "MACSTR"\n",
IP2STR(pkt->target_ip), MAC2STR(hwaddr));
update_arp_table(pkt->sender_ip, pkt->sender_mac, 1);
/* build ARP reply */
pdbuf = pdbuf_alloc(ARP_PADDING_SZ, 0);
if (pdbuf == NULL) {
ret = -1;
goto out;
}
pdbuf_push(pdbuf, sizeof(arphdr_t) + ARP_PADDING_SZ);
reply = (arphdr_t *)pdbuf->payload;
memcpy(reply, pkt, sizeof(arphdr_t));
reply->op_code = HSTON(ARP_REPLY);
memcpy(reply->sender_mac, hwaddr, ARP_HW_SZ);
memcpy(reply->sender_ip, ipaddr, ARP_PROTO_SZ);
memcpy(reply->target_mac, pkt->sender_mac, ARP_HW_SZ);
memcpy(reply->target_ip, pkt->sender_ip, ARP_PROTO_SZ);
build_ethernet_header(pdbuf, ETHERNET_ARP,
reply->sender_ip, reply->target_ip,
reply->sender_mac, reply->target_mac);
netdev_tx_pkt(pdbuf);
out:
return ret;
}
Line 8: 判断ARP Reuquest请求的IP地址和虚拟IP地址相等后,构建ARP Reply数据帧之前,调用update_arp_table,将发送ARP Reply数据帧的源IP地址和源硬件地址,加入ARP表中,更新ARP表。process_arp_reuquest其余部分的代码与7.1.3节一致,不再细述。
再来看ARP模块暴露给其他模块使用的新增函数:arp_init,arp_deinit,分别调用arp_init_table和arp_deinit_table完成ARP表的初始化,和ARP表的销毁操作。
int arp_init()
{
return init_arp_table();
}
void arp_deinit()
{
deinit_arp_table();
}
再来看ARP模块暴露给其他模块使用的一个重要函数,build_ethernet_header,当其他模块调用该函数的入参dst_mac,目标硬件地址为空时,增加查找ARP表的过程。
static int build_ethernet_header(void *buf, unsigned short type,
unsigned char *src_ip, unsigned char *dst_ip,
unsigned char *src_mac, unsigned char *dst_mac)
{
int ret = 0;
pdbuf_t *pdbuf = NULL;
ethhdr_t *ethhdr = NULL;
if (buf == NULL || (dst_ip == NULL && dst_mac == NULL )) {
log_printf(ERROR, "Invalid parameters to build ethernet header\n");
ret = -1;
goto out;
}
if (src_mac && dst_mac)
goto build_hdr;
if (src_mac == NULL)
src_mac = netdev_hwaddr();
if (dst_mac == NULL)
dst_mac = lookup_arp_table(dst_ip);
if (dst_mac == NULL || src_mac == NULL) {
ret = -1;
goto out;
}
build_hdr:
pdbuf = buf;
pdbuf_push(pdbuf, sizeof(ethhdr_t));
ethhdr = (ethhdr_t *)pdbuf->payload;
memcpy(ethhdr->dst, dst_mac, sizeof(ethhdr->dst));
memcpy(ethhdr->src, src_mac, sizeof(ethhdr->src));
ethhdr->type = HSTON(type);
log_printf(VERBOSE, "eth hdr, dst: "MACSTR" src: "MACSTR" type: %04x\n",
MAC2STR(ethhdr->dst), MAC2STR(ethhdr->src), NTOHS(ethhdr->type));
out:
return ret;
}
高亮部分的代码是本节对build_ethernet_header函数做的修改,其余部分与7.1.3节的实现一致。高亮部分首先判断build_ether_header的入参,源硬件地址src_mac和目标硬件地址dst_mac是否都不为空,如果都不为空则 直接跳转到build_hdr标号处构建以太网头部。如果源硬件地址为空,则调用网络设备模块的函数netdev_hwaddr获取网络接口硬件地址,如果目标硬件地址为空,则调用lookup_arp_table根据目标IP地址查找对应的目标硬件地址。成功获取源硬件地址和目标硬件地址的条件下,构建以太网头部数据。line188行判断如果src_mac和dst_mac任意一个为空,则返回ret=-1。
build_ethernet_header与arp_init,arp_deinit一样,都是ARP模块暴露给其他模块使用的函数接口,build_ethernet_header将在IP模块发送数据帧时被调用,arp_init和arp_deinit在DIY TCP/IP的初始化代码中被调用,来看init.c中main函数的修改。
int main(int argc, char *argv[])
{
…
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();
arp_init();
netdev_start_loop(ndev);
out:
if (ndev)
netdev_deinit(ndev);
arp_deinit();
pdbuf_deinit();
return ret;
}
DIY TCP/IP的初始化代码,init.c中的main函数,已经被多次修改过,随着各个模块的实现,加入了模块的初始化代码和销毁代码的调用。最近一次修改是在7.2节,加入了main函数入参解析的实现。本节在pdbuf模块的初始化之后,加入arp_init,对应的在pdbuf_deinit模块销毁之前加入arp_deinit的调用。pdbuf模块管理DIY TCP/IP其他模块对buffer的分配和释放,pdbuf模块被销毁时将统计buffer分配的总数和释放的总数,提示有无内存泄漏,所以应当是销毁协议栈时最后一个被销毁的模块。
编译运行结果如下:
[email protected]:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch4/3$ 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.107 8c:a9:82:11:d1:de
^Cpcap_loop ended
Network device deinit
Network device RX deinit
Dev rx routine exited
Dev rxq flushed 0 packets
Network device TX deinit
Dev tx routine exited
Dev txq flushed 0 packets
Destroy ARP table, 1 entry
#Internal Buffer Management#
Alloc: 1, Free: 1
本节的测试方法与7.1.3节一样,设置局域网中不存存在的IP地址192.168.0.7为DIY TCP/IP的虚拟IP地址,在同一局域网中的另外一台win7机器上ping 192.168.0.7,DIY TCP/IP收到ARP Request请求后,判断请求IP地址与虚拟IP地址相等,打印出192.168.0.7 is at 00:0c:29:2e:0a:ed,紧接着调用update_arp_table更新ARP表,update_arp_table函数的末尾调用dump_arp_table,打印ARP表的内容。输入ctrl+c,结束DIY TCP/IP运行时,arp_deinit释放ARP表占用的内存,并统计ARP表中表项的数目为1。
7.5 小结
本章介绍了DIY TCP/IP的ARP模块的实现,包括ARP数据帧的结构,ARP Request数据帧的接收,解析,ARP Reply数据帧的发送,ARP表的实现。新增了DIY TCP/IP初始化模块的参数解析的实现,修改了网络设备结构体的定义,以及网络设备模块通过PF_PACKET发送数据帧的实现。经验证,DIY TCP/IP可以正确接收ARP Request数据帧,并能够在ARP Request请求的目标IP地址与DIY TCP/IP的虚拟IP地址相等时,构建并回复正确的ARP Reply数据帧。在同一局域网中发送ARP Request的主机上的ARP表中,能够查找到DIY TCP/IP的虚拟IP和运行DIY TCP/IP的主机的网络接口硬件地址的映射。本章的ARP模块的覆盖了DIY TCP/IP目前实现的网络设备模块,pdbuf模块,debug模块,utility模块的代码实现,正确回复ARP Reply数据帧,表明这些模块的代码实现是正确的。
当前目录结构
下节内容:DIY TCP/IP IP模块和ICMP模块的实现