【译】使用rust来写一个容器(八)

  1. 1. 在容器中运行二进制应用
    1. 1.1. execve 系统调用
      1. 1.1.1. 测试
        1. 1.1.1.1. 动态链接的二进制应用
        2. 1.1.1.2. 静态编译测试二进制应用
        3. 1.1.1.3. Patch for this step
    2. 1.2. 挂载附加路径
      1. 1.2.1. 测试
        1. 1.2.1.1. Patch for this step
    3. 1.3. 结束语
      1. 1.3.1. 从这个项目到Docker
      2. 1.3.2. 教程结束

本文章为对 Litchi Pi《Writing a container in Rust》的翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Litchi Pi 保有。个人翻译能力有限,如有疑问可查看原文。

在容器中运行二进制应用

我们的容器基本上已经可以工作了,接下来只需要代码中添加二进制应用执行逻辑即可。

execve 系统调用

当Linux运行一个软件时,无论是二进制应用还是text脚本(如果第一行是 #!<interpreter>),它都会调用带有3个参数的execve系统调用:

  • 脚本/二进制应用的路径
  • 传递给可执行文件的参数
  • 要设置的环境变量

Linux 内核将用该二进制应用/脚本进程替换当前进程。 这也是我们在调用这个系统调用之前需要克隆我们的主进程的原因。

此处查阅execve系统调用相关文档。

目前一切都准备就绪了,我们只需要在src/child.rschild函数中调用系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use nix::unistd::execve;
use std::ffi::CString;

fn child(config: ContainerOpts) -> isize {

// ...

log::info!("Starting container with command {} and args {:?}", config.path.to_str().unwrap(), config.argv);
let retcode = match execve::<CString, CString>(&config.path, &config.argv, &[]){
Ok(_) => 0,
Err(e) => {
log::error!("Error while trying to perform execve: {:?}", e);
-1
}
};
retcode
}

这里唯一棘手的一点是告诉 Rust 我们正在使用 CString类型,因为它必须与原始 C 有一些兼容性。

如果这一步成功,execve函数将永远不会返回,因为该进程将被运行可执行文件的进程替换。

我们还要检查该命令是否由用户提供并且有效,接下来修改在src/cli.rs中的parse_args函数以添加此检查:

1
2
3
4
5
6
7
8
9
pub fn parse_args() -> Result<Args, Errcode> {
// ...

if args.command.is_empty() {
return Err(Errcode::ArgumentInvalid("command"));
}

Ok(args)
}

测试

这一步的实现很简单,让我们测试一下:

1
2
3
4
5
6
7
8
9
10
$ mkdir -p ./mountdir/bin
$ cp /bin/bash ./mountdir/bin
$ cargo build
# ...
$ sudo target/debug/crabcan --debug -u 0 -m ./mountdir -c "/bin/bash"
[2022-08-23T08:37:01Z INFO crabcan] Args { debug: false, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" }
[2022-08-23T08:37:01Z INFO crabcan::namespaces] User namespaces set up
[2022-08-23T08:37:01Z INFO crabcan::child] Container set up successfully
[2022-08-23T08:37:01Z INFO crabcan::child] Starting container with command /bin/bash and args ["/bin/bash"]
[2022-08-23T08:37:01Z ERROR crabcan::child] Error while trying to perform execve: ENOENT

哦,运行的结果看起来不妙,出现了ENOENT错误! 让我们从execve文档找找答案:

ENOENT 文件路径名、脚本或 ELF 解释器不存在。

为什么会这样?我们已经将二进制应用复制到挂载点中了,而且每个文件都允许执行!

……是这样吗?

动态链接的二进制应用

当一个程序被编译时,即使是一个简单的Hello World程序,也需要很多依赖项,例如编程语言的标准库或与底层操作系统的绑定。

静态编译二进制应用是可行的,但这种方式会产生一个大文件,并且它编译的5个应用将在每个软件中嵌入5个可以共享的软件依赖。

这就是大多数二进制应用使用共享库的原因。 在Linux中,你可以通过执行命令来查看动态链接到二进制应用的库。

1
2
3
4
5
$ ldd /bin/bash
linux-vdso.so.1 (0x00007ffca9d9e000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f7911136000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7910f0e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f79112dd000)

在这里我们可以看到:/bin/bash可执行文件依赖于位于系统根目录的4个文件,

静态编译测试二进制应用

让我们稍后再处理这个动态编译二进制程序问题,现在我们想测试我们的execve,所以让我们使用静态编译的二进制应用,它只需要单一文件就可以工作! 让我们在仓库中创建一个小crate,并将其编译为静态构建的二进制应用。

这个过程可能看起来有点深奥,但实际上我们只是告诉编译器将动态依赖项打包到二进制应用中,而不是进行引用,由于它对运行的系统有依赖,因此我们选择特定的target进行编译。

1
2
3
4
5
cargo new --bin testbin
cd testbin
RUSTFLAGS="-C target-feature=+crt-static" cargo build --target="x86_64-unknown-linux-gnu"
cp target/debug/testbin ../mountdir/
cd ..

要了解有关 Rust 中链接过程的更多信息,请查看此文档

现在我们已经有静态编译的二进制应用,可以正常测试我们的容器功能了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[2022-08-23T07:58:45Z INFO  crabcan] Args { debug: true, command: "/testbin", uid: 0, mount_dir: "./mountdir/" }
[2022-08-23T07:58:45Z DEBUG crabcan::container] Linux release: 5.13.0-52-generic
[2022-08-23T07:58:45Z DEBUG crabcan::container] Container sockets: (3, 4)
[2022-08-23T07:58:45Z DEBUG crabcan::resources] Restricting resources for hostname triangular-coffee-39
[2022-08-23T07:58:45Z DEBUG crabcan::hostname] Container hostname is now triangular-coffee-39
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Setting mount points ...
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Mounting temp directory /tmp/crabcan.yOXBrf4FO8v0
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Pivoting root
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Unmounting old root
[2022-08-23T07:58:45Z DEBUG crabcan::namespaces] Setting up user namespace with UID 0
[2022-08-23T07:58:45Z DEBUG crabcan::namespaces] Child UID/GID map done, sending signal to child to continue...
[2022-08-23T07:58:45Z DEBUG crabcan::container] Creation finished
[2022-08-23T07:58:45Z DEBUG crabcan::container] Container child PID: Some(Pid(2354182))
[2022-08-23T07:58:45Z DEBUG crabcan::container] Waiting for child (pid 2354182) to finish
[2022-08-23T07:58:45Z INFO crabcan::namespaces] User namespaces set up
[2022-08-23T07:58:45Z DEBUG crabcan::namespaces] Switching to uid 0 / gid 0...
[2022-08-23T07:58:45Z DEBUG crabcan::capabilities] Clearing unwanted capabilities ...
[2022-08-23T07:58:45Z DEBUG crabcan::syscalls] Refusing / Filtering unwanted syscalls
[2022-08-23T07:58:45Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=chmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
...
[2022-08-23T07:58:45Z DEBUG syscallz] seccomp: loading policy
[2022-08-23T07:58:45Z INFO crabcan::child] Container set up successfully
[2022-08-23T07:58:45Z INFO crabcan::child] Starting container with command /testbin and args ["/testbin"]
Hello, world!
[2022-08-23T07:58:45Z DEBUG crabcan::container] Finished, cleaning & exit
[2022-08-23T07:58:45Z DEBUG crabcan::container] Cleaning container
[2022-08-23T07:58:45Z DEBUG crabcan::resources] Cleaning cgroups
[2022-08-23T07:58:45Z DEBUG crabcan::errors] Exit without any error, returning 0

耶! 我们得到了“Hello, world!”的输出,我们的容器现在能够运行可执行文件了。

Patch for this step

这一步的代码可以在github litchipi/crabcan branch “step15”中找到.
前一步到这一步的原始补丁可以在此处找到

挂载附加路径

对于这个有用的容器来说,我们需要做的最后一项工作是是允许在容器内使用一些动态二进制文件。尽管我们可以每次都简单地复制所有内容,而且这么做更加安全,但是现在我们将使用更简单的方法,让我们在容器内挂载目录!
方法很简单,在创建容器时,我们传递主机目录列表以及它们应该出现在容器内的位置。 然后我们“挂载”它们并授予访问权限。
让我们在 CLI 中为这些待挂载的附加路径添加一个参数,在src/cli.rs中:

1
2
3
4
5
6
7
pub struct Args {
// ...

/// Mount a directory inside the container
#[structopt(parse(from_os_str), short = "a", long = "add")]
pub addpaths: Vec<PathBuf>,
}

这些参数将用于生成一对对from_dir -> to_dir数据,并将它们传递给容器配置。 在src/container.rs中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
impl Container {
pub fn new(args: Args) -> Result<Container, Errcode> {
let mut addpaths = vec![];
for ap_pair in args.addpaths.iter(){
let mut pair = ap_pair.to_str().unwrap().split(":");
let frompath = PathBuf::from(pair.next().unwrap())
.canonicalize().expect("Cannot canonicalize path")
.to_path_buf();
let mntpath = PathBuf::from(pair.next().unwrap())
.strip_prefix("/").expect("Cannot strip prefix from path")
.to_path_buf();
addpaths.push((frompath, mntpath));
}

let (config, sockets) = ContainerOpts::new(
args.command,
args.uid,
args.mount_dir,
addpaths)?;

// ...
}
}

让我们在文件src/config.rsContainerOpts::new函数中添加这个新参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub struct ContainerOpts{
// ...
pub addpaths: Vec<(PathBuf, PathBuf)>,
}


impl ContainerOpts{
pub fn new(command: String, uid: u32, mount_dir: PathBuf, addpaths: Vec<(PathBuf, PathBuf)>)
-> Result<(ContainerOpts, (RawFd, RawFd)), Errcode> {
// ...
Ok((
ContainerOpts {
// ...
addpaths,
},
sockets
))
}
}

然后,它们j将作为设置挂载点函数的附加参数传递,并用于创建挂载点并执行挂载操作。在文件src/mounts.rs中:

1
2
3
4
5
6
7
8
9
10
11
12
13
pub fn setmountpoint(mount_dir: &PathBuf, addpaths: &Vec<(PathBuf, PathBuf)>) -> Result<(), Errcode> {
// ...

log::debug!("Mounting additionnal paths");
for (inpath, mntpath) in addpaths.iter(){
let outpath = new_root.join(mntpath);
create_directory(&outpath)?;
mount_directory(Some(inpath), &outpath, vec![MsFlags::MS_PRIVATE, MsFlags::MS_BIND])?;
}

log::debug!("Pivoting root");
// ...
}

安全提示:如果你不希望所有挂载都是读/写的(尤其是在共享系统目录时)。那应该为它们添加一个附加的MS_RDONLY标志。(你可以使用 -a 表示读/写目录,使用-r表示只读目录)

最后,让我们将新的附加参数传递给src/child.rssetup_container_configurations 函数内的setmountpoint

1
2
3
4
5
fn setup_container_configurations(config: &ContainerOpts) -> Result<(), Errcode> {
// ...
setmountpoint(&config.mount_dir, &config.addpaths)?;
// ...
}

测试

现在我们可以使用多对from:to形式传递的附加路径来测试我们的容器,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ mkdir -p ./mountdir
$ cargo build
$ cp /bin/bash ./mountdir
$ cp /bin/ls ./mountdir
$ sudo ./target/debug/crabcan --debug -u 0 -m ./mountdir/ -c "/bash" -a /lib64:/lib64 -a /lib:/lib
[2022-08-23T09:27:34Z INFO crabcan] Args { debug: true, command: "/bash", uid: 0, mount_dir: "./mountdir/", addpaths: ["/lib64:/lib64", "/lib:/lib"] }
[2022-08-23T09:27:34Z DEBUG crabcan::container] Linux release: 5.13.0-52-generic
[2022-08-23T09:27:34Z DEBUG crabcan::container] Container sockets: (3, 4)
[2022-08-23T09:27:34Z DEBUG crabcan::resources] Restricting resources for hostname blue-pinguin-161
[2022-08-23T09:27:34Z DEBUG crabcan::hostname] Container hostname is now blue-pinguin-161
[2022-08-23T09:27:34Z DEBUG crabcan::mounts] Setting mount points ...
[2022-08-23T09:27:34Z DEBUG crabcan::mounts] Mounting temp directory /tmp/crabcan.0j3XHGRYJMf7
[2022-08-23T09:27:34Z DEBUG crabcan::mounts] Mounting additionnal paths
[2022-08-23T09:27:34Z DEBUG crabcan::mounts] Pivoting root
[2022-08-23T09:27:34Z DEBUG crabcan::mounts] Unmounting old root
[2022-08-23T09:27:34Z DEBUG crabcan::namespaces] Setting up user namespace with UID 0
[2022-08-23T09:27:34Z DEBUG crabcan::namespaces] Child UID/GID map done, sending signal to child to continue...
[2022-08-23T09:27:34Z DEBUG crabcan::container] Creation finished
[2022-08-23T09:27:34Z DEBUG crabcan::container] Container child PID: Some(Pid(2412263))
[2022-08-23T09:27:34Z DEBUG crabcan::container] Waiting for child (pid 2412263) to finish
[2022-08-23T09:27:34Z INFO crabcan::namespaces] User namespaces set up
[2022-08-23T09:27:34Z DEBUG crabcan::namespaces] Switching to uid 0 / gid 0...
[2022-08-23T09:27:34Z DEBUG crabcan::capabilities] Clearing unwanted capabilities ...
[2022-08-23T09:27:34Z DEBUG crabcan::syscalls] Refusing / Filtering unwanted syscalls
[2022-08-23T09:27:34Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=chmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
...
[2022-08-23T09:27:34Z DEBUG syscallz] seccomp: loading policy
[2022-08-23T09:27:34Z INFO crabcan::child] Container set up successfully
[2022-08-23T09:27:34Z INFO crabcan::child] Starting container with command /bash and args ["/bash"]
bash-5.1# ./ls
bash lib lib64 ls
bash-5.1# exit
[2022-08-23T09:27:36Z DEBUG crabcan::container] Finished, cleaning & exit
[2022-08-23T09:27:36Z DEBUG crabcan::container] Cleaning container
[2022-08-23T09:27:36Z DEBUG crabcan::resources] Cleaning cgroups
[2022-08-23T09:27:36Z DEBUG crabcan::errors] Exit without any error, returning 0

瞧! 我们刚刚从容器内部调用了动态链接的二进制应用!

Patch for this step

这一步的代码可以在github litchipi/crabcan branch “step14”中找到.
前一步到这一步的原始补丁可以在此处找到

结束语

如果您只能记住这部分的一件事的话,那么请记住您不应该将此玩具项目用于任何严肃的用途,而是使用众多容器化解决方案之一来完成这种工作。

从这个项目到Docker

这可能看起来像是我们的应用处于一个非常简约的容器的状态,但是一旦我们可以添加文件并运行二进制应用,那么我们就可以完成几乎任何事情。这个环境已经足以执行 Web 服务,或者作为编译沙箱,但它还缺少一些东西来达到与Docker相当的水平:

网络隔离:创建假接口,将它们绑定到真实的网络硬件,但是需要隔离程度足够高,保证不会出现任何不良意外
端口转发:在容器的 80 端口上执行 Web 服务,并将数据重定向到需要的本地端口
良好的安全性:安全性很困难,因为它有很多陷阱。
从文件生成容器:使用具有奇怪语法的Crabfile来从文本文件中构建容器(Dockerfile功能)

还有很多我忘记的事情。

教程结束

这是本教程的最后一篇文章,花了很长时间才写完。 如果您有任何问题或意见,请随时联系我,或者使用您的 Github 帐户发表评论。
我试图在各个方面保持它对初学者的友好,因为我在学习语言时喜欢做的就是用它来构建一些真实的东西。

本教程的代码是基于这个教程的 Rust 重写,读者应该阅读一下原教程,因为它充满了此处没有的评论、实验、评论和有趣的想法。
如果你能给这个教程提供贡献、错误纠正以及您认为可以添加到任何这些帖子的文本中的任何内容,我将非常感谢。
保重,happy coding <3