あらかじめ宣言しますが、タイトルでunshare(2)と言いながらclone(2)を使います。
コンテナを自作する のが趣味だったので、Rustでもコンテナの基本的な機能であるLinux Namespace周りのコーディングをしてみた。
必要な材料としては clone(2) のラッパーであるが、 libc::clone()
の定義を見ても使い方がよくわからない...
pub unsafe extern "C" fn clone( cb: extern "C" fn(_: *mut c_void) -> c_int, child_stack: *mut c_void, flags: c_int, arg: *mut c_void, _: ... ) -> c_int
extern "C" fn(_: *mut c_void) -> c_int
ってどういう型なんだ?- stackが
*mut c_void
だけどこれってどう作るんだ。 mmap も呼ばないとダメ? 呼びたくありませんが...。- Cでclone(2)にstackを渡す際は、スタックは末尾から先頭に伸びるので、末尾のアドレスを渡すためにポインタ演算が必要なんだけど、そういうのもやる必要があるんだよな...
頭が痛くなってきたため、そこで nix::sched::clone()
を使う。
コードを見る と、u8のスライスをスタック領域として使えるようにする操作とか、あと実はコールバック関数は void*
として第4引数に渡してるんだなるほど... クロージャだもんな... などの様々な気づきがある。
とにかくこれを使ってNamespaceの分離を行う。
実際のコード
最低限だとこうなると思う。 use
を大胆に省略するスタイル。
fn main() -> MyResult { let cb = Box::new(|| { let cmd = CString::new("bash").unwrap(); let args = vec![ CString::new("containered bash").unwrap(), CString::new("-l").unwrap(), ]; if let Err(e) = execvp(&cmd, &args.as_ref()) { eprintln!("execvp failed: {:?}", e); return 127; } 127 }); let mut child_stack = [0u8; 8192]; let flags = CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_NEWPID; let sigchld = 17; // x86/arm. ref man 7 signal let _pid = clone(cb, &mut child_stack, flags, Some(sigchld))?; while let Ok(status) = waitpid(None, None) { println!("Exit Status: {:?}", status); } Ok(()) }
書いた通りなんだけど、 child_stack は普通の u8 の配列でOKな点、サイズは雰囲気で割り当ててる点、sigchldの値は自分で調べてる点*1は留意か。
ただ、これでコンテナ化したbashは立ち上がるんだけど、 /proc を新しくマウントしてるでもなし、chrootしてるでもなしという感じで、ホスト名を弄れるぐらいしか隔離されてる感がない。
最低限必要な前処理を追加
コンテナの雰囲気を出す最低限の前処理を行う。
mount --make-rprivate /
相当を発行する
まず、Mount Namespaceを分離しているにもかかわらず、systemd管理のLinuxディストロではデフォルトでホストの /
がsharedになってしまっており、このままでは分離されたNamespaceでの変更がホストに伝播する。この辺りのお話は TenForward さんの解説が詳細なので譲ります...。
ということで mount --make-rprivate /
を打つ。コンテナ自作界隈(?)では「あるある」な処理だと思う。今回は nix::mount::mount()
でシステムコールを発行することで実施。
fn mount_make_private() -> Result<(), nix::Error> { mount( Some("none"), "/", None::<&str>, // ただのNoneだと型推論してくれないよ...。 MsFlags::MS_REC | MsFlags::MS_PRIVATE, None::<&str>, ) }
chroot先のroot filesystemを作る
その上で、コンテナが使うrootをホストのrootをは分けたいので、chroot先のrootをbind mountでさくっと作ってしまう。
fn mount_bind(source: &str, target: &str) -> Result<(), nix::Error> { mount( Some(source), target, None::<&str>, MsFlags::MS_BIND, None::<&str>, ) } // 利用時のイメージ create_dir(root)?; mount_bind("/", root)?;
procfsをマウントする
これも同じように mount()
に適切なフラグを渡す。
fn mount_proc(source: &str, target: &str) -> Result<(), nix::Error> { mount( Some(source), target, Some("proc"), MsFlags::empty(), None::<&str>, ) }
あとは nix::unistd::chroot()
と std::env::set_current_dir()
を適切な順序で呼ぶ。
type MyResult = Result<(), Box<dyn std::error::Error>>; fn container_prelude(root: &str) -> MyResult { mount_make_private()?; create_dir(root)?; mount_bind("/", root)?; chroot(root)?; set_current_dir("/")?; mount_proc("proc", "/proc")?; Ok(()) }
container_prelude()
をfork(clone)してからexecする直前までの間で呼ぶことで、コンテナとしてセットアップされた状態で bash が立ち上がることになる。
main の全体
use nix::mount::*; use nix::sched::*; use nix::sys::wait::waitpid; use nix::unistd::{chroot, execvp}; use std::env::{args, set_current_dir}; use std::ffi::CString; use std::fs::{create_dir, remove_dir}; type MyResult = Result<(), Box<dyn std::error::Error>>; // 上掲の関数がここに。省略 // ... fn main() -> MyResult { let usage = format!("Usage: {} [newroot]", args().nth(0).unwrap()); let root = args().nth(1).ok_or(usage)?; let cb = Box::new(|| { if let Err(e) = container_prelude(&root) { eprintln!("prelude failed: {:?}", e); return 127; } let cmd = CString::new("bash").unwrap(); let args = vec![ CString::new("containered bash").unwrap(), CString::new("-l").unwrap(), ]; if let Err(e) = execvp(&cmd, &args.as_ref()) { eprintln!("execvp failed: {:?}", e); return 127; } 127 }); let mut child_stack = [0u8; 8192]; let flags = CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_NEWPID; let sigchld = 17; // x86/arm. ref man 7 signal let _pid = clone(cb, &mut child_stack, flags, Some(sigchld))?; while let Ok(status) = waitpid(None, None) { println!("Exit Status: {:?}", status); } // 一応後始末する remove_dir(&root)?; Ok(()) }
動作の様子
$ sudo ../target/debug/minimum-container-example /tmp/foobar-1 root@ubuntu-groovy:/# mount /dev/sda1 on / type ext4 (rw,relatime) proc on /proc type proc (rw,relatime) root@ubuntu-groovy:/# ps auxf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.1 0.0 10008 5072 ? S 09:49 0:00 containered bash -l root 13 0.0 0.0 11476 3560 ? R+ 09:49 0:00 ps auxf root@ubuntu-groovy:/# exit logout Exit Status: Exited(Pid(198843), 0)
今回やっていないこと
- cgroup とか... ディレクトリ とファイルの操作をするだけなので、今度気が向いたらやる。
- その他、libcapやlibseccompがやってくれるようなことは一旦やっていない。FFIすればできると思う。あと setsid() するとか細かいとこ...。
- ネットワーク周り。 setns 相当のnixの関数もあるのでできると思う。netns作るには、たとえば こういうクレート があって、これはpyroute2相当のことが普通にできるみたいなので、netnsやveth作成もできると思う。ちょっと調べきれてないですが...。
- OCI specに沿った実装に持っていくほどの元気はないです...。
コードはここに置いておいた。
*1:これもっと適切な出し方がありそう