chrootベースのjailをRustで作ってみる
jailing をRustに置き換えながらjail構築の処理を追ってみた。
目的
自作Linuxコンテナの時代というy_uuk1さんが書いたブログ記事を最近読み直すことがあった。その中で触れられていたkazuhoさんのjailingのコードを眺めていたところ、これをRustで書いたらRustの勉強+jailingの勉強にもなってよいのではないか、と思った。
やってみる
処理の概要
perlはなじみがないが、まず雰囲気をつかむ。
- /etc や /tmp などのディレクトリを準備する
- /bin や /usr/bin などをbindでmountする
- /dev を準備する
- chrootする
- capabilityを制限する
- コマンドの実行
という流れになっている。
1. /etc や /tmp などのディレクトリを準備する
まずはこここを実装する。 空ディレクトリの生成、権限の変更(777)、ファイルのコピーをする
const NEW_DIRS: &'static [&'static str] = &["etc", "run", "usr", "var/log"];
const TMP_DIRS: &'static [&'static str] = &["tmp", "run/lock", "var/tmp"];
const COPY_FILES: &'static [&'static str] =
&["etc/group", "etc/passwd", "etc/resolv.conf", "etc/hosts"];
fn create_dirs(root: &str) {
let mut dirs = NEW_DIRS.to_vec();
.extend(TMP_DIRS.iter());
dirsfor d in dirs {
fs::create_dir_all(format!("{}/{}", root, d)).expect(&format!("Failed to create dir {}", d));
}
}
fn change_dir_permissions(root: &str) {
for d in TMP_DIRS.iter() {
let metadata = fs::metadata(format!("{}/{}", root, d)).expect(&format!("Failed to get metadata {}", d));
let mut permissions = metadata.permissions();
.set_mode(0o777);
permissions}
}
fn copy_files(root: &str) {
for f in COPY_FILES {
fs::copy(format!("/{}", f), format!("{}/{}", root, f)).expect(&format!("Failed to copy file {}", f));
}
}
2. /bin や /usr/bin などをbindでmountする
必要なコマンドやライブラリがはいっているディレクトリをbindでmountする。
mountはコマンドをそのまま実行してもよいが、今回はlibmountというcrateを使ってみた。
const BIND_DIRS: &'static [&'static str] = &[
"bin",
"etc/alternatives",
"etc/pki/tls/certs",
"etc/pki/ca-trust",
"etc/ssl/certs",
"lib",
"lib64",
"sbin",
"usr/bin",
"usr/include",
"usr/lib",
"usr/lib64",
"usr/libexec",
"usr/sbin",
"usr/share",
;
]
fn bind_mount(root: &str) {
for d in BIND_DIRS {
let sd = format!("/{}", d);
let rd = format!("{}/{}", root, d);
if !Path::new(&sd).exists() {
continue;
}
let sdm = fs::metadata(&sd).expect(&format!("Failed to get metadata {}", &sd));
if sdm.file_type().is_symlink() {
let rdm = fs::metadata(&rd).expect(&format!("Failed to get metadata {}", &rd));
if !rdm.file_type().is_symlink() {
fs::create_dir_all(
Path::new(&rd)
.parent()
.expect(&format!("Failed to get parent dir path {}", &rd)),
.expect(&format!("Failed to create parent dir {}", &rd));
)}
let sdl = fs::read_link(&sd).expect(&format!("Failed to read symlink {}", &sd));
std::os::unix::fs::symlink(sdl, &rd)
.expect(&format!("Failed to create symlink {}", &rd));
} else {
fs::create_dir_all(&rd).expect(&format!("Failed to create dir {}", &rd));
if fs::read_dir(&rd).unwrap().count() == 0 {
libmount::BindMount::new(Path::new(&sd), Path::new(&rd))
.recursive(false)
.readonly(true)
.mount()
.expect(&format!("Failed to mount {} to {}", &sd, &rd));
}
}
}
}
3. /dev を準備する
/dev/null
, /dev/zero
, /dev/random
, /dev/urandom
を準備する。
元々のjailingではmknod
というコマンドで準備している。今回は libc
のmknodを使った。
このmknodなどのlibcのライブラリの使い方はcoreutilsの実装が非常に参考になった。(というか一部そのまま使っている。)
fn make_devices(root: &str) {
fs::create_dir_all(format!("{}/dev", root)).expect("Cannot create /dev dir");
format!("{}/dev/null", root), 0o666, makedev(1, 3));
make_device_if_not_exists(format!("{}/dev/zero", root), 0o666, makedev(1, 5));
make_device_if_not_exists(for r in &["random", "urandom"] {
format!("{}/dev/{}", root, r), 0o444, makedev(1, 9));
make_device_if_not_exists(}
}
fn make_device_if_not_exists(path: String, mode: mode_t, dev: dev_t) {
if !Path::new(&path).exists() {
let err = makenod(&path, S_IFCHR | mode, dev);
, format!("{}", path));
handle_os_error(err}
}
fn makenod(path: &String, mode: mode_t, dev: dev_t) -> i32 {
unsafe {
mknod(CString::new(path.as_bytes())
.expect("Error in construct CString")
.as_bytes_with_nul()
.as_ptr() as *const libc::c_char,
,
mode,
dev
)}
}
fn makedev(maj: u64, min: u64) -> dev_t {
// pick up from <sys/sysmacros.h>
& 0xff) | ((maj & 0xfff) << 8) | (((min & !0xff)) << 12) | (((maj & !0xfff)) << 32))
((min as dev_t
}
fn handle_os_error<T: std::fmt::Display>(err: i32, action: T) {
if err != 0 {
panic!(
"Error: {{action: {}, code: {}, msg: {} }}",
,
action,
errError::last_os_error()
;
)}
}
4. chrootする
chrootもコマンドを呼び出してもいいのだが、折角なので、libc
のchrootを使った。
fn exec_chroot(root: &str) {
std::env::set_current_dir(&root).expect(&format!("Cannot change current dir to {}", &root));
let err = unsafe {
CString::new(".".as_bytes())
chroot(.expect("Error in construct CString")
.as_bytes_with_nul()
.as_ptr() as *const libc::c_char)
};
, "chroot");
handle_os_error(err}
5. capabilityを制限する
capabilityについてあまり理解できていなかったため、manやLinux Capability - ケーパビリティについての整理を参考にした。
また、capabilityの操作はcaps-rsを使用した。(結構有用そうなのにスター数が少なくて悲しい。)
fn drop_capabilities() {
let allowed_caps = vec![
Capability::CAP_SETGID,
Capability::CAP_SETGID,
Capability::CAP_NET_BIND_SERVICE,
;
]let cur = caps::read(None, CapSet::Bounding).expect("Cannot read capabilities");
for c in cur {
if allowed_caps.contains(&c) {
continue;
}
caps::drop(None, CapSet::Bounding, c).expect(&format!("Cannot drop capability {}", c));
}
}
6. コマンドの実行
最後に、chroot環境で、コマンドを実行する。
fn exec_command<'a>(commands: Option<clap::Values>) {
let cmds = match commands {
Some(vs) => vs.map(|v| v).collect::<Vec<&str>>(),
None => vec!["bash", "-i"],
};
let _ = Command::new(cmds[0])
.args(&cmds[1..])
.status()
.unwrap_or_else(|e| panic!("Cannot exec: {}", e));
}
コマンド化
上記をベースに、jl
という名前でコマンドを作った。
コードはhttps://github.com/yoshitsugu/jlに置いてある。
動かす
/opt/jail
というディレクトリを作ってそこで実験してみる
$ sudo mkdir /opt/jail
jl
経由でbash
起動
$ sudo jl --root bash
bash-4.4#
ls
とかpwd
をためしてみる
bash-4.4# ls
bin dev etc lib lib64 run sbin tmp usr var
bash-4.4# pwd
/
capability上許可されていないはずのkill
やchown
をためしてみる
bash-4.4# kill 26559
bash: kill: (26559) - 許可されていない操作です
bash-4.4# touch /var/log/hoge
bash-4.4# chown 1000 /var/log/hoge
chown: 'var/log/hoge' の所有者を変更中: 許可されていない操作です
ちなみに、jl
コマンドにそのままコマンドを渡すこともできる
$ sudo jl --root /opt/jail ls
bin dev etc lib lib64 run sbin tmp usr var
$ sudo jl --root /opt/jail -- ruby -e "puts 'hello'"
hello
まとめ
- jailingのコードを読みながらRustでchrootベースのjail構築ができるようにした。
- custom bindingなど元のjailingにあるもので実装していないものもあるので、その辺はまた今度。
- Linux capabilityやRustでlibcを使う方法など勉強になった。