Defining the container environment
Setting the container hostname
A hostname is what identifies our machine compared to every other living on the same network.
It is used by many different networking softwares, for example avahi
is a software that streams our hostname in the local network, allowing a command ssh crab@192.168.0.42
to become ssh crab@crabcan.local
, the website http://localhost:80
to http://crabcan.local
, etc …
Check the official website of avahi for more informations
In order to differentiate the operations performed by our software contained from the one performed by the host system, we will modify its hostname.
Generate a hostname
First of all, create a new file called src/hostname.rs
, in which we will write any code related to the hostname.
Inside, set two arrays of pre-defined names and adjectives that we’ll use together to generate a stupid random hostname.
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"];
We then generate a string using some randomness:
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))
}
We obtain a hostname in the form square-moon-64, big-pinguin-2, etc …
As we used a new Errcode::RngError
to handle errors linked to the randomness functions, we add this variant to our Errcode
enum in src/errors.rs
, along with another one that we’ll use later, the Errcode::HostnameError(u8)
variant:
pub enum Errcode {
// ...
HostnameError(u8),
RngError
}
Also, we use the rand
crate to have randomness in our hostname generation, so we have to add it to the dependencies of Cargo.toml
:
[dependencies]
# ...
rand = "0.8.4"
Adding to the configuration of the container
Now that we have a way to generate a String
containing our “random” hostname, we can use it to set our container configuration in src/config.rs
:
use crate::hostname::generate_hostname;
pub struct ContainerOpts{
// ...
pub hostname: String,
}
impl ContainerOpts {
// ...
hostname: generate_hostname()?,
}
And finally, we can create in src/hostname.rs
the function that will modify the actual hostname of our host namespace with the new one, using the sethostname
syscall:
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))
}
}
}
Check the linux manual for more informations on the
sethostname
syscall
Applying the configuration to the child process
For all the configuration we will apply to the child process, let’s create a wrapping functions setting everything, and add the set_container_hostname
function call inside it, in src/child.rs
:
use crate::hostname::set_container_hostname;
fn setup_container_configurations(config: &ContainerOpts) -> Result<(), Errcode> {
set_container_hostname(&config.hostname)?;
Ok(())
}
And we then simply call the configuration function at the beginning of our child process:
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;
}
}
// ...
}
Note that we cannot “recover” from any error hapenning in our child process, so we simply end it with a retcode = -1
along with a nice error message in case a problem occurs.
The final thing to do here is adding to src/main.rs
the hostname
module we just created:
// ...
mod hostname;
Testing
When testing, we can see the hostname we generated appear:
[2021-11-15T09:07:38Z INFO crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" }
[2021-11-15T09:07:38Z DEBUG crabcan::container] Linux release: 5.13.0-21-generic
[2021-11-15T09:07:38Z DEBUG crabcan::container] Container sockets: (3, 4)
[2021-11-15T09:07:38Z DEBUG crabcan::container] Creation finished
[2021-11-15T09:07:38Z DEBUG crabcan::container] Container child PID: Some(Pid(26003))
[2021-11-15T09:07:38Z DEBUG crabcan::container] Waiting for child (pid 26003) to finish
[2021-11-15T09:07:38Z DEBUG crabcan::hostname] Container hostname is now weird-moon-191
[2021-11-15T09:07:38Z INFO crabcan::child] Container set up successfully
[2021-11-15T09:07:38Z INFO crabcan::child] Starting container with command /bin/bash and args ["/bin/bash"]
[2021-11-15T09:07:38Z DEBUG crabcan::container] Finished, cleaning & exit
[2021-11-15T09:07:38Z DEBUG crabcan::container] Cleaning container
[2021-11-15T09:07:38Z DEBUG crabcan::errors] Exit without any error, returning 0
And running it several times outputs different funny names :D
[2021-11-15T09:08:33Z DEBUG crabcan::hostname] Container hostname is now round-cat-221
[2021-11-15T09:08:48Z DEBUG crabcan::hostname] Container hostname is now silent-man-45
[2021-11-15T09:09:01Z DEBUG crabcan::hostname] Container hostname is now soft-cat-149
Patch for this step
The code for this step is available on github litchipi/crabcan branch “step9”.
The raw patch to apply on the previous step can be found here
Modifying the container mount point
The mount point is a directory in our system that will be the root, the /
of our container. A user can pass to the arguments a directory that will be used as the root of the container.
The process will be done as followed:
- Mount the system root
/
inside the container - Create a new temporary directory
/tmp/crabcan.<random_string>
- Mount the user-given directory to the temporary directory
- Perform a root pivot over the two mounted directories
- Unmount and delete un-necessary directories
Keep in mind that everything we mount / unmount inside the container are isolated from the rest of the system by the mount namespace.
In practise, this isolation keeps separated versions of /proc/<pid>/mountinfo
, /proc/<pid>/mountstats
and /proc/<pid>/mounts/
, that describes what is mounted where, how, etc …
See proc(5) linux manual or mount_namespace linux manual for more precisions on this.
Preparing the implementation
As we will create a src/mounts.rs
file containing a function setmountpoint
, let’s create everything right now so we can focus on our directories later on.
In src/child.rs
, let’s write our setmountpoint
function as part of the container configuration process:
use crate::mounts::setmountpoint;
fn setup_container_configurations(config: &ContainerOpts) -> Result<(), Errcode> {
// ...
setmountpoint(&config.mount_dir)?;
Ok(())
}
Then in src/container.rs
, we add a new element in the clean_exit
function:
use crate::mounts::clean_mounts;
impl Container {
// ...
pub fn clean_exit(&mut self) -> Result<(), Errcode>{
// ...
clean_mounts(&self.config.mount_dir)?;
}
}
We then add a new error variant in our Errcode
enum in src/errors.rs
:
pub enum Errcode {
// ...
MountsError(u8),
}
Finally, we use the src/mounts.rs
file as a module in our project. In src/main.rs
:
// ...
mod mounts;
Remounting the root /
privately
Now to the real meat ! Let’s create the src/mounts.rs
file and add the following:
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(())
}
We want to remount the root /
of our filesystem with the MS_PRIVATE
flag which will prevent any mount operation to be propagated.
See this LWN article for more explanations on what the
MS_PRIVATE
flag is about, and this other LWN article for an example.
To do this, we will create the mount_directory
function that is essentially a wrapper around the mount
syscall provided by the nix
crate.
use nix::mount::{mount, MsFlags};
pub fn mount_directory(path: Option<&PathBuf>, mount_point: &PathBuf, flags: Vec<MsFlags>) -> Result<(), Errcode>{
// Setting up the mount flags
let mut ms_flags = MsFlags::empty();
for f in flags.iter(){
ms_flags.insert(*f);
}
// Calling the syscall, handling errors
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))
}
}
}
And call it inside our setmountpoint
function:
pub fn setmountpoint(mount_dir: &PathBuf) -> Result<(), Errcode> {
// ...
mount_directory(None, &PathBuf::from("/"), vec![MsFlags::MS_REC, MsFlags::MS_PRIVATE])?;
// ...
}
Mount the new root
Now let’s mount the directory provided by the user so we can pivot root later. I wont go into deep details of every line of code as this is simply calling library functions.
First, let’s create a random_string
function that returns, well, a random string.
// Taken from https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
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
}
That will allow us to generate a random directory name easily.
Then, after we have our random directory name, let’s create that directory:
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(())
}
}
And finally let’s tie everything together in our setmountpoint
function:
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])?;
// ...
}
We mounted our user-provided mount_dir
to the mountpoint /tmp/crabcan.<random_letters>
, this will allow us to pivot root later and use the mount_dir
as if it was the real /
root of the system.
Pivot the root
Now to the real magic trick ! We set /tmp/crabcan.<random_letters>
as our new /
root filesystem, and we will move the old /
root into a new dir /tmp/crabcan.<random_letters>/oldroot.<random_letters>
:
Example:
Outside the container Inside the container
~/container_dir == mount ==> /tmp/crabcan.12345 == pivot ==> /
/ == pivot ==> /oldroot.54321
See the linux manual for a detailed explanation of this process
This is how we can do it in the code:
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));
}
// ...
}
Unmounting the old root
As we want to achieve isolation with the host system, the “old root” has to be unmounted so the contained application cannot access to the whole filesystem.
To do this, we create the unmount_path
and delete_dir
functions:
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))
}
}
}
And we simply call them at the end of setmountpoint
function:
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));
// Ensure we are not inside the directory we want to umount
if let Err(_) = chdir(&PathBuf::from("/")) {
return Err(Errcode::MountsError(5));
}
unmount_path(&old_root)?;
delete_dir(&old_root)?;
// ...
}
Note: We umount and delete the
/oldroot.<random_letters>
directory as it was located inside the/tmp/crabcan.<random_letters>
directory which became our new/
.
The empty cleaning function
You certainly noticed that the clean_mounts
function is totally useless right now. The problem is that the parent container doesn’t have any clue of where the user-provided directory got mounted (as it’s a randomly generated filename).
The only real problem it causes right now is that all the /tmp/crabcan.<random_letters>
directories created still exist after the execution, even if they are empty and unmounted after the contained process exits.
For the sake of simplicity (or laziness), I let it like this but kept the placeholder for a cleaning function if it becomes necessary one day.
Testing
When testing, we can see the new root being located at /tmp/crabcan.<random_letters>
.
[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
The code for this step is available on github litchipi/crabcan branch “step10”.
The raw patch to apply on the previous step can be found here
Precedent post in this serie: Birth of a child process
Next post in this serie: User namespaces and Linux Capabilties