skaiuijing

前言

有人说,学网络就是学一堆协议。有人说,学网络就是学习数据包的处理。

也有人说,学习用不上的底层内容,本身就没什么意义。

这句话其实很对,但是也不对。

在搞清楚学什么之前,我们需要搞清楚什么是对,什么是不对。

为什么一个观点既是对,也是不对?在辩证法看来,纯粹的对与纯粹的不对,都是空有形式而无内容的概念,那么纯粹的对就是纯粹的不对。但是,万事万物都无法做到纯粹,所以事物本身的矛盾是内部蕴含的常态。

万事万物皆有矛盾,但是矛盾的目的不是摧毁一件事物,而是让事物内部激化,显现出事物真实的面貌,这是一个螺旋式的过程。

在这个螺旋式的过程中,起点即是终点,一个事情的对,必须要有不对进行对比,只有知道了不对,才能知道什么是对。

那么,在对的螺旋式上升的过程,却是不对的螺旋式下降的过程。对与不对,从一开始毫无内容时,它们互为本身,而当它们各自因矛盾激化而分离时,却是朝着对方的另一种姿态前进的过程。

所以,就有了一种正反合的过程。对与不对,在这个过程相互映照,相互升华。因此,我们可以看见,一个事物的终点必须会回归其起点。

所以,在带领读者踏上网络学习之旅前,请读者明白学习的原因,也搞清楚学习的目的。盲目的学习是浪费时间,不假思索的学习是浪费生命。

学习的过程不可能是坚定的,一定是有矛盾的。不管矛盾来源于学习内容是否有意义,还是来源于学习本身是否有意义,不管是坚定有意义而学习,还是批评无意义的学习,它们都是学习的一部分。

教程

本系列教程内容大概分为两部分,内容穿插描述。

1.TCP/IP 协议栈的实现原理,在这个过程中,笔者会介绍一个玩具 TCP/IP 网络协议栈的实现代码,这个协议栈是笔者以前参考 FreeBSD4.4 源码写的,虽然 TCP 部分没有写完,不过作为学习材料确实是够了。因此,笔者在这一部分重点介绍的是网络协议栈的实现。

2.Linux 网络子系统,笔者会简单介绍 Linux 网络的设计,比如数据包结构以及 epoll 的实现原理,可能大部分人了解其中有棵红黑树,但是具体实现没有细看。

笔者会简单介绍 epoll 运行高效的原因,同时,也会介绍 wireshark 与 tcpdump 这些工具的实现原理。

当然,Linux 网络子系统是本系列的重点内容,TCP/IP 协议栈的实现只是为了辅助了解。

在讲解 Linux 网络时,笔者的重点是 Linux 网络性能优化,因此,会介绍 eBPF 技术。笔者将会讲解如何使用 eBPF 技术调试并观察网络。

eBPF 是一种内核动态编程技术,笔者会简单介绍使用 XDP,TC,cgroup 等 hook 点解析网络数据包,例如结合 qdisc、tc 子系统、重定向等技术实现网络监控、负载均衡、限流、零拷贝等功能,优化网络性能。当然,这一部分属于扩展内容了。

关于第一部分,TCP/IP 协议栈的实现,笔者会给出具体的指导,包括如何搭建网卡收发环境,如何实现 arp,icmp,ip,路由,udp,tcp 等协议,以及 socket 层。

但是关于 eBPF 技术及 Linux 网络的 hook 编程,可能就要留给感兴趣的读者自行探索了,因此笔者只进行简单介绍。

网络概括

img

添加图片注释,不超过 140 字(可选)

对于 TCP/IP 网络,我们重点关注的在于 IP 与 TCP,这是网络的核心。

网络的架构是分层的,因此,笔者可能还要写一下网络的架构设计与现代操作系统网络子系统的架构设计。

搭建环境

如果读者手上有网卡,可以尝试编写硬件驱动,只要能实现收发数据即可。当然,使用现成的硬件并编写驱动还是有些麻烦。

笔者这里就简单介绍使用 TUN/TAP 虚拟网卡模拟网络接口,这种方式可以允许用户态直接读写数据。

使用 Linux 虚拟网卡

TUN/TAP 是 Linux 提供的虚拟网络设备,允许用户态程序与内核网络栈交互:

类型 模拟层级 数据类型
TUN 第三层(IP 层) IP 数据包
TAP 第二层(链路层) 以太网帧

本教程使用 TAP 用于发送和接收原始以太网帧,笔者使用的操作系统是ubuntu,对版本没有多少要求,16.04或者18.04都可以。但是如果读者想尝试eBPF技术,建议使用ubuntu25.04这些具有比较新的Linux内核的版本。

创建命令

1
2
3
4
5
6
7
8
9
10
11
# 创建 TAP 设备并分配给当前用户
sudo ip tuntap add dev tap0 mode tap user $(whoami)

# 分配 IP 地址(可选)
sudo ip addr add 192.168.1.1/24 dev tap0

# 启用接口
sudo ip link set tap0 up

# 查看接口状态
ip addr show tap0

也可以使用 ifconfig tap0 查看设备信息,确认设备已启用并具备 MAC 和 IP:

当出现下面这些信息时,虚拟网卡已经创建成功了,例如:

1
2
3
tap0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.1.1 netmask 255.255.255.0 broadcast 192.168.1.255
ether 4a:3b:2c:1d:00:01 txqueuelen 1000 (Ethernet)

用户态初始化 TAP 接口代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int fd;
int tapif_init() {
struct ifreq ifr;
int err;

// 打开 /dev/net/tun 设备
if ((fd = open(「/dev/net/tun」, O_RDWR)) < 0) {
perror(「can『t open /dev/net/tun」);
return fd;
}

// 配置 TAP 接口参数
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TAP | IFF_NO_PI; // TAP 模式,不附加协议信息
strncpy(ifr.ifr_name, 「tap0」, IFNAMSIZ); // 绑定到 tap0 接口

// 应用配置
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
perror(「ioctl(TUNSETIFF)」);
close(fd);
return err;
}

return fd;
}

数据收发函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 从 TAP 接口读取以太网帧
int tap_read(char *buf, size_t bufsize) {
int len = read(fd, buf, bufsize);
if (len < 0) {
perror(「read from TAP」);
return -1;
}
printf(「Read %d bytes from TAP\n」, len);
return len;
}

// 向 TAP 接口写入以太网帧
int tap_write(const char *buf, size_t len) {
int written = write(fd, buf, len);
if (written < 0) {
perror(「write to TAP」);
return -1;
}
printf(「Wrote %d bytes to TAP\n」, written);
return written;
}

对于网卡所在的接口层,我们关心的是实现两个接口,一个读取、一个写入。所以,不管你使用硬件网卡还是虚拟网卡,本质上对网络协议栈的实现都无所谓。

测试:发送 ARP 请求帧

ARP,是一种IP层用来获取目标mac的协议,至于下面的数组为什么这么写,读者不必在意,在后面的章节笔者会介绍的,这里只是简单验证搭建环境是否成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main() {
fd = tapif_init();

char arp_request[42] = {
// 以太网头
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 目标 MAC:广播
0x00, 0x0c, 0x29, 0xab, 0xcd, 0xef, // 源 MAC:自定义
0x08, 0x06, // 类型:ARP (0x0806)

// ARP Header
0x00, 0x01, // 硬件类型:以太网
0x08, 0x00, // 协议类型:IPv4
0x06, // MAC 长度
0x04, // IP 长度
0x00, 0x01, // 操作码:请求

// Sender MAC
0x00, 0x0c, 0x29, 0xab, 0xcd, 0xef,
// Sender IP
0xc0, 0xa8, 0x00, 0x01, // 192.168.0.1

// Target MAC(未知)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Target IP
0xc0, 0xa8, 0x00, 0x02 // 192.168.0.2
};

tap_write(arp_request, sizeof(arp_request));
}

验证

使用 tcpdump 抓取 TAP 接口上的数据:

1
sudo tcpdump -i tap0

输出示例:

1
ARP, Request who-has 192.168.0.2 tell 192.168.0.1, length 42

这表示你成功发送了一帧 ARP 请求,询问“谁拥有 IP 地址 192.168.0.2”,并声明自己是 192.168.0.1。

结语

网络协议栈的开篇就写到这里,在下面几章,笔者将介绍 buf 结构与零拷贝等技术,并介绍每个协议数据包的组成。

由于笔者的重点在于网络本身,因此,笔者可能会花几篇内容介绍 Linux 网络数据包 sk_buff,并介绍tcpdump的原理,以及如何使用 eBPF 技术在数据包进入内核网络协议栈时进行解析,从而达到网络监控的目的,其实,使用eBPF进行解析数据包,本质上也是在实现tcpdump的功能。