namespace
namespace的6项隔离
namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名与域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网线栈、端口等 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户和用户组 |
namespace的API
API | 说明 |
---|---|
clone() | 在创建新进程的同时创建namespace。docker使用namespace的基本方法 |
/proc/[pid]/ns/ | 查看namespace号。docker使用 |
setns() | 加入一个已经存在的namespace。docker中的docker exec命令 |
unshare() | 在原先进程上进行namespace隔离。docker目前没有使用到这个系统调用 |
int clone(int (child_func)(void ),void child_stack,int flags,void arg);
- child_func:传入子进程运行的程序主函数
- child_stack:传入子进程使用的栈空间
- flags:使用哪些namespace的标志位CLONE_*
- args:可用于传入用户参数
int setns(int fd,int nstype);
- fd:表示要加入namespace的文件描述符
- nstype:检查fd指向的namespace类型是否符合实际要求,参数0表示不检查。
fork():复制原来进程从而创建一个新的进程,原来进程为父进程,创建新进程为子进程。
- 父子进程同时执行剩下的代码,fork()之前的代码由父进程执行完毕
- 子进程pid=父进程pid+1
- 父进程返回子进程的pid,子进程返回0,返回负值表示出现错误
- 表现:fork()执行一次,却能返回2次
UTS
UTS(UNIX Time-sharing System):主机名和域名隔离
# 实验
gcc -Wall uts.c -o uts.o && ./uts.o ## 编译并运行
hostname ## 验证,查看主机名是否被修改
exit ## 退出
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.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); // 加入UTS隔离
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); // 加入UTS隔离
waitpid(child_pid,NULL,0);
printf("已退出\n");
return 0;
}
IPC
IPC(Inter-Process Communication):进程间通信隔离
- 信号量
- 消息队列
- 共享内存
# 实验
ipcmk -Q ## 创建一个消息队列
ipcs -q ## 查看消息队列
vim ipc.c # 编辑实验文件ipc.c
gcc -Wall ipc.c -o ipc.o && ./ipc.o # 编译并运行
ipcs -q # 子进程中查看是否隔离了消息队列
exit
# 其他参考uts.c
...
int child_pid = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD,NULL); // 加入IPC隔离
...
PID
PID:进程隔离
- 内核维护的所有PID namespace是一个树状结构
- 最顶层的PID namespace称为root namespace,父节点可以看到子节点中的进程并对进程产生影响(信号),而子节点却不能看到父节点中的任何内容
- 每个PID namespace中的第一个进程"PID 1",都会像传统Linux中的init进程一样拥有特权,起特殊作用
- 一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace中没有任何意义
- 如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程
- 在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程
# 实验
vim pid.c
...
int child_pid = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWPID |CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD,NULL); // 加入PID隔离
...
gcc -Wall pid.c -o pid.o && ./pid.o # 编译并运行
echo $$ # 查看当前shell的PID
exit
echo $$ # 对比,确认进程是否实现隔离
init进程
所有进程的父进程,维护着一张进程表,负责检测进程的状态,若进程变为孤儿进程,则会结束进程,回收资源。 传统Unix系统:init PID 1 容器:bash PID 1 相同的管理能力:资源监控与回收信号屏蔽
如果init中没有编写处理某个信号的代码逻辑,那么与init在同一个PID namespace下的进程发送给它的该信号都会被屏蔽。 PID namespace中的init进程特权之一,目的是防止init进程被误杀。 父节点有权发送SIGKILL(销毁进程)或SIGSTOP(暂停进程)信号强制终止子节点的init进程 一旦init被销毁,同一PID namespace中的其他进程也会接收到SIGKILL信号而被销毁挂载proc文件系统
PID namespace下使用ps/top这些命令还是看到原来系统的所有进程,是因为/proc文件系统没有挂载到当前PID namespace下,若想看到当前的PID namespace进程,可以重新挂载/proc。
./pid.o
mount -t proc proc /proc # 重新挂载/proc文件系统
ps -a # 检测
exit
此时没有进行mount namespace的隔离,在PID namespace下重新挂载/proc文件系统后,其实际位置已经改变,重新返回到系统进程下会出问题,若想解决,需要重新挂载一次/proc文件系统,即在当前系统进程下执行 mount -t proc proc /proc
- unshare()和setns()
unshare()和setns()在创建了新的PID namespace后,不会直接进入新的PID namespace,而是产生子进程进去,成为新namespace中的init进程。因为进程一旦创建,其PID的值和PID namespace的关系就基本确定,不会改变,一旦改变,程序就会崩溃。
Mount
mount namespace:文件系统挂载点隔离
挂载状态 | 描述 |
---|---|
共享挂载(share) | 共享对象发生变化时,其他对象也会同步发生变化 |
从属挂载(slave) | 主对象发生变化时,从对象才发生变化;从对象变化,主对象不受影响 |
共享/从属挂载(shared and slave) | 主对象发生变化后,从对象同步变化并把变化共享到其他共享对象 |
私有挂载(private) | 对象私有独立,各自变化,不受影响 |
不可绑定挂载(unbindable) | 对象一般为管理员所有,不能被其他对象挂载绑定 |
# 设置挂载状态
mount --make-shared 对象 # 共享
mount --make-slave 对象 # 从属
mount --make-shared 从对象 # 共享/从属
mount --make-private 对象 # 私有
mount --make-unbindable 对象 # 不可绑定
vim mount.c
...
int child_pid = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWNS |CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD,NULL); // 加入mount隔离
...
# 注意:实验结果不能做到挂载卸载隔离,不知道原因!
Network
network namespace:网络隔离
- 网络设备
- IPv4和IPv6协议栈
- IP路由表
- 防火墙
- /proc/net
- /sys/class/net
- socket(套接字)
vim network.c
...
int child_pid = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWNET | CLONE_NEWNS |CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD,NULL); // 加入network隔离
...
User
user namespace:用户隔离,主要隔离了安全相关的identifier(标识符)和attribute(属性),即权限
- 用户ID
- 用户组ID
- root目录
- key
- 特殊权限
用户映谢:把user namespace的用户与外部父user namespace的用户进行映谢,以保证对外部进行操作时可以验证用户的权限
编辑的文件:
- /proc/[pid]/uid_map
- /proc/[pid]/gid_map
格式:用户/组ID 外部用户/组ID 映谢数量
- user namespace这个技术并不成熟,目前还有一些文件系统并不支持,原因是安全问题。
- 用户映谢只能由父user namespace或子user namespace执行一次,只能添加,不能修改。
# 实验:以普通用户执行
# 普通用户为1000,
# 特权用户为0,
# 65534表示有全部权限但是没有映谢外部用户,显示nobody
id -u
id -g
vim user.c
gcc user.c -Wall -lcap -o user.o && ./user.o
...
#include <sys/capability.h> # 增加
...
# 修改如下
void set_uid_map(pid_t pid,int inside_id,int outside_id,int length){
char path[256];
sprintf(path,"/proc/%d/uid_map",getpid());
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){
char path[256];
sprintf(path,"/proc/%d/gid_map",getpid());
FILE* gid_map = fopen(path,"w");
fprintf(gid_map,"%d %d %d",inside_id,outside_id,length);
fclose(gid_map);
}
int child_main(void* args){
cap_t caps;
printf("在子进程中!\n");
set_uid_map(getpid(),0,1000,1);
set_gid_map(getpid(),0,1000,1);
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 | SIGCHLD,NULL);
waitpid(child_pid,NULL,0);
printf("已退出\n");
return 0;
}
实验结果失效,按理论来说应该会进入namespace,并且初始用户拥有所有权限并映谢到父user namespace的root用户,实现了用户的隔离:命名空间用户与外部用户的权限无关;但是程序运行结果如下:
原因猜测: 内核编译时没有开启USER_NS