目录

容器 | Namespace-Network Namespace

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 也创建了。

     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
    
    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。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    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 里的网络配置。

    1
    2
    3
    4
    5
    6
    7
    
    # 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 网桥。

https://img.dawnguo.cn/Container/image-20201005171258615.png

在使用 Docker 的时候,如果启动一个 Docker 容器,并使用 ip link show 查看当前宿主机上的网络情况,那么你会看到有一个 docker0 还有一个 veth**** 的虚拟网卡,这个 veth 的虚拟网卡就是上图中 veth,而 docker0 就相当于上图中的 br0。

那么,我们可以使用下面这些命令即可创建跟 docker 类似的效果(参考自耗子叔的博客,链接见文末参考,结合上图加了一些文字)。

 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
## 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 容器增加一个新的网卡

1
2
3
4
5
6
7
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 

巨人的肩膀

  1. 极客时间.《深入剖析 Kubernetes》.张磊.“白话容器基础(二):隔离与限制”
  2. DOCKER基础技术:LINUX NAMESPACE(上)
  3. DOCKER基础技术:LINUX NAMESPACE(下)
  4. 极客时间.《容器实战》