Linux 虚拟化之:Net Namespace 下的网络通信

总览

Linux 的虚拟化技术之一,就是 Namespace,其中 Net Namespace 是网络命名空间,这个在 Docker 里用的比较多,这篇文章主要想做的是,如何模拟的一个跨 Namespace 的通信过程。

提一点:跨Namespace,可以是多个 Net Namespace 之间,也可以是 宿主机和某一个 Net Namespace。我们要模拟的是:

①:只有一个自定义的网络命名空间(名为 blue )。
②:在宿主机网络和 blue 命名空间网络 两者上,分别建立虚拟网卡,并设置不同IP,进行通信(比如 ping,比如 curl)
③:尝试在名为 blue 的网络命名空间里启动 http 服务,在宿主机上网络上,访问 blue 空间里的这个 http 服务。

所以,我们需要完成以下几个步骤:

一、创建新命名空间
二、为新命名空间 blue ,添加网卡
三、打通宿主机命名空间和新命名空间之间的通信
四、在 blue 网络命名空间下,创建 http 服务,宿主机访问。

实施步骤

创建名为 blue 的网络命名空间

1
ip netns add blue

ip 工具集非常强大,即可处理网络命名空间,也可以处理网络设备。

为新命名空间,添加网卡

创建 veth pair

veth pair,主要就是用来处理不同命名空间通信用的(确切的说,是 Linux 虚拟化技术的产物之一,目前在容器化上用的比较多),veth 其实是 virtual ethernet 的缩写(虚拟以太网卡)。而 veth pair,就是一次性创建 一对 虚拟网卡,它们成对出现,向 veth pair 的一端发送的数据,经过操作系统内核后,可以通过另外一端读取到数据。

veth pair 在不同的命名空间使用如下:

vethpair

1、创建 veth pair

1
ip link add vethtest01 type veth peer name vethest002

2、查看 veth pair

1
ip link list
1
2
3
4
5
// 输出大概如下:
18: vethest002@vethtest01: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
link/ether b2:13:25:45:40:89 brd ff:ff:ff:ff:ff:ff
19: vethtest01@vethest002: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
link/ether a6:41:82:ca:4c:fd brd ff:ff:ff:ff:ff:ff

注意:
1、ip link list 里,这个 link 指的就是网络设备,感觉叫 link 不太理解,可以通过 man ip 手册,找到 link 部分的说明。
2、其实我们通过 ifconfig 也可以看到,不过需要加 -a,即:ifconfig -a。ifconfig 默认只能看到已up的网络设备。

将 veh pair 的一端,添加命名空间,并处理好网卡设置

1、添加网卡到 blue 网络命名空间

1
ip link set vethest002 netns blue

添加完成后,宿主机的环境中,已经无法通过 ip link list 看到 vethest002 这个网卡设备了,因为它已经到 blue 网络命名空间里去了。

2、去到查看 blue 网络命名空间内,查看网卡设备列表:

1
ip netns exec blue ip link list
1
2
3
4
5
// 输出:
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
18: vethest002@if19: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
link/ether b2:13:25:45:40:89 brd ff:ff:ff:ff:ff:ff link-netnsid 0

3、为 blue 里的网卡,配置网卡接口

现在,blue 网络空间下,有 vethest002 设备,但这个设备第一,它没启动,第二,它没有设置网络IP信息,我们做一下这个处理:

1
ip netns exec blue ifconfig vethest002 10.1.1.2/16 up

注意:我这里设置的是 10.1.1.2/16,这里设置为16,而不是24,是因为,一会儿我要为宿主机网络的 veth pair 另一端 vethest001 设置另外一个网络段 10.1.2.2/16,这样,他俩才是在同一个网段。

再次查看 blue 网络命名空间内的网卡接口:

1
ip netns exec blue ifconfig
1
2
3
4
5
6
7
8
vethest002: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 10.1.1.2 netmask 255.255.0.0 broadcast 10.1.255.255
inet6 fe80::61:ff:feb0:f65f prefixlen 64 scopeid 0x20<link>
ether 02:61:00:b0:f6:5f txqueuelen 1000 (Ethernet)
RX packets 25 bytes 1911 (1.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 25 bytes 1859 (1.8 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

通过上面的命令,可以看到,我们可以在某个命名空间内执行某个命令,比如添加网卡接口、查看网卡列表,都是通过 ip netns exec 来做的,所以,我们也可以在某个网络命名空间下开启一个 shell,此 shell 中执行的所有操作,都会在这个网络命名空间下。

1
2
3
// 比如在某个命名空间下,开启一个 shell,去执行其他命令。
// 这个命令可以不执行,只是一个例子而已,但如果你执行了,记得退出来,否则你的网络操作都是在这里。
ip netns exec blue /bin/bash

打通宿主机命名空间和新命名空间之间的通信

bridge 概述:

我们已经为新的网络命名空间 blue 做了配置,但现在这个命名空间无法连通宿主机的网络。

我们需将通宿主机网络和 blue 网络命名的网络打通,就需要引入一个新的工具:bridge。

①:bridge,是虚拟网络设备,可以为 bridge 配置网络设备的特征,比如:IP、MAC等。

②:bridge 也是虚拟交换机,具备和物理交换机的类似功能。

bridge的特殊性:普通网络设备,一般有2个端,发送端和接收端。比如网卡,从外网接收数据,转到内核,或者从内核接收数据,再转发出去。但是,bridge 不同,它可以有很多的端口,数据可以从任意的一端进来,至于进来之后,要从哪里出去,就和物理交换机差不多,看目的地址了,bridge 可以正确的进行路由。

在开始配置 bridge 之前,我们可以看一下,在 K8S flannel(backend为overlay网络vxlan)这种网络模型下的 bridge 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@sja22 /]# brctl show
bridge name bridge id STP enabled interfaces
cni0 8000.0a580ae94101 no veth002d4c2b
veth02267e47
veth063995d0
veth0ac99849
veth0e651a57
veth13d7ac2c
veth141294eb
veth196c3d37
veth1be51bad
veth1c17df21
veth2036d552
veth204659c2
veth22261c72
veth23ffa84b
veth24b8dbe2
veth2b24c23e
veth2bebd32b
docker_gwbridge 8000.02424ebd5efe no veth550eff2

如上,可以看到,有一个名为 cni0 的 bridge,下面挂了一堆的 veth pair。

用 bridge 打通不同网络命名空间的原来图(图是摘的),大致如下:

vethpair

总得来说有几个关键点:

①:得有一个网桥。
②:创建一对(或者多对)veth pair,取决于你想让几个 Namespace 网络进行通信。
③:veth pair 的一端,要接入 Namespace里,另外一端,接到 bridge 上。
④:为 Namespace 里的 veth pair 设备设置 IP ,并启动。
⑤:为 网桥配置 IP(这个 IP 必须和 Namespace 那头的 IP 在同一个网段),并启动。

创建 bridge 并连接 veth pair 的一端

1、创建一个 bridge(网桥)

创建 bridge 有2种方式,用到的命令也不同,最基本的,就是 ip 命令

方式1:ip 命令创建和操作 bridge

1
ip link add name test-bridge type bridge

这个命令执行后,无法通过 ifconfig 看到这个设备,因为这个设备没有启动,所以需要启动 bridge 设备:

1
ip link set test-bridge up

还是用的 ip link 操作,所以,其实 bridge 也是一个网络设备,和 veth pair 是同一个层级的东西。

此命令执行后,可以通过 ifconfig 看到设备

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
[root@192-168-20-143 ~]# ifconfig                  

ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.20.143 netmask 255.255.255.0 broadcast 192.168.20.255
inet6 fe80::4873:514:f637:51d7 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:22:41:78 txqueuelen 1000 (Ethernet)
RX packets 4226 bytes 444256 (433.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 42796 bytes 3718608 (3.5 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1 (Local Loopback)
RX packets 1869903 bytes 259940119 (247.8 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1869903 bytes 259940119 (247.8 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

test-bridge: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::d045:68ff:fe56:5a05 prefixlen 64 scopeid 0x20<link>
ether d2:45:68:56:5a:05 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5 bytes 418 (418.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

方式2:brctl

1
yum -y install bridge-utils
1
brctl addbr test-bridge up

同样,上面的操作后,无法通过 ifconfig 看到此网桥设备,因为此设备还没有启动,还是需要手动启动

注意:新创建的 bridge 除了一端连接了网络协议栈外,啥都没有,所以,我们需要将 bridge 和 veth pair 连接起来。

2、将 bridge 和 veth pair 连接

我们之前,已经将创建的 veth pair 的一端(vethest002),连接到 blue 网络命名空间内,并配置好 IP 了,宿主机上还有 veh pair 另外一端(vethest001),我们要把它接入刚才创建的网桥上。

1
ip link set dev vethtest01 master test-bridge

3、为网桥配置IP地址并做连通测试

1
ifconfig test-bridge 10.1.2.2/16 up

经过上面的操作,其实我们已经可以连通这2个网络了。我们在宿主机网络,ping blue 命名空间的网络空间的 IP

明确一点:宿主机网桥IP是 10.1.2.2,网络命名空间blue内的是 10.1.1.2

1
2
3
4
5
[root@test ~]# ping 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.327 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.085 ms
64 bytes from 10.1.1.2: icmp_seq=3 ttl=64 time=0.056 ms

如上,网络已经连通。

在 blue 网络命名空间下,创建 http 服务,宿主机访问

先创建一个简单的HTTP服务

我们直接编译一个非常简单的,基于 C 语言的 HTTP 服务就行,所以需要准备下

1
yum -y install gcc

1、创建 http_server.c 文件,写入如下内容:

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
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <pthread.h>
#include <stdlib.h>

int startup()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
exit(1);//退出进程
}

int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(8080); // 指定固定端口

int ret = bind(sock,(struct sockaddr *)&local,sizeof(local));
if( ret < 0 )
{
exit(2);
}

if( listen(sock,5) < 0 )
{
exit(3);
}
return sock;
}

void* handler_request(void * arg)
{
int sock = (int)arg;
char buf[4896];
ssize_t s = read(sock,buf,sizeof(buf)-1);
if( s > 0 )
{
buf[s] = 0;
printf(" %s ",buf);
const char *echo_str = "HTTP/1.0 200 ok\n\n<html><h1>Welcome to my http server!</h1><html>\n";
write(sock,echo_str,strlen(echo_str));
}
close(sock);
}

int main()
{
int listen_sock = startup();
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(sock < 0)
{
continue;
}
pthread_t tid;
pthread_create(&tid,NULL,handler_request,(void *)sock);
pthread_detach(tid);
}
return 0;
}

2、创建 Makefile 文件,写入如下内容

1
2
3
4
5
6
.PHONY:all clean
all:http_server
http_server:http_server.c
gcc $^ -o $@ -lpthread
clean:
rm -rf http_server

编译出二进制文件

1
make

可以看到,编译出一个可执行文件:http_server

3、在 blue 网络命名空间下,启动这个 server

1
2
ip netns exec blue /bin/bash
./http_server

在这个网络下启动的这个 server 监听 8080 端口,也意味着,我们在宿主机,理论上可以通过 curl 访问这个服务。

4、新开一个窗口,访问 blue 网络内的 8080 HTTP 服务

1
2
[root@test ~]# curl 10.1.1.2:8080
<html><h1>Welcome to my http server!</h1><html>

参考

Donate comment here