OpenStack 使用TAP/TUN设备

OpenStack Neutron

TAP/TUN

虚拟设备和物理设备的区别

在Linux内核中有一个网络设备管理层,处于网络设备驱动和协议栈之间,负责衔接它们之间的数据交互。驱动不需要了解协议栈的细节,协议栈也不需要了解设备驱动的细节。

对于一个网络设备来说,就像一个管道(pipe)一样,有两端,从其中任意一端收到的数据将从另一端发送出去。

比如一个物理网卡eth0,它的两端分别是内核协议栈(通过内核网络设备管理模块间接的通信)和外面的物理网络,从物理网络收到的数据,会转发给内核协议栈,而应用程序从协议栈发过来的数据将会通过物理网络发送出去。

对于一个虚拟的网络设备,首先它也归内核的网络设备管理子系统管理,对于Linux内核网络设备管理模块来说,虚拟设备和物理设备没有区别,都是网络设备,都能配置IP,从网络设备来的数据,都会转发给协议栈,协议栈过来的数据,也会交由网络设备发送出去,至于是怎么发送出去的,发到哪里去,那是设备驱动的事情,跟Linux内核就没关系了,所以说虚拟网络设备的一端也是协议栈,而另一端是什么取决于虚拟网络设备的驱动实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+-----------------------------------------------+
| |
| +-------------+ |
| |Application A| | User Stack
| +-------------+ |
| |↑ |
| || |
|········||·····································|
| || +-----------> +--------+ |
| || |+----------- |tun/tap0| |
| || || +--------+ |
| || || ↑| |
| ↓| ↓| |↓ |
| +------------+ +----------------------+ | Kernel Stack
| |/dev/net/tun| |Network Protocol Stack| |
| +------------+ +----------------------+ |
| |
+-----------------------------------------------+

实现tun/tap设备的内核模块为tun,其模块介绍为Universal TUN/TAP device driver,该模块提供了一个设备接口/dev/net/tun供用户层程序读写,用户层程序通过读写/dev/net/tun来向主机内核协议栈注入数据或接收来自主机内核协议栈的数据,可以把tun/tap看成数据管道,它一端连接主机协议栈,另一端连接用户程序
为了使用tun/tap设备,用户层程序需要通过系统调用打开/dev/net/tun获得一个读写该设备的文件描述符(FD),并且调用ioctl()向内核注册一个TUN或TAP类型的虚拟网卡(实例化一个tun/tap设备),其名称可能是tap7b7ee9a9-c1/vnetXX/tunXX/tap0等。此后,用户程序可以通过该虚拟网卡与主机内核协议栈交互。当用户层程序关闭后,其注册的TUN或TAP虚拟网卡以及路由表相关条目(使用tun可能会产生路由表条目,比如openvpn)都会被内核释放。可以把用户层程序看做是网络上另一台主机,他们通过tap虚拟网卡相连。

TAP TUN的区别

TUN和TAP设备区别在于他们工作的协议栈层次不同,TAP等同于一个以太网设备,用户层程序向tap设备读写的是二层数据包如以太网数据帧,tap设备最常用的就是作为虚拟机网卡。TUN则模拟了网络层设备,操作第三层数据包比如IP数据包,openvpn使用TUN设备在C/S间建立VPN隧道

示例

下面拿一个类似于VPN的例子说一下

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
+----------------------------------------------------------------+
| |
| +--------------------+ +--------------------+ |
| | User Application A | | User Application B |<-----+ |
| +--------------------+ +--------------------+ | |
| | 1 | 5 | |
|...............|......................|...................|.....|
| ↓ ↓ | |
| +----------+ +----------+ | |
| | socket A | | socket B | | |
| +----------+ +----------+ | |
| | 2 | 6 | |
|.................|.................|......................|.....|
| ↓ ↓ | |
| +------------------------+ 4 | |
| | Network Protocol Stack | | |
| +------------------------+ | |
| | 7 | 3 | |
|................|...................|.....................|.....|
| ↓ ↓ | |
| +----------------+ +----------------+ | |
| | eth0 | | tun0 | | |
| +----------------+ +----------------+ | |
| 10.32.0.11 | | 192.168.3.11 | |
| | 8 +---------------------+ |
| | |
+----------------|-----------------------------------------------+

Physical Network

上图讲述了使用tun/tap设备实现VPN的功能,为了方便讲解,我将socket层单独拿出来说。

数据包发送过程:

  1. 应用程序A 通过socket A 发送了一个数据包
  2. socket A将当前数据包丢给协议栈,假设数据包的目的地址为:10.0.0.1
  3. 协议栈根据数据包的目的地址,匹配本地路由规则,知道这个数据包应该由tun0发送,于是将数据包发送tun0
  4. tun0设备的另一端是应用程序B,等待应用程序接收数据包
  5. 进程B接收到数据包之后,根据数据包的数据执行相应的动作,丢弃或者是返回特定的数据包。假设进程B新建一个数据包,讲发送过来的数据包放在新的数据包里面,最后通过Socket B发出
  6. Socket B 讲数据包发送给协议栈,假设数据包的目的地址为:47.240.28.194
  7. 协议栈根据路由规则匹配到当前数据包应该由eth0网卡发出,于是将数据包交给eth0发送出去,
  8. eth0通过物理网络将数据包发送出去

实验:使用Python创建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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import os
from fcntl import ioctl
import struct
import time
import random
from threading import Thread

from pyroute2 import IPRoute
from pypacker.layer3.ip import IP
from pypacker.layer3.ip6 import IP6
from pypacker.layer3.icmp import ICMP


TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000


def main():
fid = os.open("/dev/net/tun", os.O_RDWR)
ioctl(fid, TUNSETIFF, struct.pack("16sH", b"tun0", IFF_TUN | IFF_NO_PI))
set_addr('tun0', '10.0.0.1')

read_thread = Thread(target=read_input, args=(fid,))
read_thread.start()

req_nr = 1
req_id = random.randint(1, 65000)
while True:
icmp_req = IP(src_s="10.0.0.1", dst_s="114.114.114.114", p=1) +\
ICMP(type=8) +\
ICMP.Echo(id=req_id, seq=req_nr, body_bytes=b"test")
os.write(fid, icmp_req.bin())
time.sleep(1)
req_nr += 1


def read_input(fid):
while True:
data = os.read(fid, 1500)
parse_packet(data)


def parse_packet(data):
try:
packet = IP(data)
except:
packet = IP6(data)
print(packet)
print()


def set_addr(dev, addr):
ip = IPRoute()
idx = ip.link_lookup(ifname=dev)[0]
ip.addr('add', index=idx, address=addr, prefixlen=24)
ip.link('set', index=idx, state='up')
ip.close()


if __name__ == '__main__':
main()

解释:

  • 打开/dev/net/tun,获得文件描述符fid,使用ioctl对fid描述符进行操作,配置设备名为tun0
  • 使用pyroute2包对tun0配置IP地址,设置状态为up
  • 创建一个线程1,使用fid文件描述符读取设备数据,每次读取1500字节,读取完之后,使用IP/IP6”格式化”,并输出
  • 线程0继续执行,构建ICMP数据包,发送给114.114.114.114

执行脚本:

以下操作是在CentOS7上面执行,为了方(tou)便(lan)我没有配置网卡名称,默认的为ens33,所以把ens33看成eth0好了

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

# console 1


[root@localhost test]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:89:17:39 brd ff:ff:ff:ff:ff:ff
inet 172.16.117.133/24 brd 172.16.117.255 scope global dynamic ens33
valid_lft 1272sec preferred_lft 1272sec
inet6 fe80::20c:29ff:fe89:1739/64 scope link
valid_lft forever preferred_lft forever
3: ens37: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:89:17:43 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.10/24 brd 192.168.0.255 scope global ens37
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe89:1743/64 scope link
valid_lft forever preferred_lft forever
[root@localhost test]# python3.7 tun.py
layer3.ip6.IP6
v_fc_flow (I): 0x60000000 = 1610612736 = 0b1100000000000000000000000000000
dlen (H): 0x8 = 8 = 0b1000
nxt (B): 0x3A = 58 = 0b111010
hlim (B): 0xFF = 255 = 0b11111111
src : b'\xfe\x80\x00\x00\x00\x00\x00\x00\xb0\xe4\xc2\x9f\xf4\x055v' = fe80::b0e4:c29f:f405:3576
dst : b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' = ff02::2
opts : []
layer3.icmp6.ICMP6
type (B): 0x85 = 133 = 0b10000101
code (B): 0x0 = 0 = 0b0
sum (H): 0xE036 = 57398 = 0b1110000000110110
bodybytes : b'\x00\x00\x00\x00'


layer3.ip6.IP6
v_fc_flow (I): 0x60000000 = 1610612736 = 0b1100000000000000000000000000000
dlen (H): 0x8 = 8 = 0b1000
nxt (B): 0x3A = 58 = 0b111010
hlim (B): 0xFF = 255 = 0b11111111
src : b'\xfe\x80\x00\x00\x00\x00\x00\x00\xb0\xe4\xc2\x9f\xf4\x055v' = fe80::b0e4:c29f:f405:3576
dst : b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' = ff02::2
opts : []
layer3.icmp6.ICMP6
type (B): 0x85 = 133 = 0b10000101
code (B): 0x0 = 0 = 0b0
sum (H): 0xE036 = 57398 = 0b1110000000110110
bodybytes : b'\x00\x00\x00\x00'

layer3.ip6.IP6
v_fc_flow (I): 0x60000000 = 1610612736 = 0b1100000000000000000000000000000
dlen (H): 0x8 = 8 = 0b1000
nxt (B): 0x3A = 58 = 0b111010
hlim (B): 0xFF = 255 = 0b11111111
src : b'\xfe\x80\x00\x00\x00\x00\x00\x00\xb0\xe4\xc2\x9f\xf4\x055v' = fe80::b0e4:c29f:f405:3576
dst : b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02' = ff02::2
opts : []
layer3.icmp6.ICMP6
type (B): 0x85 = 133 = 0b10000101
code (B): 0x0 = 0 = 0b0
sum (H): 0xE036 = 57398 = 0b1110000000110110
bodybytes : b'\x00\x00\x00\x00'

# switch console 2
[root@localhost test]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:89:17:39 brd ff:ff:ff:ff:ff:ff
inet 172.16.117.133/24 brd 172.16.117.255 scope global dynamic ens33
valid_lft 1130sec preferred_lft 1130sec
inet6 fe80::20c:29ff:fe89:1739/64 scope link
valid_lft forever preferred_lft forever
3: ens37: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:89:17:43 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.10/24 brd 192.168.0.255 scope global ens37
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe89:1743/64 scope link
valid_lft forever preferred_lft forever
29: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
link/none
inet 10.0.0.1/24 scope global tun0
valid_lft forever preferred_lft forever
inet6 fe80::b0e4:c29f:f405:3576/64 scope link flags 800
valid_lft forever preferred_lft forever


# 在此可以看到,已经添加了tun0设备,并且状态设置为UP以及配置了IP地址
# 此时线程0是正在每隔1s发送一个ICMP数据包的,那我们抓包来看看
[root@localhost test]# tcpdump -n -i tun0 -p icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
21:39:42.884724 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 336, length 20
21:39:43.888298 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 337, length 20
21:39:44.890558 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 338, length 20
21:39:45.894039 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 339, length 20
21:39:46.897343 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 340, length 20
^C
5 packets captured
5 packets received by filter
0 packets dropped by kernel

# 为什么收不到reply包?接着往下看

[root@localhost test]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.16.117.2 0.0.0.0 UG 0 0 0 ens33
10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0
....

[root@localhost test]# tcpdump -n -i ens33 -p icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
21:40:43.070594 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 396, length 20
21:40:44.074246 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 397, length 20
21:40:45.077836 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 398, length 20
21:40:46.081776 IP 10.0.0.1 > 114.114.114.114: ICMP echo request, id 8940, seq 399, length 20
^C
4 packets captured
4 packets received by filter
0 packets dropped by kernel

可以看到数据是ens33正常转发出去了,只所以收不到reply包的原因是因为上级路由不知道这个10.0.0.1这个包应该交给谁

OpenStack 使用tap

经过上面的铺垫,相信大家已经对TAP/TUN了解了。
我们知道KVM虚拟化中单个虚拟机是主机上的一个普通qemu-kvm进程,虚拟机当然也需要网卡,最常见的虚拟网卡就是使用主机上的tap设备。那从主机的角度看,这个qemu-kvm进程是如何使用tap设备?