Docker运行原理-0X01-Namespaces

Docker运行原理-0X01-Namespaces

Created
Mar 24, 2022 10:10 AM
Tags
容器

命名空间(namespace)

是 Linux 内核用来隔离内核资源的方式。也相当于对一些资源进行了权限管理。在一个namespace中的进程共享一些资源,而不再一个namespace中则无法访问这些资源
linux提供的namespace服务为容器的出现和发展提供了基础条件
官方文档:https://man7.org/linux/man-pages/man7/namespaces.7.html

linux 隔离方式

名称        宏定义             隔离内容
Cgroup      CLONE_NEWCGROUP   Cgroup root directory (since Linux 4.6)
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues (since Linux 2.6.19)
Network     CLONE_NEWNET      Network devices, stacks, ports, etc. (since Linux 2.6.24)
Mount       CLONE_NEWNS       Mount points (since Linux 2.4.19)
PID         CLONE_NEWPID      Process IDs (since Linux 2.6.24)
User        CLONE_NEWUSER     User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
UTS         CLONE_NEWUTS      Hostname and NIS domain name (since Linux 2.6.19)
  • Cgroup:物理资源namespace,使进程有一个独立的cgroup控制组
  • IPC:进程间通讯,使进程有一个独立的ipc,包括消息队列,共享内存和信号量
  • Network:网络空间,使进程有一个独立的网络栈
  • Mount:文件空间
  • PID:进程空间
  • User:用户空间
  • UTS:主机名空间

查看进程namespace

ls -al /proc/$$/ns   
总用量 0
dr-x--x--x 2 root root 0 Apr  1 15:50 .
dr-xr-xr-x 9 root root 0 Apr  1 15:43 ..
lrwxrwxrwx 1 root root 0 Apr  1 15:50 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 net -> net:[4026532293]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr  1 15:50 uts -> uts:[4026531838]
如果两个进程的某个 namespace 文件指向同一个链接文件,说明其相关资源在同一个 namespace

相关函数

  • clone:clone 在创建新进程的同时创建 namespace
  • setns:setns 加入一个已经存在的 namespace
  • unshare:unshare 在原先进程上进行 namespace 隔离

资源隔离测试用例

一、测试UTS隔离

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    printf("Container - inside the container!\n");
    sethostname("container",10); /* 设置hostname */
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent - start a container!\n");
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}
notion image
image-20220322142542912

二、测试PID隔离

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    /* 查看子进程的PID,我们可以看到其输出子进程的 pid 为 1 */
    printf("Container [%5d] - inside the container!\n", getpid());
    sethostname("container",10);
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    /*启用PID namespace - CLONE_NEWPID*/
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); 
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}
notion image
image-20220322143402190
可以看到子进程的PID为1,可以看到和docker中的进程是类似的,即实现了进程隔离。
notion image
但是在新的pid namespace中执行ps、top等命令,还是会显示很多进程,原因是这些命令读取进程时通过“/proc”目录下的文件来获取的。

三、测试Mount隔离

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    printf("Container [%5d] - inside the container!\n", getpid());
    sethostname("container",10);
    /* 重新mount proc文件系统到 /proc下 */
    system("mount -t proc proc /proc");
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    /* 启用Mount Namespace - 增加CLONE_NEWNS参数 */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}
proc是一个虚拟文件系统
隔离效果如下所示,在进程隔离的情况下,再进行文件挂载隔离,执行ps、top命令就只获取到本namespace下的资源。
notion image
image-20220322144830554

四、测试User隔离

创建一个新的user namespace ,unshare --user -r --mount /bin/bash
效果如下图所示,通过-r参数,把新user namespace (user:[4026532240])的root用户映射到了原user namespac的ctf用户。这就形成了同一个用户在不同user namespace中有不同的uid和gid。
notion image
image-20220322152031078
docker默认是没有使用user namespace的。它创建的容器和宿主机是同一 user namespace,如下所示
notion image

五、测试Network隔离

network namespace主要是用来进行网络隔离的,将网络划分为不同的namespace,不同namespace的操作互不影响,每个network namespace包含:
  1. 网卡和mac地址
  1. arp表
  1. ip地址,端口,路由表
  1. iptables等网络资源

在宿主机上抓取容器中的网络流量

1、获取某个容器的pid
❯ docker ps
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                     NAMES
7ea005a0b2fb        hive_agent:3.401.140-120   "/bin/bash /titan/ag…"   5 weeks ago         Up About a minute                             hiveagent
ea9f21f54b50        pwn_awd                    "/run.sh"                5 months ago        Up About an hour    0.0.0.0:10004->9999/tcp   awd

root@ubuntu ~
❯ docker inspect --format "{{.State.Pid}}" awd
5345
2、通过 进入到容器的网络空间
通过前后ip a来查看变化
root@ubuntu ~
❯ ip a                                                                                                                                                               1271: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:1c:42:bf:dc:a6 brd ff:ff:ff:ff:ff:ff
    inet 10.211.55.9/24 brd 10.211.55.255 scope global dynamic enp0s5
       valid_lft 1676sec preferred_lft 1676sec
    inet6 fdb2:2c26:f4e4:0:8391:699:255e:b634/64 scope global noprefixroute dynamic 
       valid_lft 2591983sec preferred_lft 604783sec
    inet6 fe80::593f:cc6d:4234:e5e6/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:7f:af:0d:34 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7fff:feaf:d34/64 scope link 
       valid_lft forever preferred_lft forever
5: veth9d533b3@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 66:fb:dd:d0:6d:68 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::64fb:ddff:fed0:6d68/64 scope link 
       valid_lft forever preferred_lft forever

root@ubuntu ~
❯ nsenter -n -t 5345

root@ubuntu ~
❯ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

root@ubuntu ~
3、tcpdump 抓取流量
tcpdump -vv

六、Chroot

该命令的作用是改变根目录位置,在指定的根目录运行命令。在Linux系统中,默认的跟系统是“/”。在某些场景下使用chroot之后,会增加系统的安全性,限制用户的权限,chroot作用范围在当前进程和当前进程的子进程中。其次是建立一个与原系统隔离的系统目录结构,方便用户的开发。