chroot就是修改系统的根目录(change root directory),在使用chroot命令后,系统读取到的目录和文件将不在是旧根系统下的而是新根下的目录结构和文件,大概有3大好处:
增加了系统的安全性,限制了用户的权力: 在经过 chroot 之后,在新根下将访问不到旧系统的根目录结构和文件,这样就增强了系统的安全性。这个一般是在登录 (login) 前使用 chroot,以此达到用户不能访问一些特定的文件。 建立一个与原系统隔离的系统目录结构,方便用户的开发: 使用 chroot 后,系统读取的是新根下的目录和文件,这是一个与原系统根下文件不相关的目录结构。在这个新的环境中,可以用来测试软件的静态编译以及一些与系统不相关的独立开发。 切换系统的根目录位置,引导 Linux 系统启动以及急救系统等: chroot 的作用就是切换系统的根位置,而这个作用最为明显的是在系统初始引导磁盘的处理过程中使用,从初始 RAM 磁盘 (initrd) 切换系统的根位置并执行真正的 init。另外,当系统出现一些问题时,我们也可以使用 chroot 来切换到一个临时的系统。
语法
chroot RootDir /bin/sh:该指令的作用是从DootDir下寻找/bin/sh并执行,即打开一个shell。
就像chroot允许进程将任意目录作为系统的根目录医院(独立于其余的进程),Linux命名空间允许操作系统的别的方面也被独立的修改,例如进程树、网络接口、挂载点、进程通信资源等等。
为什么使用命名空间进行进程隔离
一台服务器上可能运行多个服务,假设其中一个服务被攻击者入侵了,他可能用同样的方法来入侵其他的服务,甚至可能入侵整个服务器,命名空间隔离能够提供一个安全的环境来消除这个威胁。 在一个虚拟机中运行进程,虚拟机会在你的操作系统之上模拟一个硬件层,然后在硬件层之上运行别的操作系统,命名空间相比于虚拟机更加轻量级,然而能确保一个相同级别的进程隔离。
进程命名空间
Linux维护了一个进程树,进程树包含了每一个当前运行在父子层次进程的引用,一个进程如果给它充分的特权和满足特定的条件,它能够通过附加一个跟踪程序来检查另一个程序,甚至可以终止它。 随着Linux namespace的引入,多个嵌套的进程树成为了可能,每一个进程树能拥有一组完全隔离的进程,这可以确保属于一个进程树的进程不能检查或被杀,实际上甚至无法知道其他兄弟进程树或父进程树中进程的存在。 Linux刚开启会启动一个init进程,该进程的PID为1,这个进程是进程树根,它通过执行合适的维持工作和开启正确的守护进程/服务来初始化系统的剩余部分,所有的其他进程在树中会位于这个进程之下,PID namespace允许一个进程派生出一个新树,这个树带有它自己的PID为1的进程,这个进程保留在父命名空间中的原始树中,但使子进程成为其自己进程树的根。 使用PID进程空间隔离,在子命名空间中的进程没有办法知道父进程的存在,但是,在父命名空间的进程却能够拥有子命名空间进程的完整视图,就好像是任何在父命名空间中的其他进程一样。 随着PID命名空间的引入,一个单一的进程可以拥有与之关联的多个PID,它所属的每个命名空间都有一个PID,在Linux源代码中,我们可以看到一个叫作pid的结构体,这个结构体过去值记录单个PID,现在通过使用一个叫作upid的结构体来记录多个PID。
struct upid
{
int nr;
struct pid_namespace * ns;
}
struct pid{
int level;
struct upid numbers[ 0 ] ;
}
为了创建一个新的PID命名空间,必须使用clone()系统调用,并且需要设置一个特殊的标志位CLONE_NEWPID,但是下面讨论的别的命名空间可以通过unshare()系统调用创建,一个PID命名空间仅能够在使用clone()生成新进程的时候创建,一旦clone()设置了这个标志位被调用后,一个新的进程会在新的进程树、新的PID命名空间中被启动。
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[ 1048576 ] ;
static int child_fn ( )
{
printf ( "PID: %ld\n" , ( long ) getpid ( ) ) ;
return 0 ;
}
int main ( )
{
pid_t child_pid = clone ( child_fn, child_stack+ 1048576 , CLONE_NEWPID | SIGCHLD, NULL ) ;
printf ( "clone()=%ld\n" , ( long ) child_pid) ;
waitpid ( child_pid, NULL , 0 ) ;
return 0 ;
}
root@Ubuntu:~
root@Ubuntu:~
clone( ) = 13765
PID: 1
可以看到这个进程在父进程命名空间的PID为13765,在自己新创建的命名空间中PID为1. clone()函数通过克隆当前的进程创建了一个新的进程,并且开始执行child_fn()函数开始的代码,它会将新进程从原始的进程树中分离出来,为这个新进程创建一个独立的进程树。 修改child_fn()代码如下:
static int child_fn ( )
{
printf ( "Parent PID:%ld\n" , ( long ) getppid ( ) ) ;
return 0 ;
}
root@Ubuntu: ~ # gcc - o cloneDemo cloneDemo. c
root@Ubuntu: ~ # . / cloneDemo
clone ( ) = 14102
Parent PID: 0
从进程隔离的角度来看父进程的PID为0,表示没有父进程,接来下我们从clone()函数移除CLONE_NEWPID标志位再次运行程序:
pid_t child_pid = clone ( child_fn, child_stack+ 1048576 , SIGCHLD, NULL ) ;
root@Ubuntu:~
clone( ) = 14124
Parent PID: 14123
可是,这只是第一步,这些进程仍然可以不受限制的访问别的公共的或者共享的资源,例如,网络接口:如果一个上面创建的子进程监听80端口,那么系统上其他进程都无法监听它。
Linux网络命名空间
网络命名空间允许这些进程中的每个进程看见一个完全不同的网络接口,即使是本地环回接口对于每个网络命名空间来说都是不同的。 隔离进程到它自己的网络命名空间涉及到使用clone()函数的另一个标志位CLONE_NEWNET:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[ 1048576 ] ;
static int child_fn ( )
{
printf ( "New `net` Namespace:\n" ) ;
system ( "ip link" ) ;
printf ( "\n\n" ) ;
return 0 ;
}
int main ( )
{
printf ( "Original `net` Namespace:\n" ) ;
system ( "ip link" ) ;
printf ( "\n\n" ) ;
pid_t child_pid = clone ( child_fn, child_stack+ 1048576 , CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL ) ;
printf ( "clone()=%ld\n" , ( long ) child_pid) ;
waitpid ( child_pid, NULL , 0 ) ;
return 0 ;
}
root@Ubuntu: ~ # gcc - o cloneDemo cloneDemo. c
root@Ubuntu: ~ # . / cloneDemo
Original `net` Namespace:
1 : lo: < LOOPBACK, UP, LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ loopback 00 : 00 : 00 : 00 : 00 : 00 brd 00 : 00 : 00 : 00 : 00 : 00
2 : enp0s5: < BROADCAST, MULTICAST, UP, LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ ether 00 : 1 c: 42 : 46 : 13 : 0 e brd ff: ff: ff: ff: ff: ff
clone ( ) = 14164
New `net` Namespace:
1 : lo: < LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ loopback 00 : 00 : 00 : 00 : 00 : 00 brd 00 : 00 : 00 : 00 : 00 : 00
物理网络接口enp0s5属于全局网络命名空间,但是这个物理网络接口在新的网络命名空间中不可用,而且,本地环回接口是激活的在原始的网络命名空间中,但是在子网络命名空间中是关闭的。 为了在子命名空间中提供一个有用的网络接口,有必要设置额外的跨多个命名空间的虚拟网络接口,一旦接口设置完成,就可以创建以太网桥,甚至在多个命名空间中路由数据包。最后,要使整个工作正常,一个路由进程必须运行在全局网络命名空间来从不同的物理接口接收流量,通过虚拟网络可接口路由它们到正确的子网络命名空间。 可以从父命名空间中通过运行一个简单的命令来在父命名空间和子命名空间之间创建一对虚拟以太网连接:
ip link add name veth0 type veth peer name veth1 netns < pid>
<pid>需要被替换成父进程观察到在子命名空间中进程的进程ID,运行这个命令会在两个命名空间之间建立一条类似管道的连接,父命名空间保持veth0设备,传递veth1设备给子命名空间,在一端进入的任何事物,都会在另一端出现,因此,必须为虚拟以太网连接的两侧分配IP地址。
挂载命名空间
Linux还为系统的所有挂载点维持了一个数据结构,包括哪些磁盘分区被挂载了、它们被挂载在哪里、它们是否是只读的等等,使用Linux命名空间,可以克隆这个数据结构,因此在不同命名空间中的进程能够改变这些挂载点,但是互不影响。 创建一个隔离的挂载命名空间,可使这些隔离的进程中的每个进程对整个系统的挂载点结构的视图于原始视图完全不同,允许你为每个进程以及这些进程特定的其他挂载点使用不同的根。 实现挂载点命名空间是需要在clone函数添加以下标志位 - CLONE_NEWNS:
clone ( child_fn, child_stack+ 1048576 , CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL ) ;
最初,子进程会和父进程拥有完全相同的挂载点,可是,在一个新的挂载命名空间下,子进程能够挂载或者卸载它想要的任何挂载点,这个改变不会影响父进程的命名空间,也不会影响整个系统中的任何其他的命名空间。
别的命名空间
还可以将这些进程隔离到其他命名空间中,即user、IPC和UTS。用户命名空间允许进程在命名空间内拥有根权限,而不授予它访问命名空间外进程的权限。通过IPC命名空间隔离进程可以为它提供自己的进程间通信资源,例如systemvipc和POSIX消息。UTS命名空间隔离系统的两个特定标识符:nodename和domainname。 下面一段程序展示了UTS命名空间是怎么样被隔离的:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[ 1048576 ] ;
static void print_nodename ( )
{
struct utsname utsname;
uname ( & utsname) ;
printf ( "%s\n" , utsname. nodename) ;
}
static int child_fn ( )
{
printf ( "New UTS namespace nodename:" ) ;
print_nodename ( ) ;
printf ( "Changing nodename inside new UTS namespace\n" ) ;
sethostname ( "GLaDOS" , 6 ) ;
printf ( "New UTS namespace nodename:" ) ;
print_nodename ( ) ;
return 0 ;
}
int main ( )
{
printf ( "Original UTS namespace nodename:" ) ;
print_nodename ( ) ;
pid_t child_pid = clone ( child_fn, child_stack+ 1048576 , CLONE_NEWUTS | SIGCHLD, NULL ) ;
sleep ( 1 ) ;
printf ( "Original UTS namespace nodename:" ) ;
print_nodename ( ) ;
waitpid ( child_pid, NULL , 0 ) ;
return 0 ;
}
root@Ubuntu:~
root@Ubuntu:~
Original UTS namespace nodename:Ubuntu
New UTS namespace nodename:Ubuntu
Changing nodename inside new UTS namespace
New UTS namespace nodename:GLaDOS
Original UTS namespace nodename:Ubuntu
child_fn()打印了nodename,并且改变了它的值,然后再次打印它。显然,这个改变直线新的UTS命名空间内部生效。
跨命名空间通信
有必要在父命名空间与子命名空间中建立一些通信,这可能是为了在一个隔离的环境中做一些配置工作或者它能简单的保持从外部窥探环境条件的能力,实现跨命名空间通信的一种方式是维持一个SSH守护进程,在每一个网络命名空间的内部都有一个隔离的SSH守护进程,可是多个SSH守护进程运行会消耗很多宝贵的资源例如内存,在这里采用特殊的init进程再次被证明是一个好主意。 init进程可以在父命名空间与子命名空间建立一个通信通道,这个通道可以基于UNIX套接字或者甚至使用TCP,为了创建一个UNIX套接字横跨两个网络命名空间,你需要首先创建子进程,然后创建UNIX套接字,在隔离子进程到一个单独的网络命名空间,Linux提供了一个unshare()系统调用,这个系统调用允许一个进程将自己与原来的命名空间隔离,而不是让父进程首先隔离子进程。 下面这段代码与上述的网络命名空间隔离具有相同效果:
```c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[ 1048576 ] ;
static int child_fn ( )
{
unshare ( CLONE_NEWNET) ;
printf ( "New `net` Namespace:\n" ) ;
system ( "ip link" ) ;
printf ( "\n\n" ) ;
return 0 ;
}
int main ( )
{
printf ( "Original `net` Namespace:\n" ) ;
system ( "ip link" ) ;
printf ( "\n\n" ) ;
pid_t child_pid = clone ( child_fn, child_stack+ 1048576 , CLONE_NEWPID | SIGCHLD, NULL ) ;
waitpid ( child_pid, NULL , 0 ) ;
return 0 ;
}
这个特殊的init进程是你自己设计的,你能够让这个特殊的init进程做所有必须的工作,在执行目标的子进程之前将其与系统的其余部分隔离。