Networking
实验目标:
- 了解 Linux 内核网络体系结构
- 使用数据包过滤器或防火墙,学习 IP 数据包管理技能
- 熟悉如何在 Linux 内核级别使用套接字
互联网的发展导致网络应用的指数级增长,从而增加了操作系统网络子系统的速度和生产力要求
- 网络子系统不是操作系统内核的基本组件(Linux 内核可以在没有网络支持的情况下编译)
- 然而,计算系统(甚至是嵌入式设备)不太可能具有非网络操作系统
- 现代操作系统使用 TCP/IP 堆栈,由内核实现传输层的协议,而应用层协议通常在用户空间(HTTP,FTP,SSH等)中实现
Networking in user space
在用户空间中,网络通信的抽象是套接字 socket
套接字 socket 抽象了一个通信通道,是基于内核的 TCP/IP 堆栈交互接口(其实 TCP/IP 的底层就是发包,通过发包实现计算机网络之间的交互,而 socket 让发包变得更简单了)
Networking in Linux kernel
Linux 内核为处理网络数据包提供了三种基本结构:
struct socket
:
- 一个非常接近用户空间的抽象,即用于编程网络应用的 BSD 套接字
1 | struct socket { |
- 相关联的 API 如下:
1 | /* socket create/delete */ |
struct sock
:
- 在 Linux 术语中是套接字的网络表示形式,又称为 INET 套接字
- 该结构用于存储有关连接状态的信息
1 | struct sock { |
- 相关 API 如下:
1 | /* sending/receiving messages */ |
struct sk_buff
:
- 用于描述一个网络数据包及其状态
- 该结构是在用户空间或网络接口接收到内核数据包时创建的
1 | struct sk_buff { |
Conversions
在不同的系统中,有几种方法可以对单词中的字节进行排序(字节序),包括:
- Big Endian 大端
- Little Endian 小端
由于网络将系统与不同的平台互连,因此互联网已经为数字数据的存储强加了一个标准的顺序,被称为 network byte-order 网络字节顺序
- 网络字节序就是 Big Endian 大端字节序
- 对于转换大小端,我们使用以下宏:
1 | u16 htons(u16 x) /* 将16位整数从主机字节顺序转换为网络字节顺序 */ |
netfilter
网络过滤器 netfilter 是内核接口的名称,用于捕获网络数据包以修改/分析它们(用于过滤,NAT等)
用户空间通过 iptable 使用网络过滤器接口
在 Linux 内核中,使用网络过滤器的数据包捕获是通过附加钩子来完成的:
- 可以根据需要在路径中的不同位置指定钩子,后跟内核网络数据包
- 可以在此处找到组织结构图,其中包含路线后跟包裹以及钩子的可能区域
钩子 hook 是通过以下结构定义的:
1 | typedef unsigned int nf_hookfn(void *priv, |
- 钩子函数 hook 的签名中有一个
nf_hook_state
结构体,用于描述 hook 的状态信息,关键条目如下:
1 | struct nf_hook_state { |
相关 API 如下:
1 | int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops); /* 用于注册挂钩点 */ |
1 | int nf_register_net_hooks(struct net *net, const struct nf_hook_ops *reg, |
1 | int nf_register_net_hooks(struct net *net, const struct nf_hook_ops *reg, |
使用案例如下:
1 |
|
netcat
在开发包含网络代码的应用程序时,最常用的工具之一是 netcat(也被称为“用于 TCP/IP 的瑞士军刀”),它允许:
- 启动 TCP 连接
- 等待 TCP 连接
- 发送和接收 UDP 数据包
- 以十六进制转储格式显示流量
- 建立连接后运行程序(例如,shell)
- 在已发送的包中设置特殊选项
Exercises
要解决练习,您需要执行以下步骤:
- 从模板准备 skeletons
- 构建模块
- 将模块复制到虚拟机
- 启动 VM 并在 VM 中测试模块
1 | make clean |
1.netfilter:
- 编写一个内核模块,该模块显示启动出站连接的 TCP 数据包的源地址和端口
- 可以通过 MY_IOCTL_FILTER_ADDRESS ioctl 调用来指定目标地址
1 | /* |
- 结果:
1 | root@qemux86:~/skels/networking/1-2-netfilter/user# ./test-1.sh |
1 | root@qemux86:~/skels/networking/1-2-netfilter/user# ./test-2.sh |
- 使用
nc
命令时,hook 函数my_nf_hookfn
被执行了 - 当
my_ioctl
的 MY_IOCTL_FILTER_ADDRESS 命令执行以后,效验模块开启,由于ioctl_set_addr != iph->daddr
因此 hook 函数没有执行 - PS:当时眼瞎把
nf_hook_ops
初始化错了,导致后面一直报错,调试了很久
2.tcp-sock:
- 编写一个内核模块,该模块创建一个 TCP 套接字,该套接字侦听环回接口上的端口 60000 上的连接(以 init_module 为单位)
1 | /* |
- PS:这里的
sock->ops->bind
sock->ops->listen
sock->ops->accept
就是用户态同名函数的底层实现 - 结果:
1 | root@qemux86:~/skels/networking/3-4-tcp-sock# ./test-4.sh |
- 成功监听了目标端口
3.udp-sock:
- 编写一个内核模块,用于创建 UDP 套接字,并将消息从套接字上的 MY_TEST_MESSAGE 宏发送到端口 60001 上的环回地址
1 | /* |
- 本实验算一个比较套路的过程,把
msghdr
和iovec
填充好就行了 - 结果:
1 | root@qemux86:~/skels/networking/5-udp-sock# ./test-5.sh |
- 感觉本实验的侧重点是对协议栈 API 的使用
- 之后有时间去专门分析一下这些 API 的底层,了解一下协议堆栈具体的处理过程