上一篇: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子產品的實作0