Skip to content

Latest commit

 

History

History
216 lines (168 loc) · 9.59 KB

虚拟化容器学习.md

File metadata and controls

216 lines (168 loc) · 9.59 KB

本以为这么成熟的东西应该很简单,但是没想到,坑深不见底,所以只从用法来,原理层得有更多的沉淀才能写出来。

完全搞定的话起码得掌握如下知识:

  1. golang
  2. namespace
  3. cgroup
  4. capabilities
  5. 桥接网络
  6. unionfs(overlay)

namespace

这个是虚拟化的核心知识,也是kernel中关于资源隔离的实现,内核中提供的隔离方式有如下几种:

#define CLONE_NEWNS 0x00020000 /* New mount namespace group */ 文件系统
#define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace */  物理资源限制
#define CLONE_NEWUTS 0x04000000 /* New utsname namespace */   主机名和域名
#define CLONE_NEWIPC 0x08000000 /* New ipc namespace */    信号量,消息队列和共享内存
#define CLONE_NEWUSER 0x10000000 /* New user namespace */    用户和用户组
#define CLONE_NEWPID 0x20000000 /* New pid namespace */    进程号
#define CLONE_NEWNET 0x40000000 /* New network namespace */    网络设备,网络栈,端口等网络资源

简单来阐述一下就是,namespace可以看作是进程的一个属性,进程享有当前namespace中的资源,而一个namespace中可以有多个进程,他们共享这个namespace的资源,而当你修改了namespace中的资源后,也只会影响当前namespace下的进程。

为了控制namespacelinux自然提供了相应的API用在开发时使用:

  1. clone()
  2. setns()
  3. unshare()

三种API分别对应了三种情况:

  1. 创建进程时同时创建新的namespace
  2. 将进程加入到一个已经存在的namespace
  3. 在一个已经存在的进程上进行namespace隔离

写一份代码简单测试一下:

/*==============================================================================

# Author: lang [email protected]
# Filetype: C source code
# Environment: Linux & Archlinux
# Tool: Vim & Gcc
# Date: 2019.09.17
# Descprition: namespace learning

================================================================================*/

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

#define STACK_SIZE (1024*1024) /* Stack size for cloned child */

static char child_stack[STACK_SIZE];

int child_main(){
 printf("进入子进程\n");

 char *arg[] = {"/bin/bash",NULL};
 char *newhostname = "UTSnamespace";
 sethostname(newhostname,sizeof(newhostname));

 execv("/bin/bash",arg);
	
 return 1;
}

int main(void){

 printf("创建子进程\n");
 int child_pid = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWUSER | CLONE_NEWIPC | CLONE_NEWCGROUP | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | SIGCHLD,NULL);
 waitpid(child_pid,NULL,0);
 printf("退出子进程\n");

 return 0;

}

按照最后的效果来说,实际上也只有networkrootfs上有些问题,可以通过执行ifconfigps来验证这两个问题。 但是继续使用的时候就会发现user其实也是一个问题,因为按照道理来说,一个虚拟化空间中,使用的初始用户应该是root才是,也就是一个在虚拟化进程中具有所有资源访问权限的用户,但是为了安全,这个用户在其父user namespace应该为一个普通权限。但是就像如上代码运行的结果一样,只是单纯的新建一个user namespace,会导致虚拟化进程user namespace中的user为如下这种情况:

[nobody@g0dA lang]$ id
uid=65534(nobody) gid=65534(nobody) 组=65534(nobody)

现在很多发行版默认禁用了非特权用户命名空间:kernel.unprivileged_userns_clone,因此可以执行sysctl kernel.unprivileged_userns_clone=1

此刻的useridgroupid因为没有映射,使用的是由/proc/sys/kernel/overflowuid(overflowgid)提供出来的默认映射ID。

首先要知道linux当进程要去读取/写入文件的时候,内核都会检查该进程user namespaceuidgid,以确认是否具有权限,简单来说内核注重的是uidgid,而只有通过映射后,才能控制一个user namespace的用户在其余user namespace中的权限,这点的重要性主要体现在诸如给其余user namespace中进程发送信号或者是访问其余user namespace的文件。

关于进程的user namespace的映射需要用到/proc/PID/uid_map(gid_map),两个内容格式相同:

ID-inside-ns ID-outside-ns length

为了安全性考虑,对于两个文件的写入也有着严格的权限限制:

  1. 两个文件只允许拥有该user namespaceCAP_SETUID权限的进程写入一次且不允许修改
  2. 写入的进程必须是user namespace父namespace或者是子namespace
  3. 最后一个字段通常填1表示只映射一个,如果填写大于1则按照顺序一一映射

那如果用宿主机的root映射user namespace中的root会有问题吗? 答案是不会,其实docker就是这种映射方式,然而当子user namespace的用户访问父user namespace的资源的时候,启动进程的capabilities都为空,所以虽然是root映射,然而子user namespaceroot父user namespace中只相当于一个普通用户。

Linux 3.19后对gid_map做了更改,需要先向/proc/PID/setgroups文件中写入deny才能修改gid_map,这点是为了安全性考虑。因为在user namespace中,一个普通的帐号的新的user namespace中有了所有的capabilities,就可以通过调用setgroups让自己获取更大的权限

修改一下代码:

/*==============================================================================

# Author: lang [email protected]
# Filetype: C source code
# Environment: Linux & Archlinux
# Tool: Vim & Gcc
# Date: 2019.09.17
# Descprition: namespace learning

================================================================================*/

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

#define STACK_SIZE (1024*1024) /* Stack size for cloned child */

int parent_uid;
int parent_gid;

static char child_stack[STACK_SIZE];

//[...]
void set_uid_map(pid_t pid, int inside_id, int outside_id, int length) {
    char path[256];
    sprintf(path, "/proc/%d/uid_map", pid);
    FILE* uid_map = fopen(path, "w");
    fprintf(uid_map, "%d %d %d", inside_id, outside_id, length);
    fclose(uid_map);
}
void set_gid_map(pid_t pid, int inside_id, int outside_id, int length) {
 /* 3.19之后需要先修改/proc/PID/setgroups
  * 将内容从allow修改为deny
  * 否则无法修改gid_map的内容
  * */
    
    char path2[256];
    sprintf(path2,"/proc/%d/setgroups",pid);
    FILE* setgroups = fopen(path2,"w");
    fprintf(setgroups, "deny");
    fclose(setgroups);

    char path[256];
    sprintf(path, "/proc/%d/gid_map", pid);
    FILE* gid_map = fopen(path, "w");
    fprintf(gid_map, "%d %d %d", inside_id, outside_id, length);
    fclose(gid_map);
}
int child_main(){
 printf("进入子进程:%d\n",getpid());

 cap_t caps;
 set_uid_map(getpid(), 0, parent_uid, 1);
    set_gid_map(getpid(), 0, parent_gid, 1);
 caps = cap_get_proc();
 printf("capabilities: %s\n",cap_to_text(caps,NULL));
 char *arg[] = {"/bin/bash",NULL};
 char *newhostname = "UTSnamespace";
 //sethostname(newhostname,sizeof(newhostname));

 execv("/bin/bash",arg);
	
 return 1;
}


int main(void){

 printf("创建子进程\n");
 parent_uid = getuid();
 parent_gid = getgid();

 int child_pid = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWUSER | CLONE_NEWIPC | CLONE_NEWCGROUP | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | SIGCHLD,NULL);
	

 waitpid(child_pid,NULL,0);
 printf("退出子进程\n");

 return 0;

}

user namespace被创建后,第一个进程被认定为init进程,需要被赋予该namespace中的全部capabilities,这样才能完成所有必要的初始化工作。

文件系统

这个点得结合mount namespace一起说。mount namespace会隔离文件系统的挂载点,使得不同的mount namespace拥有独立的挂载信息,并且不会相互影响,当clone或者unshare创建新的mount namespace的时候,新创建的namespace会拷贝一份老namespace的挂载列表,从此之后相互之间的挂载卸载不会相互影响,这些对于构建专属文件系统目录非常有用。

网络

参考资料