Linux Namespace - 超簡單容器構建

Namespace隔離了進程、網路、用戶等系統資源,本文將講述如何通過Namespace創建一個超簡單容器,並在容器內部運行簡單的busybox程序。本文內容參考了LinuxNamespace系列(09),雖然目的相同,但本文采用C語言系統調用的方式實現,因此更加實用。

P.S:本文全部代碼都在Ubuntu18.04Server下編譯運行通過

步驟概覽

  1. 準備busybox二進制文件
    作者使用的是busybox_1.31.0,構建容器後會在容器內安裝busybox,並運行busybox中的shell。
  2. 編寫程序
    1. 創建所需目錄,包括/bin, /proc, /old_root等
    2. 複製可執行文件
    3. 調用unshare創建Namespace
    4. 調用pivot_root設置root文件系統的路徑
    5. 掛載/proc等文件系統
    6. 執行busybox --install ./bin,運行./bin/sh
  3. 執行程序,busybox中運行ls等命令

本文首先展示最終的運行結果,之後分模塊講述程序的構建過程。

運行結果展示

eric@ubuntu:~/coding/linux_learn/simple_container$ ./simp_container.out
Running......
preparing root...
preparing dirs...
copying file...
doing unshare...
setting hostname...
chdir to new root...
pivot root...
doing mount and umount...
set env and exec busybox sh...
/ $ ls
bin       data      old_root  proc
/ $ ps -ef
PID   USER     TIME  COMMAND
    1 65534     0:00 sh
    5 65534     0:00 ps -ef
/ $ 

上述執行結果中,以...結尾的爲日誌輸出。容器創建後,執行了busybox中的shell程序,ps -ef命令的輸出表明當前容器隔離了PID,即容器內進程無法看到外部進程。由於沒有映射用戶和組id,因此USER爲默認的65534。程序運行後的目錄結構如下所示,bin目錄存放了busybox安裝後的可執行文件:

simp_container_root/
└── new_root
    ├── bin
    ├── data
    ├── old_root
    └── proc

構建過程

目錄準備

目錄準備主要使用mkdir函數實現,該函數創建指定目錄,並授予指定權限,函數原型爲int mkdir(const char *pathname, mode_t mode)。如下爲代碼:

#define md(A) mkdir(A, S_IRWXU | S_IRWXG)

void prepare_dirs() {
    logger("preparing dirs");
    md("./simp_container_root/");
    chdir("./simp_container_root/");
    md("./new_root");
    md("./new_root/bin");
    md("./new_root/data");
    md("./new_root/proc");
    md("./new_root/old_root");
}

複製可執行文件

由於沒有找到複製文件的庫函數,因此這裏直接使用標準庫的FILE對象實現,複製後調用chmod添加執行權限:

void cpy_file(const char old_path[], const char new_path[]) {
    FILE *oldfp, *newfp;
    oldfp = fopen(old_path, "rb");
    newfp = fopen(new_path, "wb");
    while (!feof(oldfp)) {
        const int buf_size = 4096;
        char buf[buf_size] = {};
        int n = fread(buf, 1, buf_size, oldfp);
        fwrite(buf, 1, n, newfp);
    }
    fclose(oldfp);
    fflush(newfp);
    fclose(newfp);
    chmod(new_path, S_IRWXU | S_IRWXG);
}

創建Namespace,配置文件系統

通過調用unshare創建新的Namespace,這裏指定PID, UTS, USER, NETWORK, MOUNT, IPC, CGROUP所有七個Namespace。之後調用fork在子進程中執行接下來的操作。關於pivot_root系統調用,作者未作深入研究,感興趣的可以參考LinuxNamespace系列(09)

void do_unshare() {
    int ret = 0;
    unshare(CLONE_NEWCGROUP | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWUTS);
    ret = fork();
    if (ret == 0) { // child
        const char hostname[] = "container01";
        sethostname(hostname, strlen(hostname));
        mount("./new_root", "./new_root", NULL, MS_BIND, NULL); // 來自參考文章,pivot_root命令要求原根目錄和新根目錄不能在同一個掛載點下
                                                                // 所以這裏使用bind mount,在原地創建一個新的掛載點
        chdir("./new_root");
        syscall(SYS_pivot_root, "./", "./old_root");            // 調用pivot_root,只支持syscall方式,參考man手冊
        mount("none", "/proc", "proc", 0, NULL);                // 掛載 proc 文件系統,ps命令依賴於該文件系統,若不掛載,其輸出依舊是宿主機中的進程信息
        umount("/old_root");
        exec_cmd();                                             // 執行busybox安裝程序
    } else if (ret > 0) { // parent
        waitpid(ret, NULL, 0);
        exit(0);
    }
}

安裝busybox並執行shell程序

安裝程序通過調用execl實現即可,安裝之後以同樣的方式啓動/bin下的sh程序即可:

void exec_cmd() {
    int ret = 0;
    if ((ret = fork()) > 0) { // parent
        waitpid(ret, NULL, 0);                                          // 等待安裝完畢
        execl("/bin/sh", "sh", NULL);                                   // 啓動shell
        errExit("execl");
    }
    if (ret == 0) { // child
        execl("/bin/busybox", "busybox", "--install", "/bin/", NULL);   // 安裝
        errExit("execl");
    }
    errExit("fork");
}

完整代碼

#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <syscall.h>
#include <unistd.h>
#include <wordexp.h>

#define logger(A)                      \
    do {                               \
        fprintf(stdout, "%s...\n", A); \
        fflush(stdout);                \
    } while (0)

#define md(A) mkdir(A, S_IRWXU | S_IRWXG)

const void errExit(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    fprintf(stderr, fmt, ap);
    fprintf(stderr, ".\nerrno: %d, strerror: %s\n", errno, strerror(errno));
    va_end(ap);
    exit(EXIT_FAILURE);
}

void cpy_file(const char old_path[], const char new_path[]) {
    FILE *oldfp, *newfp;
    oldfp = fopen(old_path, "rb");
    newfp = fopen(new_path, "wb");
    while (!feof(oldfp)) {
        const int buf_size = 4096;
        char buf[buf_size] = {};
        int n = fread(buf, 1, buf_size, oldfp);
        fwrite(buf, 1, n, newfp);
    }
    fclose(oldfp);
    fflush(newfp);
    fclose(newfp);
    chmod(new_path, S_IRWXU | S_IRWXG);
}

void prepare_dirs() {
    logger("preparing dirs");
    md("./simp_container_root/");
    chdir("./simp_container_root/");
    md("./new_root");
    md("./new_root/bin");
    md("./new_root/data");
    md("./new_root/proc");
    md("./new_root/old_root");
}

void exec_cmd() {
    int ret = 0;
    if ((ret = fork()) > 0) { // parent
        waitpid(ret, NULL, 0);
        execl("/bin/sh", "sh", NULL);
        errExit("execl");
    }
    if (ret == 0) { // child
        execl("/bin/busybox", "busybox", "--install", "/bin/", NULL);
        errExit("execl");
    }
    errExit("fork");
}

void do_unshare() {
    logger("doing unshare");
    int ret = 0;
    int us_flags = CLONE_NEWCGROUP | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWUTS;
    ret = unshare(us_flags);
    if (ret < 0)
        errExit("unshare");
    if ((ret = fork()) < 0)
        errExit("fork");
    if (ret == 0) { // child
        const char hostname[] = "container01";
        logger("setting hostname");
        sethostname(hostname, strlen(hostname));
        logger("chdir to new root");
        mount("./new_root", "./new_root", NULL, MS_BIND, NULL);
        chdir("./new_root");
        logger("pivot root");
        syscall(SYS_pivot_root, "./", "./old_root");
        logger("doing mount and umount");
        mount("none", "/proc", "proc", 0, NULL);
        umount("/old_root");
        logger("set env and exec busybox sh");
        setenv("hostname", "container01", 0);
        exec_cmd();
    }
    if (ret > 0) { // parent
        waitpid(ret, NULL, 0);
        exit(0);
    }
}

void create_container() {
    logger("preparing root");
    prepare_dirs();
    // copy file
    logger("copying file");
    cpy_file("/home/eric/coding/busybox", "./new_root/bin/busybox");
    do_unshare();
    errExit("do unshare");

}

int main(int argc, char const *argv[]) {
    logger("Running...");
    create_container();
}

總結

本文通過Linux中的系統調用實現了一個超簡單容器,由於缺少必須的配置信息如用戶ID、網絡、hostname等,busybox的一些命令無法使用。另外,程序中缺少必要的錯誤處理,因此代碼僅供參考,歡迎評論指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章