书籍来源:《Kubernetes网络权威指南:基础、原理与实践》
一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!
附上汇总贴:《Kubernetes网络权威指南》读书笔记 | 汇总_COCOgsta的博客-CSDN博客
顾名思义,Linux的namespace(名字空间)的作用就是“隔离内核资源”。Linux的namespace给里面的进程造成了两个错觉:
(1)它是系统里唯一的进程。
(2)它独享系统的所有资源。
默认情况下,Linux进程处在和宿主机相同的namespace,即初始的根namespace里,默认享有全局系统资源。
尽管Linux的namespace隔离技术很早便存在于内核中,而且它就是为Linux的容器技术而设计的,但它一直鲜为人知。直到Docker引领的容器技术革命爆发,它才进入普罗大众的视线。
每个网络namespace里都有自己的网络设备(如IP地址、路由表、端口范围、/proc/net目录等)。从网络的角度看,network namespace使得容器非常有用,使得在一个主机上同时运行多个监听80端口的Web服务器变为可能,如图1-1所示。
图1-1 network namespace示意图
1.1.1 初识network namespace
network namespace可以通过系统调用来创建,可以调用Linux的clone()(其实是UNIX系统调用fork()的延伸)API创建一个通用的namespace,然后传入CLONE_NEWNET参数表面创建一个network namespace。
创建一个名为netns1的networknamespace可以使用以下命令:
一个network namespace被创建出来后,可以使用ip netns exec命令进入,做一些网络查询/配置的工作。
默认只有一块本地回环设备lo。
想查看系统中有哪些networknamespace,可以使用以下命令:
想删除networknamespace,可以通过以下命令实现:
注意,上面这条命令实际上并没有删除netns1这个network namespace,它只是移除了这个network namespace对应的挂载点。只要里面还有进程运行着,network namespace便会一直存在。
1.1.2 配置network namespace
network namespace自带的lo设备状态是DOWN的,因此,当尝试访问本地回环地址时,网络也是不通的。下面的小测试就说明了这一点。
如果想访问本地回环地址,首先需要进入netns1这个network namespace,把设备状态设置成UP。
然后,尝试ping 127.0.0.1,发现能够ping通。
如果我们想与外界(比如主机上的网卡)进行通信,就需要在namespace里再创建一对虚拟的以太网卡,即所谓的veth pair。
下面的命令将创建一对虚拟以太网卡,然后把veth pair的一端放到netns1 network namespace。
在默认情况下,它们都在主机的根network namespce中,将其中一块虚拟网卡veth1通过ip link set命令移动到netns1 network namespace。两块网卡刚创建出来还都是DOWN状态,需要手动把状态设置成UP。
上面两条命令首先进入netns1这个network namespace,为veth1绑定IP地址10.1.1.1/24,并把网卡的状态设置成UP,而仍在主机根network namespace中的网卡veth0被我们绑定了IP地址10.1.1.2/24。这样一来,我们就可以ping通veth pair的任意一头了。
同理,我们可以进入netns1 network namespace去ping主机上的虚拟网卡,如下所示:
另外,不同network namespace之间的路由表和防火墙规则等也是隔离的。
如上所示,我们进入netns1 network namespace,分别输入route和iptables-L命令,期望查询路由表和iptables规则,却发现空空如也。这意味着从netns1 network namespace发包到因特网也是徒劳的,因为网络还不通!
该怎么理解上面这条看似有点复杂的命令呢?分解成两部分:
(1)ip netns exec netns1进入netns1 network namespace。
(2)ip link set veth1 netns 1把netns1 network namespace下的veth1网卡挪到PID为1的进程(即init进程)所在的network namespace。
上面这条命令其实就是把veth1从netns1 network namespace移动到系统根network namespace。
对namespace的root用户而言,他们都可以把其namespace里的虚拟网络设备移动到其他network namespace,甚至包括主机根network namespace!这就带来了潜在的安全风险。
1.1.3 network namespace API的使用
本节我们将通过几个C语言程序的例子介绍network namespace API的使用方法。clone()、unshare()和setns()系统调用会使用CLONE_NEW*常量来区别要操作的namespace类型。
- 创建namespace的黑科技:clone系统调用
用户可以使用clone()系统调用创建一个namespace。
我们可以通过其flags参数(标志位)控制特定的功能。clone()总共有二十多种CLONE_*标志位用来控制clone(克隆)进程时的行为。
只要在clone()设置了其中一个标志位CLONE_NEW,系统就会创建一个新的对应类型的namespace及一个新的进程,并且会把这个进程放到这个新创建的namespace中。通过|(位或)操作,我们可以实现clone()同时指定多个CLONE_NEW标志位。
- 维持namespace存在: /proc/PID/ns目录的奥秘
每个Linux进程都拥有一个属于自己的/proc/PID/ns,这个目录下的每个文件都代表一个类型的namespace。
从Linux内核3.8版本开始,每个文件都是一个特殊的符号链接文件,这些文件提供了操作进程关联namespace的一种方式。先来看一眼这些符号链接文件都长什么样。
如果两个进程在同一个namespace中,那么这两个进程/proc/PID/ns目录下对应符号链接文件的inode数字(即上文例子中[]内的数字,例如4026531839)会是一样的。
除此之外,/proc/PID/ns目录下的文件还有一个作用,保持namespace存在。因此,Linux内核提供的黑科技允许:只要打开文件描述符,不需要进程存在也能保持namespace存在!怎么操作?请看下面的命令:
如上所示,把/proc/PID/ns目录下的文件挂载起来就能起到打开文件描述符的作用,而且这个network namespace会一直存在,直到/proc/self/ns/net被卸载。
- 往namespace里添加进程:setns系统调用
setns()就是用来做这个工作的,其主要功能就是把一个进程加入一个已经存在的namespace中。setns()的定义如下所示:
一个简单的C语言样例如下所示:
让我们编译上面的代码段,假设编译后的二进制文件是enterns,那么执行以下命令:
以上命令执行完后,我们就进入一个新的network namespace,并且可以在里面执行shell命令。这种方式广泛应用于Docker和Kubernetes中。
- 帮助进程逃离namespace:unshare系统调用
unshare(),用于帮助进程“逃离”namespace。unshare()系统调用的函数声明如下:
它的作用就是在当前shell所在的namespace外执行一条命令。unshare命令的用法如下所示:
Linux会为需要执行的命令(在上面的例子中,即program)启动一个新进程,然后在另外一个namespace中执行操作,这样就可以起到执行结果和原(父)进程隔离的效果。
- 一个完整的例子
一个使用Linux network namespace相关系统调用的程序样例如下。
以上程序的大意是,在一个新的network namespace中初始化一个网络设备veth1,绑定IP地址169.254.1.1。当我们使用如下命令编译并运行以上程序时,会得到如下输出:
意思是父、子进程分别在两个不同的namespace中,其中父进程的PID是1831,子进程的PID是1(新建namespace中的第一个进程)。
进入这个新的namespace中,使用nc -l 1234命令监听0.0.0.0:1234。然后打开另一个终端,输入以下命令:
意思是向新建network namespace的169.254.1.2:1234发送一个网络报文Hi。切回原先的终端,会发现顺利地收到了一个Hi字符串。