1. Network Namespace
Linux Programmer‘s Manual(https://man7.org/linux/man-pages/man7/network_namespaces.7.html)里对 Network Namespace 有一段简短的描述,在里面就列出了最主要的几部分资源,它们都是通过 Network Namespace 隔离的。这些资源主要有:
- 第一种,网络设备,这里指的是 lo,eth0 等网络设备。你可以通过 ip link 命令看到它们。
- 第二种是 IPv4 和 IPv6 协议栈参数,IP 层以及上面的 TCP 和 UDP 协议栈都是每个 Namespace 独立工作的,这里其实是指 IP、TCP、UDP 等许多协议,它们相关参数是每个 Namespace 独立的,同时也包括了 TCP、UDP 的 port 资源(这些参数大多数都在 /proc/sys/net 目录下面)。
- 第三种,IP 路由表。在不同的 Network Namespace 运行 ip route 命令,就能看到不同的路由表了。
- 第四种,防火墙规则,其实说的就是 iptables 规则了,每个 Namespace 里都可以独立配置 iptables 规则。
- 最后一种,网络的状态信息,这些信息可以从 /proc/net 和 /sys/class/net 里得到。这里的状态基本上包括了前面 4 种资源的状态信息。
1.1. 相关的操作
-
通过系统调用 clone() 函数来创建新的 network namespace。这种方法主要用在新进程创建的时候。通过 clone() 系统调用带上 CLONE_NEWNET flag,那么新进程被创建出来的时候,新的 network namespace 也创建了。
int new_netns(void *para) { printf("New Namespace Devices:\n"); system("ip link"); printf("\n\n"); sleep(100); return 0; } int main(void) { pid_t pid; printf("Host Namespace Devices:\n"); system("ip link"); printf("\n\n"); pid = clone(new_netns, stack + STACK_SIZE, CLONE_NEWNET | SIGCHLD, NULL); if (pid == -1) errExit("clone"); if (waitpid(pid, NULL, 0) == -1) errExit("waitpid"); return 0; }
-
调用 unshare() 系统调用来直接改变当前进程的 Network Namespace。
int main(void) { pid_t pid; printf("Host Namespace Devices:\n"); system("ip link"); printf("\n\n"); if (unshare(CLONE_NEWNET) == -1) errExit("unshare"); printf("New Namespace Devices:\n"); system("ip link"); printf("\n\n"); return 0; }
其他 namespace 也都可以通过 clone() 或者 unshare() 系统调用建立。创建容器的程序,runc 也是用 unshare() 给新建的容器建立 Namespace 的。
-
用 ip netns 命令操作 network namespace,但是需要在 /var/run/netns 中有命名文件指向一个 network namespace。
-
lsns -t net 这个命令来查看系统里已有的 Network Namespace。
当然,lsns也可以用来查看其它 Namespace。
-
用 nsenter 这个命令进入到某个 Network Namespace 里,具体去查看这个 Namespace 里的网络配置。
# lsns -t net NS TYPE NPROCS PID USER NETNSID NSFS COMMAND 4026531992 net 283 1 root unassigned /usr/lib/systemd/systemd --switched-root --system --deserialize 16 4026532241 net 1 7734 root unassigned ./clone-ns # nsenter -t 7734 -n ip addr 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
1.2. 动手实现
隔离容器中的网络,每个容器都有自己的虚拟网络接口和 IP 地址。在 Linux 中,可以使用 ip 命令创建 Network Namespace(Docker 的源码中,它没有使用 ip 命令,而是自己实现了 ip 命令内的一些功能)。
下面就使用 ip 命令来讲解一下 Network Namespace 的构建,以 bridge 网络为例。bridge 网络的拓扑图一般如下图所示,其中 br0 是 Linux 网桥。
在使用 Docker 的时候,如果启动一个 Docker 容器,并使用 ip link show 查看当前宿主机上的网络情况,那么你会看到有一个 docker0 还有一个 veth****
的虚拟网卡,这个 veth 的虚拟网卡就是上图中 veth,而 docker0 就相当于上图中的 br0。
那么,我们可以使用下面这些命令即可创建跟 docker 类似的效果(参考自耗子叔的博客,链接见文末参考,结合上图加了一些文字)。
## 1. 首先,我们先增加一个网桥 lxcbr0,模仿 docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址
## 2. 接下来,我们要创建一个 network namespace ,命名为 ns1
# 增加一个 namesapce 命令为 ns1 (使用 ip netns add 命令)
ip netns add ns1
# 激活 namespace 中的 loopback,即127.0.0.1(使用 ip netns exec ns1 相当于进入了 ns1 这个 namespace,那么 ip link set dev lo up 相当于在 ns1 中执行的)
ip netns exec ns1 ip link set dev lo up
## 3. 然后,我们需要增加一对虚拟网卡
# 增加一对虚拟网卡,注意其中的 veth 类型。这里有两个虚拟网卡:veth-ns1 和 lxcbr0.1,veth-ns1 网卡是要被安到容器中的,而 lxcbr0.1 则是要被安到网桥 lxcbr0 中的,也就是上图中的 veth。
ip link add veth-ns1 type veth peer name lxcbr0.1
# 把 veth-ns1 按到 namespace ns1 中,这样容器中就会有一个新的网卡了
ip link set veth-ns1 netns ns1
# 把容器里的 veth-ns1 改名为 eth0 (容器外会冲突,容器内就不会了)
ip netns exec ns1 ip link set dev veth-ns1 name eth0
# 为容器中的网卡分配一个 IP 地址,并激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up
# 上面我们把 veth-ns1 这个网卡按到了容器中,然后我们要把 lxcbr0.1 添加上网桥上
brctl addif lxcbr0 lxcbr0.1
# 为容器增加一个路由规则,让容器可以访问外面的网络
ip netns exec ns1 ip route add default via 192.168.10.1
## 4. 为这个 namespace 设置 resolv.conf,这样,容器内就可以访问域名了
echo "nameserver 8.8.8.8" > conf/resolv.conf
上面基本上就相当于 docker 网络的原理,只不过:
- Docker 不使用 ip 命令而是,自己实现了 ip 命令内的一些功能。
- Docker 的 resolv.conf 没有使用这样的方式,而是将其写到指定的 resolv.conf 中,之后在启动容器的时候将其和 hostname、host 一起以只读的方式加载到容器的文件系统中。
- docker 使用进程的 PID 来做 network namespace 的名称。
同理,我们还可以使用如下的方式为正在运行的 docker 容器增加一个新的网卡
ip link add peerA type veth peer name peerB
brctl addif docker0 peerA
ip link set peerA up
ip link set peerB netns ${container-pid}
ip netns exec ${container-pid} ip link set dev peerB name eth1
ip netns exec ${container-pid} ip link set eth1 up
ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1
巨人的肩膀
- 极客时间.《深入剖析 Kubernetes》.张磊."白话容器基础(二):隔离与限制"
- DOCKER基础技术:LINUX NAMESPACE(上)
- DOCKER基础技术:LINUX NAMESPACE(下)。
- 极客时间.《容器实战》