本文最后编辑于   前,其中的内容可能需要更新。
                
                
                    
                
                 
                
    注
    
        本文章为对 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” 中找到. 前一步到这一步的原始补丁可以在此处 找到