昨天我們完成了發送 ARP Request 。
今天我們要完成 ARP 協定的另一半:接收與解析 ARP Reply,並建立了網路堆疊的第一個快取記憶庫 ── ARP Table,同時具備自動回應 ARP Request 的能力。
在 Day04 中,我們已經能送出 ARP Request 詢問「誰擁有這個 IP?」;Day05 則接著處理對方回覆後的學習流程:解析 ARP Reply、記住 IP/MAC 對應,並在後續傳送封包時查詢 ARP Table。

有了 ARP Table 之後,Network Stack 就不再只是單純收發封包,而是開始具備「記憶網路狀態」的能力:它可以把學到的位址對應快取起來,讓後續封包能直接找到正確的 Destination MAC。
ARP Request
↓
收到 ARP Reply ──► 解析 Sender IP/MAC ──► 寫入與查詢 ARP Table
↓
收到 ARP Request ──► 判斷是否為本機 IP ──► 自動組裝並發送 ARP Reply
完成以下完整流程:
10.0.0.1 ────► 52:54:00:12:34:56 (自動學習並存入 ARP Table)
驗收項目:
arp_table.h 與 arp_table.c 結構與實作arp_table_init(), arp_table_insert(), arp_table_lookup(), arp_table_dump()
arp_handle_reply() 與 arp_handle_request() 自動回覆機制arp_receive():同時支援 ARP_REQUEST (Opcode=1) 與 ARP_REPLY (Opcode=2)tap0 介面自動 UP 機制與 Raw Socket 測試腳本include/arp_table.h定義 ARP Table Entry 資料結構與函式介面:
#ifndef ARP_TABLE_H
#define ARP_TABLE_H
#include <stdint.h>
#define ARP_TABLE_SIZE 32
struct arp_entry {
uint8_t ip[4];
uint8_t mac[6];
int valid;
};
extern struct arp_entry arp_table[ARP_TABLE_SIZE];
void arp_table_init(void);
int arp_table_insert(const uint8_t *ip, const uint8_t *mac);
struct arp_entry *arp_table_lookup(const uint8_t *ip);
void arp_table_dump(void);
#endif
src/arp_table.c實作 ARP Table 的記憶體初始化、新增/更新、查詢與格式化傾印:
#include <stdio.h>
#include <string.h>
#include "arp_table.h"
struct arp_entry arp_table[ARP_TABLE_SIZE];
void arp_table_init(void)
{
memset(arp_table, 0, sizeof(arp_table));
}
int arp_table_insert(const uint8_t *ip, const uint8_t *mac)
{
// 若 IP 已存在則更新 MAC
for (int i = 0; i < ARP_TABLE_SIZE; i++) {
if (arp_table[i].valid && memcmp(arp_table[i].ip, ip, 4) == 0) {
memcpy(arp_table[i].mac, mac, 6);
return 0;
}
}
// 寫入第一個空位
for (int i = 0; i < ARP_TABLE_SIZE; i++) {
if (!arp_table[i].valid) {
memcpy(arp_table[i].ip, ip, 4);
memcpy(arp_table[i].mac, mac, 6);
arp_table[i].valid = 1;
return 0;
}
}
return -1; // 表滿
}
struct arp_entry *arp_table_lookup(const uint8_t *ip)
{
for (int i = 0; i < ARP_TABLE_SIZE; i++) {
if (arp_table[i].valid && memcmp(arp_table[i].ip, ip, 4) == 0) {
return &arp_table[i];
}
}
return NULL;
}
void arp_table_dump(void)
{
printf("\nARP Table\n=========================\n");
for (int i = 0; i < ARP_TABLE_SIZE; i++) {
if (arp_table[i].valid) {
printf("%u.%u.%u.%u -> %02x:%02x:%02x:%02x:%02x:%02x\n",
arp_table[i].ip[0], arp_table[i].ip[1],
arp_table[i].ip[2], arp_table[i].ip[3],
arp_table[i].mac[0], arp_table[i].mac[1],
arp_table[i].mac[2], arp_table[i].mac[3],
arp_table[i].mac[4], arp_table[i].mac[5]);
}
}
printf("\n");
fflush(stdout);
}
include/arp.h & src/arp.c (ARP 處理核心)// include/arp.h
#ifndef ARP_H
#define ARP_H
#include <stdint.h>
#include <stddef.h>
#define ARP_REQUEST 1
#define ARP_REPLY 2
struct arp_packet {
uint16_t htype;
uint16_t ptype;
uint8_t hlen;
uint8_t plen;
uint16_t oper;
uint8_t sha[6];
uint8_t spa[4];
uint8_t tha[6];
uint8_t tpa[4];
} __attribute__((packed));
void arp_build_request(
struct arp_packet *arp,
const uint8_t *src_mac,
const uint8_t *src_ip,
const uint8_t *target_ip
);
void arp_handle_reply(const struct arp_packet *arp);
void arp_handle_request(int fd, const struct arp_packet *arp);
void arp_receive(int fd, const uint8_t *payload, size_t len);
#endif
// src/arp.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "config.h"
#include "ethernet.h"
#include "arp.h"
#include "arp_table.h"
void arp_handle_reply(const struct arp_packet *arp)
{
arp_table_insert(arp->spa, arp->sha);
printf("[ARP] Reply Received\n");
fflush(stdout);
arp_table_dump();
}
void arp_handle_request(int fd, const struct arp_packet *arp)
{
// 只回應詢問 LOCAL_IP (10.0.0.2) 的 ARP Request
if (memcmp(arp->tpa, LOCAL_IP, 4) != 0) {
return;
}
printf("[ARP] Request for %u.%u.%u.%u received -> Generating ARP Reply\n",
arp->tpa[0], arp->tpa[1], arp->tpa[2], arp->tpa[3]);
fflush(stdout);
// 順便學習請求者的 IP/MAC
arp_table_insert(arp->spa, arp->sha);
// 組裝完整的 Ethernet + ARP Reply Frame (14 + 28 = 42 bytes)
uint8_t frame[ETH_HEADER_LEN + sizeof(struct arp_packet)];
struct ethernet_hdr *eth = (struct ethernet_hdr *)frame;
struct arp_packet *reply = (struct arp_packet *)(frame + ETH_HEADER_LEN);
// 1. Ethernet Header
memcpy(eth->dst, arp->sha, ETH_ADDR_LEN);
memcpy(eth->src, LOCAL_MAC, ETH_ADDR_LEN);
eth->ethertype = htons(ETHERTYPE_ARP);
// 2. ARP Header & Payload
reply->htype = htons(1);
reply->ptype = htons(0x0800);
reply->hlen = 6;
reply->plen = 4;
reply->oper = htons(ARP_REPLY);
memcpy(reply->sha, LOCAL_MAC, 6);
memcpy(reply->spa, LOCAL_IP, 4);
memcpy(reply->tha, arp->sha, 6);
memcpy(reply->tpa, arp->spa, 4);
// 3. 發送回 TAP 虛擬網卡
ssize_t sent = write(fd, frame, sizeof(frame));
if (sent < 0) {
perror("[ARP] write failed");
} else {
printf("[ARP] Reply sent (%ld bytes)\n", sent);
fflush(stdout);
}
}
void arp_receive(int fd, const uint8_t *payload, size_t len)
{
if (len < sizeof(struct arp_packet))
return;
const struct arp_packet *arp = (const struct arp_packet *)payload;
uint16_t opcode = ntohs(arp->oper);
if (opcode == ARP_REQUEST) {
arp_handle_request(fd, arp);
} else if (opcode == ARP_REPLY) {
arp_handle_reply(arp);
}
}
src/tap.c (主程式與 EtherType 分流整合)#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/if_tun.h>
#include <net/if.h>
#include <arpa/inet.h>
#include "ethernet.h"
#include "arp.h"
#include "arp_table.h"
int tun_alloc(char *dev)
{
struct ifreq ifr;
int fd;
fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
perror("open /dev/net/tun");
exit(1);
}
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
if (dev && *dev) {
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
}
if (ioctl(fd, TUNSETIFF, &ifr) < 0) {
perror("ioctl TUNSETIFF");
close(fd);
exit(1);
}
strcpy(dev, ifr.ifr_name);
// 自動將 TAP 網卡設置為 UP | RUNNING 狀態
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock >= 0) {
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
if (ioctl(sock, SIOCGIFFLAGS, &ifr) >= 0) {
ifr.ifr_flags |= (IFF_UP | IFF_RUNNING);
ioctl(sock, SIOCSIFFLAGS, &ifr);
}
close(sock);
}
return fd;
}
int main()
{
char dev[IFNAMSIZ] = "tap0";
unsigned char buffer[2048];
int fd = tun_alloc(dev);
arp_table_init();
printf("TAP device: %s (UP)\n", dev);
printf("Ethernet header size: %lu bytes\n", sizeof(struct ethernet_hdr));
printf("Waiting for Ethernet frame...\n");
fflush(stdout);
while (1) {
int n = read(fd, buffer, sizeof(buffer));
if (n < 0) {
perror("read");
break;
}
if (n < ETH_HEADER_LEN) {
printf("Invalid Ethernet frame\n");
fflush(stdout);
continue;
}
struct ethernet_hdr *eth = (struct ethernet_hdr *)buffer;
if (!ethernet_accept_frame(eth)) {
printf("[DROP] Not for me\n");
fflush(stdout);
continue;
}
printf("[ACCEPT]\n");
ethernet_print_header(eth);
// Ethernet EtherType 分流器 (Dispatcher)
const uint8_t *payload = buffer + ETH_HEADER_LEN;
size_t payload_len = n - ETH_HEADER_LEN;
switch (ntohs(eth->ethertype)) {
case ETHERTYPE_ARP:
arp_receive(fd, payload, payload_len);
break;
default:
printf("Unknown EtherType: 0x%04x\n", ntohs(eth->ethertype));
break;
}
printf("Frame length: %d bytes\n\n", n);
fflush(stdout);
}
close(fd);
return 0;
}
tests/send_arp_reply.c (Raw Socket 測試發送端)#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/if_ether.h>
#include <netpacket/packet.h>
#include "config.h"
#include "ethernet.h"
#include "arp.h"
int main()
{
int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockfd < 0) {
perror("socket");
return 1;
}
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = if_nametoindex("tap0");
sll.sll_protocol = htons(ETH_P_ALL);
if (sll.sll_ifindex == 0) {
perror("if_nametoindex tap0 (請確保 ./network 正在執行中!)");
close(sockfd);
return 1;
}
unsigned char frame[64];
memset(frame, 0, sizeof(frame));
struct ethernet_hdr *eth = (struct ethernet_hdr *)frame;
struct arp_packet *arp = (struct arp_packet *)(frame + ETH_HEADER_LEN);
uint8_t sender_mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56};
uint8_t sender_ip[4] = {10, 0, 0, 1};
memcpy(eth->dst, LOCAL_MAC, ETH_ADDR_LEN);
memcpy(eth->src, sender_mac, ETH_ADDR_LEN);
eth->ethertype = htons(ETHERTYPE_ARP);
arp->htype = htons(1);
arp->ptype = htons(0x0800);
arp->hlen = 6;
arp->plen = 4;
arp->oper = htons(ARP_REPLY);
memcpy(arp->sha, sender_mac, 6);
memcpy(arp->spa, sender_ip, 4);
memcpy(arp->tha, LOCAL_MAC, 6);
memcpy(arp->tpa, LOCAL_IP, 4);
int len = ETH_HEADER_LEN + sizeof(struct arp_packet);
printf("Sending ARP Reply via Raw Socket: 10.0.0.1 is at 52:54:00:12:34:56...\n");
if (sendto(sockfd, frame, len, 0, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
perror("sendto");
} else {
printf("Sent %d bytes ARP Reply frame successfully!\n", len);
}
close(sockfd);
return 0;
}
Opcode 是什麼?Opcode (Operation Code,操作碼) 是 ARP 標頭中 16-bit 的欄位,用來告訴接收端這個封包是在「問問題」還是在「回答問題」。
Opcode = 1 (ARP_REQUEST) ➔ 廣播詢問「誰有這個 IP?請告訴我 MAC」。Opcode = 2 (ARP_REPLY) ➔ 單播回應「這個 IP 在這個 MAC」。ETHERTYPE_ARP (0x0806) 與 ETHERTYPE_IPV4 (0x0800) 的用途分別是什麼?ETHERTYPE_ARP (0x0806):Layer 2 與 Layer 3 之間的「譯者與尋人廣播」,用途是將 IP 位址翻譯為 MAC 位址並建立 ARP Table。ETHERTYPE_IPV4 (0x0800):正式的 Layer 3 網路層數據傳輸通道,裝載真正的 IPv4 標頭與高層數據(如 TCP/UDP/Ping)。src/tap.c 的 while(1) 迴圈中,依據 ntohs(eth->ethertype) 是否為 0x0806 分流至 arp_receive()。src/arp.c 的 arp_receive() 函式中:
ARP_REQUEST (Opcode = 1) ➔ 分流至 arp_handle_request()(判斷是否問自己並自動回送 ARP Reply)。ARP_REPLY (Opcode = 2) ➔ 分流至 arp_handle_reply()(寫入 ARP Table)。./send_arp_reply 時遇到 TUNSETIFF: Device or resource busy 該如何解決?/dev/net/tun 裝置同時只能被一個行程獨佔鎖定(即運作中的 network)。解決方案是將發送端改用 Linux Raw Socket (socket(AF_PACKET, SOCK_RAW, ...) ),直接經由 tap0 介面注入封包,無需重複開啟 /dev/net/tun。./send_arp_reply 時遇到 sendto: Network is down 該如何解決?tap0 網卡狀態預設為 DOWN。可透過命令 sudo ip link set tap0 up 切換為 UP,或在 src/tap.c 的 tun_alloc() 內呼叫 ioctl(SIOCSIFFLAGS) 自動將 IFF_UP | IFF_RUNNING 狀態拉起。network 執行時會收到很多 [DROP] Not for me?tap0 切換為 UP 狀態時,Linux 作業系統背景會自動經由該網卡發送維護封包(如 IPv6 NDP 多播封包,Destination MAC 為 33:33:xx:xx:xx:xx)。因為這些封包既不是全廣播 (ff:ff:ff:ff:ff:ff) 也不是我們的本機 MAC (02:00:00:00:00:01),因此被我們的 ethernet_accept_frame() 守門員正確判定為 Not for me 並過濾丟棄。這證明我們的 Layer 2 MAC 過濾器完全正常運作!arp_handle_reply() 嗎?[ARP] Reply Received 與底下的 ARP Table (10.0.0.1 -> 52:54:00:12:34:56) 就是 arp_handle_reply() 與 arp_table_dump() 100% 成功執行的結果。視窗一(執行主程式):
gcc -Iinclude src/tap.c src/ethernet.c src/arp.c src/arp_table.c -o network
sudo ./network
視窗二(執行測試發送端):
gcc -Iinclude tests/send_arp_reply.c src/ethernet.c src/arp.c src/arp_table.c -o send_arp_reply
sudo ./send_arp_reply
TAP device: tap0 (UP)
Ethernet header size: 14 bytes
Waiting for Ethernet frame...
[DROP] Not for me
[ACCEPT]
Ethernet Frame
-------------------------
Destination : 02:00:00:00:00:01
Source : 52:54:00:12:34:56
EtherType : 0x0806
[ARP] Reply Received
ARP Table
=========================
10.0.0.1 -> 52:54:00:12:34:56
Frame length: 42 bytes
明天我們將正式離開 Layer 2,跨入 Layer 3 (Network Layer):
Ethernet
│
├── ARP (0x0806) ───► ARP Table / ARP Reply (已完成)
│
└── IPv4 (0x0800) ───► 解析第一個真正的 IP 封包
我們會解析 IPv4 標頭欄位:Version, IHL, TTL, Protocol, Source IP, Destination IP,並建立完整的 IP 處理模組!