9.30更新:这本书应该不会再看下去了。豆瓣评价不多且差评蛮多,有几个好评才决定看的,看了一部分后还是及时止损吧。把时间花在更难更有价值的书籍上吧,官网学习也是正确的道路。

容器网络虚拟化基础


macOS M1下使用docker运行linux(centos):

1
docker run -it -v ~/Documents/docker/:/datas centos

报错:mount --make-shared /var/run/netns failed: Operation not permitted

解决方法:加上--privileged,即:

1
docker run --privileged -it -v ~/Documents/docker/:/datas centos

Linux网络虚拟化

network namespace

配置network namespace

Linux的namespace:隔离内核资源

network namespace分类:

  • Mount namespace:文件系统挂载点
  • UTS namespace:主机名
  • IPC namespace:POSIX进程间通信消息队列
  • PID namespace:进程PID数字空间
  • network namespace:IP地址
  • user namespace:user ID数字空间
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
docker run --privileged -it -v ~/Documents/docker/:/datas centos
# ip netns add netns1
# ip netns exec netns1 ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/tunnel6 :: brd :: permaddr 9a0c:8a2c:6c02::
# ip netns list
netns1
# ip netns delete netns1

ip netns add NAME - create a new named network namespace

If NAME is available in /var/run/netns this command creates a new network namespace and assigns NAME.

当ip命令创建了一个network namespace时,系统会在/var/run/netns路径下面生成一个挂载点。

1
2
3
4
5
6
7
8
# ip netns exec netns1 ping 127.0.0.1
connect: Network is unreachable
# ip netns exec netns1 ip link set dev lo up
# ip netns exec netns1 ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.043 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.123 ms
...
1
2
# ip link add veth0 type veth peer name veth1
# ip link set veth1 netns netns1

创建了veth0和veth1这么一对虚拟以太网卡。

在默认情况下,它们都在主机的根network namespce中,将其中一块虚拟网卡veth1通过ip link set命令移动到netns1 network namespace。


报错:exec of "ifconfig" failed: No such file or directory

解决方法:

How to fix “bash: /usr/sbin/ifconfig: No such file or directory” on Linux

centos下运行:

yum install net-tools -y

再次运行ifconfig:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 27688  bytes 133307537 (127.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 26844  bytes 1814858 (1.7 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
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

1
2
# ip netns exec netns1 ifconfig veth1 10.1.1.1/24 up
# ifconfig veth0 10.1.1.2/24 up
  • 首先进入netns1这个network namespace

  • 为veth1绑定IP地址10.1.1.1/24

  • 把网卡的状态设置成UP

在主机根network namespace中的网卡veth0绑定IP地址10.1.1.2/24。

在主机上ping 10.1.1.1(netns1 network namespace里的网卡):

1
2
3
4
5
# ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.946 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.295 ms
...

进入netns1 network namespace去ping主机上的虚拟网卡:

1
2
3
4
5
# ip netns exec netns1 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.317 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.387 ms
...

不同network namespace之间的路由表和防火墙规则等也是隔离的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    0      0        0 eth0
10.1.1.0        0.0.0.0         255.255.255.0   U     0      0        0 veth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
# ip netns exec netns1 route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
10.1.1.0        0.0.0.0         255.255.255.0   U     0      0        0 veth1

连接因特网的若干解决方法。例如:

  • 主机的根network namespace创建一个Linux网桥并绑定veth pair的一端到网桥上
  • 通过适当的NAT(网络地址转换)规则并辅以Linux的IP转发功能(配置net.ipv4.ip_forward=1)

用户可以随意将虚拟网络设备分配到自定义的network namespace里,而连接真实硬件的物理设备则只能放在系统的根network namesapce中。

1
# ip netns exec netns1 ip link set veth1 netns 1

ip link set veth1 netns 1把netns1 network namespace下的veth1网卡挪到PID为1的进程(即init进程)所在的network namespace。

有两种途径索引network namespace:

  • 名字(例如netns1)
  • 属于该namespace的进程PID(例如1)

network namespace API

clone()、unshare()和setns()系统调用会使用CLONE_NEW*常量来区别要操作的namespace类型。

CLONE_NEW*常量一共有6个:CLONE_NEWIPC、CLONE_NEWNS、CLONE_NEWNET、CLONE_NEWPID、CLONE_NEW USER和CLONE_NEWUTS,分别代表6种不同的namespace类型。

创建namespace:clone系统调用

1
int clone(int (&child_func)(void *), void *child_stack, int flags, void *arg);

函数指针child_func:指定一个由新进程执行的函数。当这个函数返回时,子进程终止。该函数返回一个整数(表示子进程的退出代码)。

指针child_stack:传入子进程使用的栈空间,也就是把用户态堆栈指针赋给子进程的esp寄存器。调用clone()的进程应该总是为子进程分配新的堆栈。

int flags:表示CLONE_*标志位(可以多个)。

void *args:用户的自定义参数。

维持namespace存在:/proc/PID/ns目录

每个Linux进程都拥有一个属于自己的/proc/PID/ns,这个目录下的每个文件都代表一个类型的namespace。

在Linux内核3.8版本以前,/proc/PID/ns目录下的文件都是硬链接(hard link),而且只有ipc、net和uts这三个文件。

从Linux内核3.8版本开始,每个文件都是一个特殊的符号链接文件,这些文件提供了操作进程关联namespace的一种方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Sep 19 00:51 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 ipc -> 'ipc:[4026532305]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 mnt -> 'mnt:[4026532303]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 net -> 'net:[4026532308]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 pid -> 'pid:[4026532306]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 pid_for_children -> 'pid:[4026532306]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Sep 19 00:51 uts -> 'uts:[4026532304]'
注:$$是shell进程的PID

符号链接的其中一个用途是确定某两个进程是否属于同一个namespace

如果两个进程在同一个namespace中,那么这两个进程/proc/PID/ns目录下对应符号链接文件的inode数字(即上文例子中[]内的数字,例如4026532308;也可以通过stat()系统调用获取返回结构体的st_ino字段)会是一样的。

/proc/PID/ns目录下的文件还有一个作用——当我们打开这些文件时,只要文件描述符保持open状态,对应的namespace就会一直存在,哪怕这个namespace里的所有进程都终止运行了。

1
2
# touch /my/net
# mount --bind /proc/$$/ns/net /my/net

把/proc/PID/ns目录下的文件挂载起来就能起到打开文件描述符的作用,而且这个network namespace会一直存在,直到/proc/self/ns/net被卸载。

往namespace里添加进程:setns系统调用

1
int setns(int fd,int nstype);

int fd:进程待加入的namespace对应的文件描述符。它是一个指向/proc/PID/ns目录下符号链接的文件描述符。

int nstype:让调用者检查第一个参数fd指向的namespace类型是否符合实际要求,0表示不检查。

让进程离开namespace:unshare系统调用

1
int unshare(int flags);

unshare()系统调用的工作机制是:

  • 先通过指定的flags(即CLONE_NEW*bit位的组合)创建相应的namespace
  • 再把这个进程挪到新创建的namespace中

于是也就完成了进程从原先namespace的撤离。unshare()提供的功能其实很像clone(),区别在于unshare()作用在一个已存在的进程上,而clone()会创建一个新的进程。

大部分Linux发行版自带的unshare命令就是基于unshare()系统调用的,它的作用就是在当前shell所在的namespace外执行一条命令。

1
# unshare [options] program [arguments]

Linux会为需要执行的命令(在上面的例子中,即program)启动一个新进程,然后在另外一个namespace中执行操作,这样就可以起到执行结果和原(父)进程隔离的效果。

veth pair

veth是虚拟以太网卡(Virtual Ethernet)的缩写。veth设备总是成对的,因此称之为veth pair。

容器与host veth pair的关系

经典容器组网模型就是veth pair+bridge的模式。

方法1:

1
2
3
4
5
# ip link show eth0
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# ip link show | grep 19
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default

18: eth0@if19,其中18是eth0接口的index,19是和它成对的veth的index。

方法2:

1
2
3
4
[root@d3eff7a40411 /]# ethtool -S  eth0
NIC statistics:
     peer_ifindex: 19
     ...

可以通过ethtool -S命令列出veth pair对端的网卡index。

Linux bridge

Linux bridge就是Linux系统中的网桥,但是Linux bridge的行为更像是一台虚拟的网络交换机,任意的真实物理设备(eth0)和虚拟设备(veth pair和tap设备)都可以连接到Linux bridge上。Linux bridge不能跨机连接网络设备。

Linux bridge有多个端口,数据可以从任何端口进来,进来之后从哪个口出去取决于目的MAC地址,原理和物理交换机差不多。

创建一个bridge:

1
2
[root@d3eff7a40411 /]# ip link add name br0 type bridge
[root@d3eff7a40411 /]# ip link set br0 up

创建一对veth设备,并配置IP地址:

1
2
3
4
5
[root@d3eff7a40411 /]# ip link add veth0 type veth peer name veth1
[root@d3eff7a40411 /]# ip addr add 1.2.3.101/24 dev veth0
[root@d3eff7a40411 /]# ip addr add 1.2.3.102/24 dev veth1
[root@d3eff7a40411 /]# ip link set veth0 up
[root@d3eff7a40411 /]# ip link set veth1 up

将veth0连接到br0上:

1
2
# ip link set dev veth0 master br0
# brctl addif br0 veth0

tun/tap 设备

iptables

ipip

VXLAN

Macvlan

IPvlan

Docker容器网络

Linux容器

Docker的四大网络模式

CNM

容器组网

Kubernetes网络

Istio网络