本文转载自 骏马金龙
物理网卡收发数据的流程
物理网卡
可以接收和发送数据:
- 收:外界向该
物理网卡
发送数据时,外界发送到网卡的数据最终会传输到内核空间
的网络协议栈
中 - 发:本机要从
物理网卡
发送数据时,数据将从内核的网络协议栈
传输到网卡,网卡负责将数据发送出去 - 现在的网卡具备
DMA
能力,所以网卡
和网络协议栈
之间的数据传输由网卡
负责,而非由内核亲自占用CPU
来执行读和写
这里可以将内核看作是一个封闭的加工厂,将物理网卡
看作是加工厂的一扇门,门的一端是加工厂,门的另一端是外界。物理网卡
也一样,它的一端是内核空间
的网络协议栈
,另一端是外界网络,物理网卡
就是这两者之间以比特流方式收发数据的硬件设备。
一般来说,数据的起点和终点是用户程序
,所以多数时候的数据需要在用户空间
和内核空间
(网络协议栈) 再传输一次:
- 当用户进程的数据要发送出去时,数据从
用户空间
写入内核的网络协议栈
,再从网络协议栈传输到网卡,最后发送出去 - 当用户进程等待外界响应数据时,数据从网卡流入,传输至内核的
网络协议栈
,最后数据写入用户空间
被用户进程读取
在这些过程中,内核和用户空间
的数据传输由内核占用 CPU
来完成,内核和网卡之间的数据传输由网卡的 DMA
来完成,不需要占用过多的 CPU
。
虚拟网卡设备
物理网卡
需要通过网卡驱动在内核中注册后才能工作,它在内核网络协议栈
和外界网络之间传递数据,用户可以为物理网卡
配置网卡接口属性,比如 IP 地址,这些属性都配置在内核的网络协议栈
中。
内核也可以直接创建虚拟网卡
,只要为虚拟网卡
提供网卡驱动程序,使其在内核中可以注册成为网卡设备,它就可以工作。
其实,从 Linux
内核 3.x
版本开始,物理网卡
和虚拟网卡
是平等的设备,它们都会在注册时创建 net_device
数据结构来保存 (物理或虚拟) 设备信息。
相比于物理网卡
负责内核网络协议栈
和外界网络之间的数据传输,虚拟网卡
的两端则是内核网络协议栈
和用户空间
,它负责在内核网络协议栈
和用户空间
的程序之间传递数据:
- 发送到
虚拟网卡
的数据来自于用户空间
,然后被内核读取到网络协议栈
中 - 内核写入
虚拟网卡
准备通过该网卡发送的数据,目的地是用户空间
虚拟网卡和物理网卡的对比
物理网卡
是硬件网卡,它位于硬件层,虚拟网卡
则可以看作是用户空间
的网卡,就像用户空间
的文件系统 (fuse) 一样。
物理网卡
和虚拟网卡
唯一的不同点在于物理网卡
本身的硬件功能:物理网卡
以比特流的方式传输数据。
也就是说,内核会公平对待物理网卡
和虚拟网卡
,物理网卡
能做的配置,虚拟网卡
也能做。比如可以为虚拟网卡
接口配置 IP 地址、设置子网掩码,可以将虚拟网卡
接入网桥等等。
只有在数据流经物理网卡
和虚拟网卡
的那一刻,才会体现出它们的不同,即传输数据的方式不同:
物理网卡
以比特流的方式传输数据虚拟网卡
则直接在内存中拷贝数据 (即,在内核之间和读写虚拟网卡
的程序之间传输)
正因为虚拟网卡
不具备物理网卡
以比特流方式传输数据的硬件功能,所以,绝不可能通过虚拟网卡
向外界发送数据,外界数据也不可能直接发送到虚拟网卡
上。能够直接收发外界数据的,只能是物理设备。
虽然虚拟网卡
无法将数据传输到外界网络,但却:
- 可以将数据传输到本机的另一个网卡 (
虚拟网卡
或物理网卡
) 或其它虚拟设备 (如虚拟交换机) 上 - 可以在
用户空间
运行一个可读写虚拟网卡
的程序,该程序可将流经虚拟网卡
的数据包进行处理,这个用户程序就像是物理网卡
的硬件功能一样,可以收发数据 (可将物理网卡
的硬件功能看作是嵌入在网卡上的程序),比如OpenVPN
就是这样的工具
很多人会误解这样的用户空间
程序,认为它们可以对数据进行封装。比如认为 OpenVPN
可以在数据包的基础上再封装一层隧道 IP 首部,但这种理解是错的。
一定请注意,系统用户空间
的程序是无法对数据包做任何封装和解封操作的,所有的封装和解封都只能由内核的网络协议栈
来完成。
使用 OpenVPN
之所以可以对数据再封装一层隧道 IP 层,是因为 OpenVPN
可以读取已经封装过一次 IP 首部的数据,并将包含 IP 首部的数据作为普通数据通过虚拟网卡
再次传输给内核。
因为内核接收到的是来自虚拟网卡
的数据,所以内核会将其当作普通数据从头开始封装 (从四层封装到二层封装)。当数据从网络协议栈
流出时,就有了两层 IP 首部的封装。
换句话说,每一次看似由用户空间
程序进行的额外封装,都意味着数据要从内核空间
到用户空间
,再到内核空间
。以 OpenVPN
为例:
tcp/ip stack --> tun --> OpenVPN --> tcp/ip stack --> Phyical NIC
其中 tun
是 OpenVPN
创建的一个三层虚拟网卡
,tun
设备在用户空间
和内核空间
之间传递数据。
虚拟网卡设备 tun/tab
tun
、tap
是Linux
提供的两种可收发数据的虚拟网卡
设备。tun
、tap
作为虚拟网卡
,除了不具备物理网卡
的硬件功能外,它们和物理网卡
的功能是一样的,此外tun
、tap
负责在内核网络协议栈
和用户空间
之间传输数据。
tun 或 tap 的区别
tun
和 tap
都是虚拟网卡
设备,但是:
tun
是三层设备,其封装的外层是 IP 头tap
是二层设备,其封装的外层是以太网帧 (frame) 头tun
是 PPP 点对点设备,没有 MAC 地址tap
是以太网设备,有 MAC 地址tap
比tun
更接近于物理网卡
,可以认为,tap
设备等价于去掉了硬件功能的物理网卡
这意味着,如果提供了用户空间
的程序去收发 tun/tap
虚拟网卡的数据,所收发的内容是不同的:
- 收发
tun
设备的用户程序,只能间接提供封装和解封数据包的 IP 头的功能 - 收发
tap
设备的用户程序,只能间接提供封装和解封数据包的帧头的功能
注意,此处用词是【收发数据】而非【处理数据】,是【间接提供】而非【直接提供】,因为在不绕过内核网络协议栈
的情况下,读写虚拟网卡
的用户程序是不能封装和解封数据的,只有内核的网络协议栈
才能封装和解封数据。
前面说过,虚拟网卡
的两个主要功能是:
- 连接其它设备 (虚拟网卡或物理网卡) 和虚拟交换机 (bridge)
- 提供用户空间程序去收发虚拟网卡上的数据
基于这两个功能,tap
设备通常用来连接其它网络设备 (它更像网卡),tun
设备通常用来结合用户空间程序实现再次封装。换句话说,tap
设备通常接入到虚拟交换机 (bridge) 上作为局域网的一个节点,tun
设备通常用来实现三层的 ip 隧道。
但 tun/tap
的用法是灵活的,只不过上面两种使用场景更为广泛。例如,除了可以使用 tun
设备来实现 IP 层隧道,使用 tap
设备实现二层隧道的场景也颇为常见。
创建并使用 tun/tap 设备
使用命令创建 tun
、tap
设备的方式有多种,比如 openvpn --mktun、ip tuntap、tunctl
等。
openvpn --mktun --dev tun0
openvpn --mktun --dev tap0
ip tuntap add dev tun0 mode tun
ip tuntap add dev tap0 mode tap
tunctl -t tap0 # 默认创建tap设备
tunctl -n -t tap0
可使用 ifconfig
等工具查看这些虚拟网络设备。
ifconfig -a
# 注意tap0是以太网设备,具有MAC地址
tap0: flags=4098<BROADCAST,MULTICAST> mtu 1500
ether 0e:38:5b:10:e9:1c txqueuelen 1000 (Ethernet)
......
# 注意tun0是POINTOPOINT设备,没有MAC地址
tun0: flags=4241<UP,POINTOPOINT,NOARP,MULTICAST> mtu 1500
inet 10.10.10.10 netmask 255.255.255.255 destination 10.10.10.10
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC)
......
可以为 tun/tap 分配 IP 地址或配置其它属性,例如:
ifconfig tun0 10.0.0.33 up
# 或者
ip link set tun0 up
tun/tap 的创建细节
创建 tun/tap
设备时,内核会自动为 tun
、tap
提供网卡驱动程序,使其能正常工作。此外,内核还会为 tun
、tap
提供字符设备驱动,使其能够在用户空间
和内核空间
传递数据。
其实,tun
和 tap
都是基于 /dev/net/tun
字符设备所创建 (或称为克隆) 的虚拟网络设备:
ls -l /dev/net/tun
crw-rw-rw-. 1 root root ... /dev/net/tun
Linux
中创建 tun
、tap
时,要求打开 /dev/net/tun
设备,打开后会返回一个文件描述符 fd
,再使用 ioctl()
在此 fd
上注册 tun
或 tap
设备,注册后将自动创建 tunX 或 tapX 设备 (这取决于 ioctl () 中注册的设备类型),其中 X 是一个从 0 开始的正整数。创建成功后,在 ifconfig 等命令中就可以看到 tunX 或 tapX。
在使用 ioctl()
注册时,还可以指定创建的 tun/tap
设备是否持久保留,如果不持久保留,那么程序退出或关闭 fd,都会自动移除对应的 tun/tap 设备。比如 tunctl、ip、openvpn 等工具创建的都是持久化的 tun/tap 设备,即使这些程序退出了,虚拟网卡设备也仍然保留。
注册 tun
或 tap
之后,可使用 fd 来读写 tunX 或 tapX 设备文件。
用户程序读写 tunX 或 tapX 设备,即表示虚拟网卡收发数据 (在用户空间和内核网络协议栈之间传输数据):
读
虚拟网卡
表示从虚拟网卡
收,即读取来自内核网络协议栈
的数据,这些数据是内核决策后决定要从tun/tap
设备发送出去的数据,一般是已经被内核封装过的数据写
虚拟网卡
表示向虚拟网卡
发,即用户空间
程序将数据写入网卡tun/tap
,内核网络协议栈
将收到这些数据并对数据进行解封 (就像外界数据经过物理网卡后进入内核协议栈一样)程序从
虚拟网卡
中读数据没什么可讲的,但写虚拟网卡
却有必要一提。
程序写入虚拟网卡时的注意事项
用户空间
的程序不可随意向虚拟网卡
写入数据,因为写入虚拟网卡
的这些数据都会被内核网络协议栈
进行解封处理,就像来自物理网卡
的数据都会被解封一样。
因此,如果用户空间
程序要写 tun/tap
设备,所写入的数据需具有特殊结构:
- 要么是已经封装了 PORT 的数据,即传输层的 tcp 数据段或 udp 数据报
- 要么是已经封装了 IP+PORT 的数据,即 ip 层数据包
- 要么是已经封装了 IP+PORT+MAC 的数据,即链路层数据帧
- 要么是其它符合 tcp/ip 协议栈的数据,比如二层的 PPP 点对点数据,比如三层的 icmp 协议数据
也就是说,程序只能向虚拟网卡
写入已经封装过的数据。
由于网络数据的封装都由内核的网络协议栈
负责,所以程序写入虚拟网卡
的数据实际上都原封不动地来自于上一轮的网络协议栈
,用户空间
程序无法对这部分数据做任何修改。
也就是说,这时写虚拟网卡
的用户空间
程序仅充当了一个特殊的【转发】程序:要么转发四层 tcp/udp 数据,要么转发三层数据包,要么转发二层数据帧。
这一段话可能不好理解,下面给个简单的示例分析。
假如物理网卡
eth0 从外界网络接收了这么一段特殊的 ping 请求数据:
这份数据会从物理网卡
传输到内核网络协议栈
,网络协议栈
会对其解封,解封的内容只能是 tcp/ip 协议栈中的内容,即只能解封帧头部、IP 头部以及端口头部,网络协议栈
解封后还剩下一段包含了内层 IP 头部 (tun 的 IP) 以及 icmp 请求的数据。
内核会根据刚才解封的端口号
找到对应的服务进程,并将解封剩下的数据传输给该进程,即传输给用户空间
的程序。
用户空间
的程序不做任何修改地将读取到的包含了内层 IP 头部和 ICMP 请求的数据原封不动地写入虚拟网卡
设备,内核从虚拟网卡
接收到数据后,将数据进行解封,解封得到最终的 icmp 请求数据,于是内核开始构建用于响应 ping 请求的数据。