這個系列是我報名 2026 iThome 鐵人賽 Build on Google AI 組別的 30 天實作挑戰。
我的目標不是只讀懂網路概念,而是親手實作一條從瀏覽器一路通往封包底層的完整路徑:
Web Browser
↓
HTTP
↓
TCP
↓
IPv4
↓
Ethernet
↓
TAP Device
↓
My Network Stack
在這個系列中,我會使用 Google Gemini 作為 AI 教授,請它協助規劃每天的課程內容、學習目標與實作方向;完成每日實作後,我會再回報成果,讓 Gemini 協助銜接下一階段的學習。
同時,我也會使用 Antigravity CLI 作為程式碼助教,協助審查 C 語言實作、檢查封包處理邏輯,並把大量 debug 與 code review 細節留在助教上下文中,避免污染 Gemini 的主要教學脈絡。
每天的文章都會盡量包含:
這個系列的最終目標,是透過 30 天的實作,逐步理解資料如何從應用層一路被封裝成封包,經過 TCP/IP、Ethernet 與 TAP Device,最後由自己寫的 Network Stack 進行處理。
比起只記住各層協定名稱,我更希望透過實作建立一條完整的網路理解路徑:從 Bytes、封包格式、協定行為,到網路應用背後的資料流動。
完成以下內容:
[x] 建立並啟用 tap0
[x] 使用 C 語言開啟 /dev/net/tun
[x] 將程式綁定至 tap0
[x] 透過 read() 接收 Linux 核心送出的 Ethernet Frame
[x] 理解 Ethernet Frame 的基本結構
在真正開始撰寫網路協定之前,我們需要先建立一條能夠讓 Linux 核心與我們的程式互相傳送封包的「虛擬網路線」。
這條虛擬網路線,就是 TAP Device。

實體網卡負責:
流程如下:
網路線
↓
實體網卡
↓
Linux Kernel
TAP 網卡不存在任何實體硬體。
它是 Linux 核心建立出來的虛擬網路介面,而介面的另一端直接連接到我們的程式。
流程如下:
Linux Kernel
↓
tap0
↓
我們的程式
因此:
從核心的角度來看,我們的程式就像是一張真正存在的網路卡。
| 裝置類型 | 提供給程式的資料層級 | 內容 | 適用場景 |
|---|---|---|---|
| TUN | Layer 3(Network Layer) | IP Packet | VPN、IP Tunnel |
| TAP | Layer 2(Data Link Layer) | Ethernet Frame(含 MAC Header) | 虛擬機、自製網路協定堆疊 |
因為我們要親手實作完整網路堆疊:
Application
↓
TCP / UDP
↓
IPv4
↓
ARP
↓
Ethernet
若使用 TUN:
IP Packet
↓
程式
Ethernet 與 ARP 都會被 Linux 核心隱藏。
而使用 TAP:
Ethernet Frame
↓
程式
我們可以從 Layer 2 開始實作,真正理解整個網路協定的運作方式。
建立一張名為 tap0 的虛擬網卡。
# 建立 TAP 裝置
sudo ip tuntap add dev tap0 mode tap
# 指定 IP 位址
sudo ip addr add 10.0.0.1/24 dev tap0
# 啟用網卡
sudo ip link set tap0 up
# 檢查狀態
ip addr show tap0
建立完成後,可以看到:

此時 Linux 已經認為系統中存在一張名為 tap0 的網路卡。
接下來撰寫程式,與 Linux 核心建立連線。
檔案:
src/tap.c
#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>
int tun_alloc(char *dev)
{
struct ifreq ifr;
int fd;
// 開啟 TUN/TAP 核心驅動
fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
perror("open /dev/net/tun");
exit(1);
}
memset(&ifr, 0, sizeof(ifr));
// TAP 模式 + 不附加額外資訊
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
if (dev && *dev) {
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
}
// 綁定 tap0
if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0) {
perror("ioctl TUNSETIFF");
close(fd);
exit(1);
}
strcpy(dev, ifr.ifr_name);
return fd;
}
int main()
{
char dev[IFNAMSIZ] = "tap0";
unsigned char buffer[2048];
int fd = tun_alloc(dev);
printf("TAP device: %s\n", dev);
printf("Waiting for Ethernet frame...\n");
while (1) {
int n = read(fd, buffer, sizeof(buffer));
if (n < 0) {
perror("read");
break;
}
printf("Received %d bytes\n", n);
printf("First bytes: ");
for (int i = 0; i < n && i < 32; i++) {
printf("%02x ", buffer[i]);
}
printf("\n");
}
close(fd);
return 0;
}
gcc src/tap.c -o tap
sudo ./tap
執行後:

此時程式已經進入封包接收狀態。
打開另一個終端機:
ping 10.0.0.2
此時 Linux 核心會嘗試尋找:
10.0.0.2 對應的 MAC Address
因此送出 ARP Request。
我們的程式便能接收到第一個 Ethernet Frame。
/dev/net/tun?為什麼不是:
/dev/tap0
或
/dev/tap
?
原因是 Linux 將 TUN 與 TAP 統一交由同一個核心驅動管理:
tun.ko
所有程式都先開啟:
/dev/net/tun
接著再透過:
ioctl(fd, TUNSETIFF, &ifr);
指定:
IFF_TAP
告訴核心:
我要使用 TAP 模式,並綁定到 tap0。
因為:
10.0.0.1
是 tap0 自己的 IP。
若執行:
ping 10.0.0.1
Linux 會直接在核心內部處理。
Ping
↓
Kernel
↓
回覆
完全不會經過 tap0。
因此我們的程式收不到任何封包。
當執行:
ping 10.0.0.2
時:
Linux Kernel
↓
需要查詢 MAC
↓
發送 ARP Request
↓
送到 tap0
↓
我們的程式收到
所以才能看到 Ethernet Frame。
可以。
例如:
sudo ip addr add 10.0.0.10/24 dev tap0
此時:
ping 10.0.0.2
仍然會收到 ARP Request。
因為:
10.0.0.10/24
與
10.0.0.2
同樣位於:
10.0.0.0/24
網段內。
Linux 仍然需要透過 ARP 查詢對方 MAC Address,因此會送出廣播封包。
今天完成了整個網路堆疊的第一步:
Linux Kernel
↓
TAP Device
↓
C Program
我們已經能夠:
這代表我們正式取得了網路世界最底層的資料流控制權。
明天我們將實作:
Ethernet Frame Parser
把目前看到的 Raw Bytes:
ff ff ff ff ff ff
52 54 00 12 34 56
08 06
...
解析成真正可閱讀的資料結構:
Destination MAC
Source MAC
EtherType
Payload
正式踏入 Ethernet Protocol 的世界。