本文最后编辑于 前,其中的内容可能需要更新。
注
本文章为对 Litchi Pi
的《Writing a container in Rust》 的翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Litchi Pi 保有。个人翻译能力有限,如有疑问可查看原文。
定义容器环境 设置容器的主机名称 主机名是我们的机器在同一网络中与其他机器相区别的标识。 许多不同的网络软件都会使用主机名,例如avahi
这个软件就会在本地网络中广播我们的主机名,使得命令ssh crab@192.168.0.42
可以变为ssh crab@crabcan.local
,网站http://localhost:80
可以变为http://crabcan.local
等等…
如果想了解更多信息可以访问avahi官方网站
为了区分我们的容器软件执行的操作和宿主机系统执行的操作,我们接下来要修改容器的主机名。
生成主机名 首先,我们创建一个新文件src/hostname.rs
,所有与主机名相关的代码都会在这完成。 在文件内部,设置两个预定义的名字和形容词数组,我们将联合使用这两个数组来生成一个滑稽的随机主机名。
1 2 3 4 5 6 7 8 9 const HOSTNAME_NAMES: [&'static str ; 8 ] = [ "cat" , "world" , "coffee" , "girl" , "man" , "book" , "pinguin" , "moon" ]; const HOSTNAME_ADJ: [&'static str ; 16 ] = [ "blue" , "red" , "green" , "yellow" , "big" , "small" , "tall" , "thin" , "round" , "square" , "triangular" , "weird" , "noisy" , "silent" , "soft" , "irregular" ];
接下来我们随机生成一些字符串
1 2 3 4 5 6 7 8 9 10 use rand::Rng;use rand::seq::SliceRandom;pub fn generate_hostname () -> Result <String , Errcode> { let mut rng = rand::thread_rng(); let num = rng.gen::<u8 >(); let name = HOSTNAME_NAMES.choose(&mut rng).ok_or(Errcode::RngError)?; let adj = HOSTNAME_ADJ.choose(&mut rng).ok_or(Errcode::RngError)?; Ok (format! ("{}-{}-{}" , adj, name, num)) }
我们会得到形如 square-moon-64、big-pinguin-2的主机名。
由于我们使用了新的Errcode::RngError
来处理与随机函数相关的错误,我们需要在src/errors.rs
中的Errcode
枚举中添加这个变体,以及另一个稍后会用到的变体 Errcode::HostnameError(u8)
:
1 2 3 4 5 pub enum Errcode { HostnameError(u8 ), RngError }
此外,我们使用rand
包来随机生成主机名,所以我们需要将它添加到Cargo.toml
的依赖中:
1 2 3 [dependencies] rand = "0.8.4"
译者注
关于Docker容器的名称,实际上名称的生成方式和这个rust容器差不多,由随机形容词+随机科学家/黑客的名字组成,不过这个名称也存在一个小彩蛋,在容器名称中不存在boring_wozniak
这个名字,因为Steve Wozniak is not boring
:
1 2 3 4 5 name := left[rand.Intn(len (left))] + "_" + right[rand.Intn(len (right))] if name == "boring_wozniak" { goto begin }
具体参见names-generator.go代码
向容器的配置中添加信息 现在,我们已经有了一种生成包含我们”随机”主机名的String
的方法,我们可以用它来在src/config.rs
中设置我们的容器配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use crate::hostname::generate_hostname;pub struct ContainerOpts { pub hostname: String , } impl ContainerOpts{ pub fn new (...) -> ... { Ok (( ContainerOpts { hostname: generate_hostname()?, }, )) } }
最后,我们在src/hostname.rs
中创建一个函数,该函数将使用sethostname
系统调用,用新的主机名修改我们host namespace的实际主机名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use crate::errors::Errcode;use nix::unistd::sethostname;pub fn set_container_hostname (hostname: &String ) -> Result <(), Errcode> { match sethostname(hostname){ Ok (_) => { log::debug!("Container hostname is now {}" , hostname); Ok (()) }, Err (_) => { log::error!("Cannot set hostname {} for container" , hostname); Err (Errcode::HostnameError(0 )) } } }
你可以查阅linux手册 来获取更多有关sethostname
系统调用的信息
将配置应用到子进程 对于将应用于子进程的配置,我们封装一个函数来进行所有内容的设置,并在其中添加set_container_hostname
函数的调用,让我们先来修改src/child.rs
:
1 2 3 4 5 6 use crate::hostname::set_container_hostname;fn setup_container_configurations (config: &ContainerOpts) -> Result <(), Errcode> { set_container_hostname(&config.hostname)?; Ok (()) }
然后我们只需在子进程开始时调用这个配置函数:
1 2 3 4 5 6 7 8 9 10 fn child (config: ContainerOpts) -> isize { match setup_container_configurations(&config) { Ok (_) => log::info!("Container set up successfully" ), Err (e) => { log::error!("Error while configuring container: {:?}" , e); return -1 ; } } }
请注意,我们无法从“恢复”在子进程中发生的错误,如果出现问题,我们只以retcode = -1
退出,并附带一个友好的错误消息。 这里要做的最后一件事是在src/main.rs
中添加我们刚刚创建的hostname
模块:
测试 在测试时,我们能看见我们生成的容器名称出现在日志中:
1 2 3 4 5 6 7 8 9 10 11 12 [2021 -11 -15 T09:07 :38 Z INFO crabcan] Args { debug: true , command: "/bin/bash" , uid: 0 , mount_dir: "./mountdir/" } [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Linux release: 5.13 .0 -21 -generic [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Container sockets: (3 , 4 ) [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Creation finished [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Container child PID: Some (Pid(26003 )) [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Waiting for child (pid 26003 ) to finish [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::hostname] Container hostname is now weird-moon-191 [2021 -11 -15 T09:07 :38 Z INFO crabcan::child] Container set up successfully [2021 -11 -15 T09:07 :38 Z INFO crabcan::child] Starting container with command /bin/bash and args ["/bin/bash" ] [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Finished, cleaning & exit [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::container] Cleaning container [2021 -11 -15 T09:07 :38 Z DEBUG crabcan::errors] Exit without any error, returning 0
我们多运行几次这个程序,它会输出不同的有趣名字:D
1 2 3 4 5 [2021 -11 -15 T09:08 :33 Z DEBUG crabcan::hostname] Container hostname is now round-cat-221 [2021 -11 -15 T09:08 :48 Z DEBUG crabcan::hostname] Container hostname is now silent-man-45 [2021 -11 -15 T09:09 :01 Z DEBUG crabcan::hostname] Container hostname is now soft-cat-149
Patch for this step 这一步的代码可以在github litchipi/crabcan branch “step9” 中找到. 前一步到这一步的原始补丁可以在此处 找到
修改容器挂载点 挂载点是我们将作为容器根目录(即容器的/
)挂载的某个容器目录,用户可以将一个目录作为参数传递,该目录将被用作容器的根目录。
该过程分为以下几步:
在容器内部挂载系统根目录 /
创建一个新的临时目录 /tmp/crabcan.<random_string>
将用户给定的目录挂载到临时目录
在两个已挂载的目录之间执行root pivot
卸载并删除不必要的目录
请记住,我们在容器内部挂载/卸载的所有内容都通过mountnamespace从系统的其余部分隔离开来。 在实践中,这种隔离保持了 /proc/<pid>/mountinfo
、/proc/<pid>/mountstats
和 /proc/<pid>/mounts/
的独立,它们描述了什么在哪里挂载,如何挂载等信息。
你可以查阅proc(5) linux manual 或者mount_namespace linux manual 来获取更多精确信息
译者注
pivot_root
是由 Linux 提供的一种系统调用,它能够将一个mount namespace
中的所有进程的根目录和当前工作目录切换到一个新的目录。 与chroot
不同的是,chroot
只会改变当前进程的根目录。
准备实现 接下来我们将创建一个包含setmountpoint
函数的src/mounts.rs
文件,让我们先完成所有的准备工作,以便稍后专注于完成我们的目录部分。
让我们将setmountpoint
函数作为容器配置过程的一部分添加到src/child.rs
中:
1 2 3 4 5 6 use crate::mounts::setmountpoint;fn setup_container_configurations (config: &ContainerOpts) -> Result <(), Errcode> { setmountpoint(&config.mount_dir)?; Ok (()) }
然后,在src/container.rs
中的clean_exit
函数中添加一个新的部分:
1 2 3 4 5 6 7 8 9 use crate::mounts::clean_mounts;impl Container { pub fn clean_exit (&mut self ) -> Result <(), Errcode>{ clean_mounts(&self .config.mount_dir)?; } }
接着,我们在src/errors.rs
中的Errcode
枚举中添加一个新的错误类型:
1 2 3 4 pub enum Errcode { MountsError(u8 ), }
最后,在src/main.rs
中引用src/mounts.rs
模块
重新以private形式挂载根目录/
现在真正的重头戏来了!让我们创建src/mounts.rs
文件,并添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 use crate::errors::Errcode;use std::path::PathBuf;pub fn setmountpoint (mount_dir: &PathBuf) -> Result <(), Errcode> { log::debug!("Setting mount points ..." ); Ok (()) } pub fn clean_mounts (_rootpath: &PathBuf) -> Result <(), Errcode>{ Ok (()) }
我们使用MS_PRIVATE
这个标志来重新挂载我们文件系统的根目录/
,这将阻止任何挂载操作的传播。
请参阅这篇LWN 文章 ,以获取更多关于MS_PRIVATE
标志的解释,在另一篇LWN 文章 中可以获得示例。
为了实现这个目标,我们将创建一个名为mount_directory
的函数,它本质上是对nix
库提供的mount
系统调用 的封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use nix::mount::{mount, MsFlags};pub fn mount_directory (path: Option <&PathBuf>, mount_point: &PathBuf, flags: Vec <MsFlags>) -> Result <(), Errcode>{ let mut ms_flags = MsFlags::empty(); for f in flags.iter(){ ms_flags.insert(*f); } match mount::<PathBuf, PathBuf, PathBuf, PathBuf>(path, mount_point, None , ms_flags, None ) { Ok (_) => Ok (()), Err (e) => { if let Some (p) = path{ log::error!("Cannot mount {} to {}: {}" , p.to_str().unwrap(), mount_point.to_str().unwrap(), e); }else { log::error!("Cannot remount {}: {}" , mount_point.to_str().unwrap(), e); } Err (Errcode::MountsError(3 )) } } }
我们需要在setmountpoint
函数中调用它:
1 2 3 4 5 pub fn setmountpoint (mount_dir: &PathBuf) -> Result <(), Errcode> { mount_directory(None , &PathBuf::from("/" ), vec! [MsFlags::MS_REC, MsFlags::MS_PRIVATE])?; }
挂载新的根目录 现在,让我们挂载用户提供的目录,以便稍后进行pivot root
操作。我不会对每一行代码做详细解释,因为这只是简单地调用库函数。
首先,我们创建一个random_string
函数,它返回的,嗯,就是一个随机字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use rand::Rng;pub fn random_string (n: usize ) -> String { const CHARSET: &[u8 ] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ abcdefghijklmnopqrstuvwxyz\ 0123456789" ; let mut rng = rand::thread_rng(); let name: String = (0 ..n) .map(|_| { let idx = rng.gen_range(0 ..CHARSET.len()); CHARSET[idx] as char }) .collect(); name }
我们可以通过这个函数简单的得到一个随机目录名称,在得到这个名称后我们可以创建这个目录:
1 2 3 4 5 6 7 8 9 10 11 use std::fs::create_dir_all;pub fn create_directory (path: &PathBuf) -> Result <(), Errcode>{ match create_dir_all(path) { Err (e) => { log::error!("Cannot create directory {}: {}" , path.to_str().unwrap(), e); Err (Errcode::MountsError(2 )) }, Ok (_) => Ok (()) } }
最后,在`setmountpoint·函数中将所有内容整合起来:
1 2 3 4 5 6 7 8 pub fn setmountpoint (mount_dir: &PathBuf) -> Result <(), Errcode> { let new_root = PathBuf::from(format! ("/tmp/crabcan.{}" , random_string(12 ))); log::debug!("Mounting temp directory {}" , new_root.as_path().to_str().unwrap()); create_directory(&new_root)?; mount_directory(Some (&mount_dir), &new_root, vec! [MsFlags::MS_BIND, MsFlags::MS_PRIVATE])?; }
我们将用户提供的mount_dir
挂载到了挂载点/tmp/crabcan.<random_letters>
,这将使我们能够在后面进行pivot root
操作,并像使用系统真实的根目录一样使用mount_dir
。
Pivot the root 现在我们可以见识一下真正的魔术了!我们将/tmp/crabcan.<random_letters>
设置为我们的新的/
根文件系统,并将旧的/
移动到新的目录 /tmp/crabcan.<random_letters>/oldroot.<random_letters>
。 例:
1 2 3 容器外 容器内 ~/container_dir == mount ==> /tmp/crabcan.12345 == pivot ==> / / == pivot ==> /oldroot.54321
请参阅Linux 手册 ,以获取此过程的详细解释。 以下是代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 use nix::unistd::pivot_root;pub fn setmountpoint (mount_dir: &PathBuf) -> Result <(), Errcode> { log::debug!("Pivoting root" ); let old_root_tail = format! ("oldroot.{}" , random_string(6 )); let put_old = new_root.join(PathBuf::from(old_root_tail.clone())); create_directory(&put_old)?; if let Err (_) = pivot_root(&new_root, &put_old) { return Err (Errcode::MountsError(4 )); } }
卸载旧根目录 我们希望容器与主机系统实现隔离,所以必须卸载”旧的根目录”,这样被容器化的应用程序就无法访问整个文件系统。 为了实现这一点,我们创建了unmount_path
和delete_dir
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use nix::mount::{umount2, MntFlags};pub fn unmount_path (path: &PathBuf) -> Result <(), Errcode>{ match umount2(path, MntFlags::MNT_DETACH){ Ok (_) => Ok (()), Err (e) => { log::error!("Unable to umount {}: {}" , path.to_str().unwrap(), e); Err (Errcode::MountsError(0 )) } } } use std::fs::remove_dir;pub fn delete_dir (path: &PathBuf) -> Result <(), Errcode>{ match remove_dir(path.as_path()){ Ok (_) => Ok (()), Err (e) => { log::error!("Unable to delete directory {}: {}" , path.to_str().unwrap(), e); Err (Errcode::MountsError(1 )) } } }
接下来简单的在setmountpoint
函数的末尾调用它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use nix::unistd::chdir;pub fn setmountpoint (mount_dir: &PathBuf) -> Result <(), Errcode> { log::debug!("Unmounting old root" ); let old_root = PathBuf::from(format! ("/{}" , old_root_tail)); if let Err (_) = chdir(&PathBuf::from("/" )) { return Err (Errcode::MountsError(5 )); } unmount_path(&old_root)?; delete_dir(&old_root)?; }
注意:我们卸载并删除了/oldroot.<random_letters>
目录,因为它位于/tmp/crabcan.<random_letters>
目录内,而后者已经成为了我们的新根目录。
空的清理函数 你可能已经注意到,现在的clean_mounts
函数完全没有用。主要的问题在于,父容器完全不知道用户提供的目录被挂载在哪里(因为它是一个随机生成的文件名)。 它现在造成的唯一真正问题是,所有创建的/tmp/crabcan.<random_letters>
目录在执行后仍然存在,即使在被容器化的进程退出后它们是空的并已经被卸载。 为了简单期间(或者说懒),我简单的保留了这些目录,但保留了一个清理函数的占位符,以防万一。
测试 在测试时,我们可以看到一个位于/tmp/crabcan.<random_letters>
的新的root目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [2022-01-04T06:50:25Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" } [2022-01-04T06:50:25Z DEBUG crabcan::container] Linux release: 5.13.0-22-generic [2022-01-04T06:50:25Z DEBUG crabcan::container] Container sockets: (3, 4) [2022-01-04T06:50:25Z DEBUG crabcan::container] Creation finished [2022-01-04T06:50:25Z DEBUG crabcan::container] Container child PID: Some(Pid(324564)) [2022-01-04T06:50:25Z DEBUG crabcan::container] Waiting for child (pid 324564) to finish [2022-01-04T06:50:25Z DEBUG crabcan::hostname] Container hostname is now blue-man-109 [2022-01-04T06:50:25Z DEBUG crabcan::mounts] Setting mount points ... [2022-01-04T06:50:25Z DEBUG crabcan::mounts] Mounting temp directory /tmp/crabcan.wYGDJtGIKxZ4 [2022-01-04T06:50:25Z DEBUG crabcan::mounts] Pivoting root [2022-01-04T06:50:25Z DEBUG crabcan::mounts] Unmounting old root [2022-01-04T06:50:25Z INFO crabcan::child] Container set up successfully [2022-01-04T06:50:25Z INFO crabcan::child] Starting container with command /bin/bash and args ["/bin/bash"] [2022-01-04T06:50:25Z DEBUG crabcan::container] Finished, cleaning & exit [2022-01-04T06:50:25Z DEBUG crabcan::container] Cleaning container [2022-01-04T06:50:25Z DEBUG crabcan::errors] Exit without any error, returning 0
Patch for this step 这一步的代码可以在github litchipi/crabcan branch “step10” 中找到. 前一步到这一步的原始补丁可以在此处 找到