hisamounaのブログ

アウトプットを習慣化するためのブログ

Rustを使ってコンテナの実装を学ぶ

普段なんとなくコンテナを利用しているのですが、内部でどういったことをしているのかあまり分かっていなかったので勉強しました。

(内部のロジックを知らずに扱えるDockerの抽象度の高さに改めて感心しました。)

クックパッドさんが、Linuxカーネルの機能を用いてコンテナの実装を学ぶための資料をアップしていました。詳細に説明がされており、大変勉強になりました。

GitHub - rrreeeyyy/container-internship

せっかくなので、Rustを使ってコンテナ機能の一部を実装してみました。

名前空間の分離

/bin/shを起動する際にclone(2)に渡すflagsの値によって、IPC,Network,Userなどの名前空間を分離することができます。

改めて、shとは

man shの実行結果を確認 (DistributionがDebianなので、dash)

shは、ファイルまたはターミナルからコマンドを読み込み、解釈し、実行するコマンド。

shellが実行 -> 完了までの流れはこちらの記事が大変参考になりました。

shell実行のために子プロセスが作られた後はどうなるのだろうと疑問に思っていましたが、子プロセスがexit(0)をcallしてプロセスをtermiateさせ、

親プロセスでcallしたwait()で子プロセスのstateの変更を検知して、子プロセスのために割り当てたリソースがリリースされるということを学びました。

Rustで名前空間の分離を実装

実行環境

GCPVMインスタンスを1台起動させて、実行環境として利用しました。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 10 (buster)
Release:    10
Codename:   buster

$ arch
x86_64

実装

syscallを呼び出すために、nixを利用しました。libc crateによるunsafeなAPIをsafeな形で提供してくれているようです。

まずは、flagsに CLONE_IOのみセットしました。

use std::process::Command ;
use nix::sys::signal::Signal;
use nix::sched;
use nix::sys::wait::waitpid;


fn run(cmd: &str) -> isize {
    let exit_status = Command::new(cmd)
        .spawn().expect("failed to execute container command")
        .wait().unwrap();
    match exit_status.code() {
        Some(code) => code as isize,
        None => -1
    }
}

fn main() {
    const STACK_SIZE: usize = 1024 * 1024;
    let stack: &mut [u8; STACK_SIZE] = &mut [0; STACK_SIZE];

    let cmd :&str = "/bin/sh";
    let cb = Box::new(|| run(cmd));

    let flags = sched::CloneFlags::CLONE_IO;

    let child_pid = sched::clone(cb, stack, flags, Some(Signal::SIGCHLD as i32)).expect("failed to create child process");
    waitpid(child_pid,None).expect("faile to wait pid");
}

cloneで子プロセスを生成しています。

そして、waitpidメソッドが引数でセットされた子プロセスのstatusを変更を観測しています。

子プロセスの中で、 ip addr show, id を実行してみました、

ネットワーキングの名前空間が分離されていないので、NetworkInterface(ens4)が子プロセスも確認できます。

UID, GIDも親プロセスと同じ値が確認できます。

$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000
...

$ id
uid=1000(xxxxx) gid=1001(xxxxx) groups=1001(xxxxx),,,

flagsにセットするCloneFlagsを追加し、NetworkingとUser名前空間を分離

let flags = sched::CloneFlags::CLONE_NEWUSER
        | sched::CloneFlags::CLONE_NEWNET
        | sched::CloneFlags::CLONE_NEWIPC
        | sched::CloneFlags::CLONE_IO;

もう一度、ip addr show, id を実行してみました。

見事に名前空間が分離されていることを確認できました。

$ ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

残りのコンテナ実装についても引き続きRustで実装してみたいと思います。