Skip to content

Commit b7e956a

Browse files
chore: new article written
1 parent 62fac8f commit b7e956a

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
title: "深入理解并实现基本的 IPv6 协议栈"
3+
author: "李睿远"
4+
date: "Oct 16, 2025"
5+
description: "从数据包结构到代码实现"
6+
latex: true
7+
pdf: true
8+
---
9+
10+
从数据包结构到代码实现,亲手构建网络核心
11+
12+
13+
随着互联网的快速发展,IPv4 地址的枯竭已成为不争的事实。IPv4 仅提供约 43 亿个地址,而全球设备数量早已远超这一数字,导致地址分配紧张和各种过渡技术的出现。相比之下,IPv6 采用 128 位地址长度,理论上可提供 $2^{128}$ 个地址,这一数量级足以满足未来数十年的需求。根据最新数据,全球 IPv6 部署率持续上升,许多大型网络和服务商已全面支持 IPv6。因此,对于现代开发者和网络工程师而言,深入理解 IPv6 不再是可选技能,而是必备知识。
14+
15+
IPv6 的核心优势远不止于地址空间的扩展。其报头格式经过简化,固定为 40 字节,去除了 IPv4 中的可选字段,转而使用扩展报头链式处理,这大大提升了路由效率。此外,IPv6 内置了对 IPsec 的支持,增强了端到端的安全性,同时更好地支持移动性和无状态地址自动配置(SLAAC),使得设备能够快速接入网络。本文旨在通过理论与实践相结合的方式,引导读者从零开始实现一个用户态的 IPv6 协议栈。我们将聚焦于 IPv6 层、ICMPv6 和邻居发现协议(NDP),暂不涉及 TCP/UDP 等上层协议,最终目标是实现一个能响应 Ping(ping6)并进行邻居发现的微型协议栈。
16+
17+
## 理论基础:深入剖析 IPv6 核心机制
18+
19+
IPv6 数据包结构是理解其设计理念的基础。IPv6 基本报头包含多个字段:版本(Version)固定为 6,流量类别(Traffic Class)用于 QoS,流标签(Flow Label)标识特定流,载荷长度(Payload Length)指示扩展报头和数据的总长度,下一个报头(Next Header)指定后续内容类型,跳数限制(Hop Limit)类似 IPv4 的 TTL,以及源和目的地址各 128 位。与 IPv4 报头相比,IPv6 报头更加简化,去除了校验和、分片等相关字段,转而依赖上层协议处理。扩展报头以链式结构存在,例如 Hop-by-Hop Options 和 ICMPv6(下一个报头值为 58),它们允许灵活地添加功能而不改变基本结构。
20+
21+
ICMPv6 在 IPv6 中扮演着多重角色,不仅是错误报告和网络诊断的工具,更是邻居发现协议(NDP)的载体。NDP 替代了 IPv4 中的 ARP,用于解析 IPv6 地址到 MAC 地址的映射。关键报文类型包括 Echo Request 和 Echo Reply 用于实现 ping6,Neighbor Solicitation 和 Neighbor Advertisement 用于地址解析,以及 Router Solicitation 和 Router Advertisement 用于无状态地址自动配置。NDP 的状态机涉及多个状态:INCOMPLETE、REACHABLE、STALE、DELAY 和 PROBE,它们根据超时和事件进行转换,确保邻居关系的有效管理。
22+
23+
在地址体系方面,IPv6 定义了单播、组播和任播地址。单播地址包括全球单播(如 2000::/3)和链路本地地址(fe80::/10),其中链路本地地址在 NDP 通信中至关重要。无状态地址自动配置(SLAAC)允许设备通过接收 Router Advertisement 报文并结合 EUI-64 格式生成接口标识符,自动配置全球单播地址,这简化了网络部署过程。
24+
25+
## 实战准备:搭建开发环境与架构设计
26+
27+
在技术选型上,我们推荐使用 C 语言进行实现,因为它贴近系统底层,能够直接操作内存和网络接口,适合协议栈开发。开发平台选择 Linux,因其原生支持 TUN/TAP 设备,允许用户态程序模拟网络接口。关键工具包括 tcpdump 或 Wireshark 用于抓包分析,ping6 和 traceroute6 用于测试,而 TUN/TAP 设备则是核心,它通过字符设备接口提供原始数据包的读写能力。
28+
29+
协议栈的软件架构应模块化设计。TUN 驱动模块负责从 TUN 设备读取和写入原始以太网帧;以太网帧处理模块解析帧类型,识别 IPv6 数据包(以太网类型 0x86DD);IPv6 解包/组包模块处理基本报头和扩展报头;ICMPv6 处理模块响应 Echo 请求和邻居发现报文;邻居缓存表存储 IPv6 地址到 MAC 地址的映射及其状态;定时器模块处理 NDP 状态超时和重传。数据流从 TUN 设备进入,经以太网帧解析后,如果是 IPv6 包,则递交给 IPv6 模块,再根据下一个报头调用 ICMPv6 模块,最终可能更新邻居缓存或返回响应。
30+
31+
## 代码实现:构建核心模块
32+
33+
首先,我们搭建项目骨架并初始化 TUN 接口。以下代码展示如何在 C 语言中创建和配置 TUN 设备。
34+
35+
```c
36+
#include <fcntl.h>
37+
#include <linux/if.h>
38+
#include <linux/if_tun.h>
39+
#include <sys/ioctl.h>
40+
#include <unistd.h>
41+
#include <string.h>
42+
43+
int tun_alloc(char *dev) {
44+
struct ifreq ifr;
45+
int fd, err;
46+
if ((fd = open("/dev/net/tun", O_RDWR)) < 0) {
47+
return -1;
48+
}
49+
memset(&ifr, 0, sizeof(ifr));
50+
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
51+
if (dev) {
52+
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
53+
}
54+
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
55+
close(fd);
56+
return err;
57+
}
58+
strcpy(dev, ifr.ifr_name);
59+
return fd;
60+
}
61+
```
62+
63+
这段代码通过打开 `/dev/net/tun` 设备文件并使用 ioctl 调用配置 TUN 接口。IFF_TUN 标志表示创建 TUN 设备(三层网络设备),IFF_NO_PI 表示不提供包信息,从而简化数据包处理。函数返回文件描述符,用于后续读写操作。初始化后,程序可以进入数据包读写循环,不断从文件描述符读取原始数据包并进行处理。
64+
65+
接下来,实现以太网帧与 IPv6 报文的解析。我们定义相关结构体并处理字节序转换。
66+
67+
```c
68+
#include <arpa/inet.h>
69+
#include <stdint.h>
70+
71+
struct ethhdr {
72+
unsigned char h_dest[6];
73+
unsigned char h_source[6];
74+
uint16_t h_proto;
75+
};
76+
77+
struct ip6_hdr {
78+
uint32_t v_tc_fl;
79+
uint16_t plen;
80+
uint8_t nxt;
81+
uint8_t hlim;
82+
struct in6_addr src;
83+
struct in6_addr dst;
84+
};
85+
86+
void parse_ethernet(unsigned char *buffer, int length) {
87+
struct ethhdr *eth = (struct ethhdr *)buffer;
88+
uint16_t proto = ntohs(eth->h_proto);
89+
if (proto == 0x86DD) {
90+
parse_ipv6(buffer + sizeof(struct ethhdr), length - sizeof(struct ethhdr));
91+
}
92+
}
93+
```
94+
95+
在这段代码中,我们定义了以太网帧头和 IPv6 报头的结构体。`ethhdr` 包含目的和源 MAC 地址以及协议类型字段;`ip6_hdr` 使用组合字段处理版本、流量类别和流标签,注意这些字段需要字节序转换,例如 `ntohs` 用于将网络字节序转换为主机字节序。在 `parse_ethernet` 函数中,我们检查协议类型是否为 IPv6(0x86DD),如果是,则调用 IPv6 解析函数。IPv6 报头解析需要校验版本字段是否为 6,并处理载荷长度以确保数据完整性。
96+
97+
现在,实现 ICMPv6 协议处理,首先是响应 Ping 请求。
98+
99+
```c
100+
struct icmp6_hdr {
101+
uint8_t type;
102+
uint8_t code;
103+
uint16_t checksum;
104+
};
105+
106+
void handle_icmp6(unsigned char *buffer, int length, struct in6_addr src, struct in6_addr dst) {
107+
struct icmp6_hdr *icmp6 = (struct icmp6_hdr *)buffer;
108+
if (icmp6->type == 128) {
109+
icmp6->type = 129;
110+
struct in6_addr tmp = src;
111+
src = dst;
112+
dst = tmp;
113+
icmp6->checksum = 0;
114+
icmp6->checksum = compute_checksum(src, dst, buffer, length);
115+
send_packet(buffer, length);
116+
}
117+
}
118+
```
119+
120+
这里,我们定义 ICMPv6 报头结构,类型字段为 128 表示 Echo Request,129 表示 Echo Reply。处理时,我们改变类型,交换源和目的地址,并重新计算校验和。校验和计算涉及 IPv6 伪首部,包括源地址、目的地址、载荷长度和下一个报头值。计算函数 `compute_checksum` 需要实现,它基于标准算法,对伪首部和 ICMPv6 报文进行求和。
121+
122+
校验和计算是 ICMPv6 的关键部分。IPv6 的校验和使用伪首部,其结构包括源地址(16 字节)、目的地址(16 字节)、上层包长度(32 位)、下一个报头(8 位,后跟 24 位零)。公式可表示为:校验和 = ~(sum(伪首部) + sum(ICMPv6 报文)),其中 sum 是 16 位字的补码和。在代码中,我们需要遍历这些数据并计算。
123+
124+
```c
125+
uint16_t compute_checksum(struct in6_addr src, struct in6_addr dst, unsigned char *data, int len) {
126+
uint32_t sum = 0;
127+
for (int i = 0; i < 16; i += 2) {
128+
sum += (src.s6_addr[i] << 8) | src.s6_addr[i+1];
129+
}
130+
for (int i = 0; i < 16; i += 2) {
131+
sum += (dst.s6_addr[i] << 8) | dst.s6_addr[i+1];
132+
}
133+
sum += len;
134+
sum += 58;
135+
for (int i = 0; i < len; i += 2) {
136+
if (i+1 < len) {
137+
sum += (data[i] << 8) | data[i+1];
138+
} else {
139+
sum += data[i] << 8;
140+
}
141+
}
142+
while (sum >> 16) {
143+
sum = (sum & 0xFFFF) + (sum >> 16);
144+
}
145+
return ~sum;
146+
}
147+
```
148+
149+
这段代码实现了校验和计算。我们首先处理伪首部:源和目的地址各作为 8 个 16 位字处理,上层包长度作为 32 位值但以 16 位字形式添加,下一个报头值 58 也作为 16 位字添加。然后处理 ICMPv6 报文数据,以 16 位为单位求和,并处理进位。最终返回补码。注意,在实现中,我们假设数据是网络字节序,且长度参数 `len` 是 ICMPv6 报文的字节长度。
150+
151+
## 实现邻居发现协议
152+
153+
邻居发现协议(NDP)是 IPv6 的核心组件,用于解析链路层地址。我们首先设计邻居缓存表,存储 IPv6 地址到 MAC 地址的映射及其状态。
154+
155+
```c
156+
#include <time.h>
157+
158+
#define ND6_INCOMPLETE 0
159+
#define ND6_REACHABLE 1
160+
161+
struct neighbor_cache {
162+
struct in6_addr ip6_addr;
163+
unsigned char mac_addr[6];
164+
int state;
165+
time_t last_updated;
166+
};
167+
168+
struct neighbor_cache ncache[100];
169+
int ncache_size = 0;
170+
171+
void handle_neighbor_solicitation(unsigned char *buffer, int length, struct in6_addr src, struct in6_addr dst) {
172+
struct icmp6_ns *ns = (struct icmp6_ns *)buffer;
173+
if (is_my_address(ns->target)) {
174+
send_neighbor_advertisement(ns->target, my_mac_addr);
175+
}
176+
}
177+
178+
void send_neighbor_solicitation(struct in6_addr target) {
179+
}
180+
```
181+
182+
在这段代码中,我们定义了邻居缓存表的结构,包括 IPv6 地址、MAC 地址、状态和时间戳。`handle_neighbor_solicitation` 函数处理收到的 NS 报文,如果目标地址匹配本机地址,则发送 NA 响应。发送 NA 时,需要设置相关标志,如覆盖位(O)和请求位(S),以指示响应的性质。同时,当协议栈需要发送数据包但缓存中无对应条目时,应主动发送 NS 报文,并将状态设置为 INCOMPLETE,等待 NA 响应后更新为 REACHABLE。
183+
184+
## 测试与验证
185+
186+
搭建测试环境时,需要将协议栈程序与主机网络连接。通过配置 TUN 设备并设置路由,使主机的 IPv6 流量指向该设备。例如,在 Linux 中,可以使用 `ip link set dev tun0 up` 和 `ip route add local 2001:db8::/64 dev tun0` 等命令。
187+
188+
功能测试包括 Ping 测试和邻居发现测试。从主机执行 `ping6 fe80::1%tun0`(假设协议栈配置了链路本地地址),同时使用 Wireshark 抓包验证请求和回复报文。此外,通过 `ip -6 neighbor show` 命令观察邻居表,确认协议栈的条目出现并达到 REACHABLE 状态。抓包结果应显示完整的 NS/NA 和 Echo 交互过程:例如,当发送 Ping 请求时,Wireshark 应显示 Echo Request 和 Echo Reply 报文,且邻居发现报文应显示 NS 和 NA 的交换,确保地址解析成功。
189+
190+
191+
通过本文的实现,我们构建了一个具备 IPv6 基本报头处理、ICMPv6 Echo 响应和邻居发现功能的用户态协议栈。这个过程不仅加深了对 IPv6 协议的理解,还展示了如何将理论转化为实践。未来,可以扩展支持更多扩展报头如分片、实现 DHCPv6 客户端、添加 TCP/UDP 上层协议支持,以及优化性能和多线程处理。网络协议的设计精巧而实用,亲手实现是掌握它的最佳途径。鼓励读者尝试实现,并参考相关开源项目进一步探索。

0 commit comments

Comments
 (0)