namespace的6项隔离技术
目录:
linux的隔离系统调用
namespace | 系统调用参数 | 隔离内容 | 内核版本 |
---|---|---|---|
Mount | CLONE_NEWNS | 挂载点 | 2.4.19 |
UTS | CLONE_NEWUTS | 主机名域名 | 2.6.19 |
IPC | CLONE_NEWIPC | 信号量,消息队列和共享内存 | 2.6.19 |
PID | CLONE_NEWPID | 进程编号 | 2.6.24 |
Network | CLONE_NEWNET | 网络设备,网络栈和端口号等 | 2.6.29 |
User | CLONE_NEWUSER | 用户和用户组 | 3.8 |
namespace的操作通过clone(),setns(),unshare()和/proc下的文件实现
clone
clone()是Linux系统调用fork()的通用实现方式
int clone(int(*child_func)(void *), void *child_stack, int flags, void *arg);
- child_func 传入子进程运行的程序主函数
- child_stack 传入子进程使用的栈空间
- flags 使用的CLONE_*标志位
- args 用于用户传入的参数
setns
进程从原来的namespace加入一个已经存在的namespace,docker的exec就是这样操作的
unsetns
进程从原有的namespace隔离,docker并没有使用这个系统调用
proc文件
/proc/[pid]/ns目录
$ ll /proc/$$/ns
total 0
lrwxrwxrwx 1 mrwhy mrwhy 0 Aug 22 23:33 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 mrwhy mrwhy 0 Aug 22 23:33 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 mrwhy mrwhy 0 Aug 22 23:33 net -> net:[4026531956]
lrwxrwxrwx 1 mrwhy mrwhy 0 Aug 22 23:33 pid -> pid:[4026531836]
lrwxrwxrwx 1 mrwhy mrwhy 0 Aug 22 23:33 user -> user:[4026531837]
lrwxrwxrwx 1 mrwhy mrwhy 0 Aug 22 23:33 uts -> uts:[4026531838]
如果两个进程指向的同一个namespace,证明两个进程在一个namespace,作为软链,如果link文件被打开文件描述符,这个namespace会一直存在,其他的进程也可以挂载到namespace
$ mount --bind /proc/1234/ns/uts ~/uts
fork
fork调用的时候,会创建新的进程,并分配资源,例如存储数据和代码空间,然后原来进程的值复制到新的进程
fork调用调用一次却返回两次,父进程和子进程分别返回一次,通过返回值历来区分,子进程返回的是0,父进程返回的子进程的pid
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t fpid;
fpid=fork();
if (fpid < 0)printf("error in fork!");
else if (fpid == 0){
printf("Child process, pid is %d\n", getpid());
}
else{
printf("Parent process, pid is %d\n", getpid());
}
return 0;
}
编译执行一下
$ gcc -Wall fork_example.c
$ ./a.out
Parent process, pid is 15415
Child process, pid is 15416
UTS
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sched.h>
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
char* const child_args[] = {
"/bin/bash",
NULL
};
int child_main(void* args){
printf("在子进程中!\n");
sethostname("newnamespace", 12);
execv(child_args[0], child_args);
return 1;
}
int main(){
printf("程序运行: \n");
int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS | SIGCHLD, NULL);
waitpid(child_pid, NULL, 0);
printf("已退出\n");
return 0;
}
编译执行一下
[root@localhost ~] gcc -Wall uts.c -o uts.o
[root@localhost ~] ./uts.o
程序运行:
在子进程中!
[root@newnamespace ~]# exit
已退出
[root@localhost ~]
ipc
通过shell创建message queue
$ ipcmk -Q
Message queue id: 0
$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x5e9ee8bc 0 root 644 0 0
进入隔离ipc的namespace
$ ./ipc.o
程序运行:
在子进程中!
[root@newnamespace ~]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
$ exit
已退出
可以看到是没有之前声明的queue
PID
每个PID的namespace会有一个单独的PID计数器
内核中维护了PID的namespace树状结构,父namespace可以看到子namespace中的进程,并且可以通过信号对子namespace进程产生影响,而子namespace看不到父namespace进程
- PIDnamespace中第一个进程pid为1,和linux系统的init一样,当子进程成为孤儿进程,PID中的namespace的init进程维护
- 一个PIDnamespace进程,不会影响其他namespace的节点的进程
- PIDnamespace中重新挂载/proc,只会显示同属一个PIDnamespace中的其他进程
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sched.h>
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
char* const child_args[] = {
"/bin/bash",
NULL
};
int child_main(void* args){
printf("在子进程中!\n");
sethostname("newnamespace", 12);
execv(child_args[0], child_args);
return 1;
}
int main(){
printf("程序运行: \n");
int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
waitpid(child_pid, NULL, 0);
printf("已退出\n");
return 0;
}
进入隔离pid的namespace
$ gcc -Wall pid.c -o pid.o
$ ./pid.o
程序运行:
在子进程中!
$ echo $$
1
$ mount -t proc proc /proc
$ ls /proc/
1
33
...
$ exit
已退出
init进程有信号屏蔽的特权,如果init进程没有处理该信号的逻辑,namespace内其他进程发送的信号会被屏蔽,父PIDnamespace发送的,只会接受SIGKILL或SIGSTOP
NS
mount namespace是linux的第一个namespace,所以是CLONE_NEWNS,通过隔离文件系统挂载点对隔离文件系统提供支持
在创建mount namespace时,会将文件系统复制到新的namespace,新的挂载操作都会在新的namespace
挂载有挂载传播的功能,包括共享关系和从属关系
- 共享关系:如果两个挂载对象为共享关系,一个挂载对象中的事件会同步到另一个挂载对象
- 从属关系:如果两个挂载对象为主从关系,主挂载对象的事件会同步到从挂载对象,而反之不行
挂载关系
- 共享挂载share
- 主从挂载slave
- 共享主从挂载shared and slave
- 私有挂载private
- 不可绑定挂载unbindable
mount默认就是私有挂载,如果使用其他挂载方式可以指定--make-shared
和--make-slave
使用CLONE_NEWNS标志位
NET
隔离有网络设备,协议栈,路由表,防火墙,/proc/net,/sys/class/net和套接字等
使用CLONE_NEWNET标志位,创建的net隔离的namespace是独立出来网络,docker是通过vethpair进行通信,在通信之前是管道建立的连接
USER
usernamespace主要隔离了标识符,属性(用户,用户组,root目录,key密钥和特殊权限)
#define _GNU_SOURCE
#include <sys/capability.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sched.h>
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
char* const child_args[] = {
"/bin/bash",
NULL
};
int child_main(void* args){
printf("在子进程中!\n");
cap_t caps;
printf("eUID = %ld; eGID = %ld; ", (long)geteuid(), (long)getegid());
caps = cap_get_proc();
printf("capabilities: %s\n", cap_to_text(caps, NULL));
execv(child_args[0], child_args);
return 1;
}
int main(){
printf("程序运行: \n");
int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
waitpid(child_pid, NULL, 0);
printf("已退出\n");
return 0;
}
构建
$ yum install -y libcap-devel
$ gcc -Wall -lcap user.c -o user.o
$ ./user.o
程序运行:
在子进程中!
eUID = 0; eGID = 0; capabilities: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,35,36+ep
$ exit
已退出