文档章节

Docker 基础技术:Linux Namespace(下)

linuxprobe
 linuxprobe
发布于 2016/09/29 23:09
字数 2707
阅读 8
收藏 0

Docker 基础技术:Linux Namespace(下)Docker 基础技术:Linux Namespace(下)

User Namespace

User Namespace主要是用了CLONE_NEWUSER的参数,使用了这个参数后,内部看到的UID和GID已经与外部不同了。默认情况下容器没有的UID,系统自动设置上了最大的UID65534,默认UID的定义文件在“/proc/sys/kernel/overflowuid”。
要把容器中的uid和真实系统的uid给映射在一起,需要修改 /proc/$$/uid_map 和/proc/$$/gid_map 这两个文件。这两个文件的格式为:

ID-inside-ns ID-outside-ns length

PS:
第一个字段ID-inside-ns表示在容器显示的UID或GID,
第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。
第三个字段表示映射的范围,一般填1,表示一一对应。

例:把真实的uid=1000映射成容器内的uid=0

$cat/proc/2465/uid_map
         0       1000          1

例:把namespace内部的uid映射到外部设置整形

把namespace内部从0开始的uid映射到外部从0开始的uid,其最大范围是无符号32位整形

$cat/proc/$$/uid_map
          0          0          4294967295

需要注意的是:

写这两个文件的进程需要这个namespace中的CAP_SETUID (CAP_SETGID)权限(可参看Capabilities)写入的进程必须是此user namespace的父或子的user namespace进程。
另外需要满如下条件之一:

1)父进程将effective uid/gid映射到子进程的user namespace中

2)父进程如果有CAP_SETUID/CAP_SETGID权限,那么它将可以映射到父进程中的任一uid/gid。

附:一些其他规则

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define STACK_SIZE (1024 * 1024)
staticcharcontainer_stack[STACK_SIZE];
char*constcontainer_args[] = {
    “/bin/bash”,
    NULL
};
intpipefd[2];
voidset_map(char* file,intinside_id,intoutside_id,intlen) {
    FILE* mapfd =fopen(file,”w”);
    if(NULL == mapfd) {
        perror(“open file error”);
        return;
    }
    fprintf(mapfd,”%d %d %d”, inside_id, outside_id, len);
    fclose(mapfd);
}
voidset_uid_map(pid_t pid,intinside_id,intoutside_id,intlen) {
    charfile[256];
    sprintf(file,”/proc/%d/uid_map”, pid);
    set_map(file, inside_id, outside_id, len);
}
voidset_gid_map(pid_t pid,intinside_id,intoutside_id,intlen) {
    charfile[256];
    sprintf(file,”/proc/%d/gid_map”, pid);
    set_map(file, inside_id, outside_id, len);
}
intcontainer_main(void* arg)
{
    printf(“Container [%5d] – inside the container!/n”, getpid());
    printf(“Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld/n”,
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
    /* 等待父进程通知后再往下执行(进程间的同步) */
    charch;
    close(pipefd[1]);
    read(pipefd[0], &ch, 1);
    printf(“Container [%5d] – setup hostname!/n”, getpid());
    //set hostname
    sethostname(“container”,10);
    //remount “/proc” to make sure the “top” and “ps” show container’s information
    mount(“proc”,”/proc”,”proc”, 0, NULL);
    execv(container_args[0], container_args);
    printf(“Something’s wrong!/n”);
    return1;
}
intmain()
{
    constintgid=getgid(), uid=getuid();
    printf(“Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld/n”,
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
    pipe(pipefd);
    printf(“Parent [%5d] – start a container!/n”, getpid());
    intcontainer_pid = clone(container_main, container_stack+STACK_SIZE,
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
    printf(“Parent [%5d] – Container [%5d]!/n”, getpid(), container_pid);
    //To map the uid/gid,
    //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    //The file format is
    //   ID-inside-ns   ID-outside-ns   length
    //if no mapping,
    //   the uid will be taken from /proc/sys/kernel/overflowuid
    //   the gid will be taken from /proc/sys/kernel/overflowgid
    set_uid_map(container_pid, 0, uid, 1);
    set_gid_map(container_pid, 0, gid, 1);
    printf(“Parent [%5d] – user/group mapping done!/n”, getpid());
    /* 通知子进程 */
    close(pipefd[1]);
    waitpid(container_pid, NULL, 0);
    printf(“Parent – container stopped!/n”);
    return0;
}

上面的程序,我们用了一个pipe来对父子进程进行同步,为什么要这样做?因为子进程中有一个execv的系统调用,这个系统调用会把当前子进程的进程空间给全部覆盖掉,我们希望在execv之前就做好user namespace的uid/gid的映射,这样,execv运行的/bin/bash就会因为我们设置了uid为0的inside-uid而变成#号的提示符。

整个程序的运行效果如下:

hchen@ubuntu:~$id
uid=1000(hchen) gid=1000(hchen)groups=1000(hchen)
hchen@ubuntu:~$ ./user#< –以hchen用户运行
Parent: eUID = 1000;  eGID = 1000, UID=1000, GID=1000
Parent [ 3262] – start a container!
Parent [ 3262] – Container [ 3263]!
Parent [ 3262] – user/groupmappingdone!
Container [    1] – inside the container!
Container: eUID = 0;  eGID = 0, UID=0, GID=0#<—Container里的UID/GID都为0了
Container [    1] – setuphostname!
root@container:~# id #<—-我们可以看到容器里的用户和命令行提示符是root用户了
uid=0(root) gid=0(root)groups=0(root),65534(nogroup)

我们注意到,User Namespace是以普通用户运行,但是别的Namespace需要root权限,那么,如果我要同时使用多个Namespace,该怎么办呢?一般来说,我们先用一般用户创建User Namespace,然后把这个一般用户映射成root,在容器内用root来创建其它的Namesapce,这样可以提高容器的安全性。

Network Namespace

在Linux下,我们一般用ip命令创建Network Namespace,但是Docker的源码中,它没有用ip命令用了Raw Socket发些“奇怪”的数据,我以ip命令分析一下。

docker网络分析

首先,我们先看个图,下面这个图基本上就是Docker在宿主机上的网络示意图

 

Docker 基础技术:Linux Namespace(下)Docker 基础技术:Linux Namespace(下)


Docker在运行中可能使用到的私有网段有:172.40.1.0和10.0.0.0、192.168.0.0这三个个私有网段,如果你的环境已经使用了这三个私有网段docker启动时就会出错。当你启动一个Docker容器后,你可以使用ip link show或ip addr show来查看当前宿主机的网络情况(我们可以看到有一个docker0,还有一个veth22a38e6的虚拟网卡——给容器用的):

 

hchen@ubuntu:~$ ip link show
1: lo:  mtu 65536 qdisc noqueue state …
    link/loopback00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0:  mtu 1500 qdisc …
    link/ether00:0c:29:b7:67:7d brd ff:ff:ff:ff:ff:ff
3: docker0:  mtu 1500 …
    link/ether56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff
5: veth22a38e6:  mtu 1500 qdisc …
    link/ether8e:30:2a:ac:8c:d1 brd ff:ff:ff:ff:ff:ff

那么,要做成这个样子应该怎么办呢?我们来看一组命令:

## 首先,我们先增加一个网桥lxcbr0,模仿docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfiglxcbr0 192.168.10.1/24up#为网桥设置IP地址
## 接下来,我们要创建一个network namespace – ns1
# 增加一个namesapce 命令为 ns1 (使用ip netns add命令)
ip netns add ns1
# 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1来操作ns1中的命令)
ip netnsexecns1   ip linksetdev lo up
## 然后,我们需要增加一对虚拟网卡
# 增加一个pair虚拟网卡,注意其中的veth类型,其中一个网卡要按进容器中
ip link add veth-ns1typeveth peer name lxcbr0.1
# 把 veth-ns1 按到namespace ns1中,这样容器中就会有一个新的网卡了
ip linksetveth-ns1 netns ns1
# 把容器里的 veth-ns1改名为 eth0 (容器外会冲突,容器内就不会了)
ip netnsexecns1  ip linksetdev veth-ns1 name eth0
# 为容器中的网卡分配一个IP地址,并激活它
ip netnsexecns1ifconfigeth0 192.168.10.11/24up
# 上面我们把veth-ns1这个网卡按到了容器中,然后我们要把lxcbr0.1添加上网桥上
brctl addif lxcbr0 lxcbr0.1
# 为容器增加一个路由规则,让容器可以访问外面的网络
ip netnsexecns1     ip route add default via 192.168.10.1
# 在/etc/netns下创建network namespce名称为ns1的目录,
# 然后为这个namespace设置resolv.conf,这样,容器内就可以访问域名了
mkdir-p/etc/netns/ns1
echo”nameserver 8.8.8.8″>/etc/netns/ns1/resolv.conf

上面基本上就是docker网络的原理了,只不过,Docker的resolv.conf没有用这样的方式,而是用了Mount Namesapce的那种方式。另外,docker是用进程的PID来做Network Namespace的名称的。

为docker容器增加一个新的网卡:

ip link add peerAtypeveth peer name peerB
brctl addif docker0 peerA
ip linksetpeerA up
ip linksetpeerB netns ${container-pid}
ip netnsexec${container-pid} ip linksetdev peerB name eth1
ip netnsexec${container-pid} ip linkseteth1 up ;
ip netnsexec${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ;

上面的示例是我们为正在运行的docker容器,增加一个eth1的网卡,并给了一个静态的可被外部访问到的IP地址。

这个需要把外部的“物理网卡”配置成混杂模式,这样这个eth1网卡就会向外通过ARP协议发送自己的Mac地址,然后外部的交换机就会把到这个IP地址的包转到“物理网卡”上,因为是混杂模式,所以eth1就能收到相关的数据,一看,是自己的,那么就收到。这样,Docker容器的网络就和外部通了。

当然,无论是Docker的NAT方式,还是混杂模式都会有性能上的问题,NAT不用说了,存在一个转发的开销,混杂模式呢,网卡上收到的负载都会完全交给所有的虚拟网卡上,于是就算一个网卡上没有数据,但也会被其它网卡上的数据所影响。

这两种方式都不够完美,我们知道,真正解决这种网络问题需要使用VLAN技术,Google为Linux内核实现了一个IPVLAN的驱动,这基本上就是为Docker量身定制的。

Namespace文件

首先我们运行一下上篇中的那个pid.mnt的程序(也就是PID Namespace中那个mount proc的程序),然后不要退出。

$ sudo ./pid.mnt
[sudo] passwordforhchen:
Parent [ 4599] – start a container!
Container [    1] – inside the container!

然后我们到另一个shell中查看一下父子进程的PID:

hchen@ubuntu:~$ pstree -p 4599
pid.mnt(4599)───bash(4600)

我们可以到proc下(/proc/$$/ns)查看进程的各个namespace的id(内核版本需要3.8以上)。

下面展示的是父进程的:

hchen@ubuntu:~$sudols-l/proc/4599/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026531838]

下面是展示的是子进程的:

hchen@ubuntu:~$sudols-l/proc/4600/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026532520]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026532522]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026532521]

我们可以看到,其中的ipc,net,user是同一个ID,而mnt,pid,uts都是不一样的。如果两个进程指向的namespace编号相同,就说明他们在同一个namespace下,否则则在不同namespace里面。一旦这些文件被打开,只要其fd被占用着,那么就算PID所属的所有进程都已经结束,创建的namespace也会一直存在。比如:我们可以通过:mount –bind /proc/4600/ns/uts ~/uts 来hold这个namespace。

另外,我们在上篇中讲过一个setns的系统调用,其函数声明如下:

intsetns(intfd,intnstype);

其中第一个参数就是一个fd,也就是一个open()系统调用打开了上述文件后返回的fd,比如:

fd = open(“/proc/4600/ns/nts”, O_RDONLY); // 获取namespace文件描述符
setns(fd, 0);// 加入新的namespace

免费提供最新Linux技术教程书籍,为开源技术爱好者努力做得更多更好:http://www.linuxprobe.com/

本文转载自:http://www.linuxprobe.com/docker-linux-namespace-2.html

共有 人打赏支持
linuxprobe
粉丝 21
博文 257
码字总数 45072
作品 0
朝阳
私信 提问
Docker技术三大要点:cgroup, namespace和unionFS的理解

www.docker.com的网页有这样一张有意思的动画: 从这张gif图片,我们不难看出Docker网站想传达这样一条信息, 使用Docker加速了build,ship和run的过程。 Docker最早问世是2013年,以一个开源...

jerrywangsap
2018/12/21
0
0
你应当了解的 Docker 底层技术

本文已获得原作者 七把刀_授权。 Docker 容器技术已经发展了好些年,在很多项目都有应用,线上运行也很稳定。整理了部分 Docker 的学习笔记以及新版本特性,对Docker 感兴趣的同学可以看看,...

掘金官方
2018/07/06
0
0
『中级篇』 Linux网络命名空间(25)

>原创文章,欢迎转载。转载请注明:转载自IT人故事会,谢谢! >原文链接地址:『中级篇』 Linux网络命名空间(25) docker底层技术,非常重要的关于namespace,network的namespace看看到底是...

IT人故事
2018/08/18
0
0
Linux Namespace : 简介

在初步的了解 docker 后,笔者期望通过理解 docker 背后的技术原理来深入的学习和使用 docker,接下来的几篇文章简单的介绍下 linux namespace 的概念以及基本用法。 namespace 的概念 name...

sparkdev
2018/07/26
0
0
『中级篇』 Docker Bridge详解(26)

上节主要学习了network-namespace,并创建了network-namespace,并把2个network-namespace连接在一起,我们也演示了创建一个容器test1和test2,其实在创建容器的同时也创建了对应的一个netwo...

IT人故事会
2018/07/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

租房软件隐私保护如同虚设

近日,苏州市民赵先生向江苏新闻广播新闻热线025-84658888反映,他在“安居客”手机应用软件上浏览二手房信息,并且使用该软件自动生成的虚拟号码向当地一家中介公司进行咨询。可电话刚挂不久...

linux-tao
今天
1
0
分布式项目(五)iot-pgsql

书接上回,在Mapping server中,我们已经把数据都整理好了,现在利用postgresql存储历史数据。 iot-pgsql 构建iot-pgsql模块,这里我们写数据库为了性能考虑不在使用mybatis,换成spring jd...

lelinked
今天
4
0
一文分析java基础面试题中易出错考点

前言 这篇文章主要针对的是笔试题中出现的通过查看代码执行结果选择正确答案题材。 正式进入题目内容: 1、(单选题)下面代码的输出结果是什么? public class Base { private Strin...

一看就喷亏的小猿
今天
2
0
cocoapods 用法

cocoapods install pod install 更新本地已经install的仓库 更新所有的仓库 pod update --verbose --no-repo-update 更新制定的仓库 pod update ** --verbose --no-repo-update...

HOrange
今天
3
0
linux下socket编程实现一个服务器连接多个客户端

使用socekt通信一般步骤 1)服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept()等待客户端连接。 2)客户端:socker()建立套接字,连接(connect)服务器,连接上后...

shzwork
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部