diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f9a282 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +copy.sh +tc +ts \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ec1a6a --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Creating a container from scratch + +This repository contains the demo files for how to create a Linux container from scratch. For detailed information about the files in this repo and how to use them go to http://hectorcorrea.com/blog/tiny-container + +## Quick rundown + +If you want to compile the code follow the following steps on a Linux machine with Go installed: + +``` +$ git clone https://github.com/hectorcorrea/tiny-container.git +$ cd tiny-container +$ GOOS=linux go build -o tc tinyContainer.go +$ GOOS=linux go build -o ts tinyShell.go + +$ ./tc -root=/root/tiny-container -shell=./ts +Tiny shell started +ts: _ +``` + +## Quick rundown (without the source code) + +Download TinyContainer (`tc`) and TinyShell (`ts`) from https://github.com/hectorcorrea/tiny-container/releases and run + +``` +$ pwd +/root/tiny-container + +$ ./tc -root=/root/tiny-container -shell=./ts +Tiny shell started +ts: _ +``` + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e17e263 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +GOOS=linux go build -o tc tinyContainer.go +GOOS=linux go build -o ts tinyShell.go diff --git a/createBusybox.sh b/createBusybox.sh new file mode 100755 index 0000000..1207067 --- /dev/null +++ b/createBusybox.sh @@ -0,0 +1,24 @@ +# Downloads BusyBox and creates symlinks for each of the +# commands that it supports so that they are available +# by ther Linux common names (e.g. ls, hostname, et cetera) + +BUSYBOX_ROOT=./bb_root/bin + +echo "Creating BusyBox directory: $BUSYBOX_ROOT" +mkdir -p $BUSYBOX_ROOT +cd $BUSYBOX_ROOT + +echo "Downloading BusyBox..." +curl https://www.busybox.net/downloads/binaries/1.30.0-i686/busybox > busybox +chmod u+x busybox + +echo "Creating symlinks..." +for i in $(busybox --list) +do + if [ "$i" != "busybox" ] + then + ln -s busybox $i + fi +done + +echo "Done" \ No newline at end of file diff --git a/tinyContainer.go b/tinyContainer.go new file mode 100644 index 0000000..dea86bb --- /dev/null +++ b/tinyContainer.go @@ -0,0 +1,204 @@ +// This program creates a Linux container using system calls +// instead of a separate tool (like Docker). The idea is to +// see what functionality Linux provides by itself. +// +// This program MUST BE RUN on a Linux machine. +// +// Sources: +// "Linux Containers and Virtualization: A Kernel Perspective" by Shashank Mohan Jain (p.93-106) +// https://medium.com/@ssttehrani/containers-from-scratch-with-golang-5276576f9909 +// https://medium.com/@jain.sm/writing-your-own-linux-container-259054465bd1 +// +// To compile: +// $ GOOS=linux go build -o tc tinyContainer.go +// +// You must compile for Linux (notice GOOS=linux), it will not compile +// on Mac or Windows without it. +// +// Usage: +// $ pwd +// /root/tiny-container +// $ ./tc -root=/root/tiny-container -shell=./ts +// Creating container... +// Tiny shell started +// $ ls +// ... +// +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "syscall" +) + +var root, shell, xaction string + +func init() { + flag.StringVar(&root, "root", "", "Full path of directory to mount as root in the container. Required.") + flag.StringVar(&shell, "shell", "", "Path to shell program to run (relative to root once mounted). Required.") + flag.StringVar(&xaction, "x-action", "create", "Used internally. Please ignore.") + flag.Parse() +} + +func main() { + + // Root and shell are required args + if root == "" || shell == "" { + printHelp() + os.Exit(1) + } + + if xaction == "create" { + // Create the wrapper for the container + createContainer(root, shell) + os.Exit(0) + } + + if xaction == "launch-shell" { + // This is used internally to lanch the shell inside + // the container. Therefore this command must be exectuted + // AFTER the container has been created. + runShell(root, shell) + os.Exit(0) + } + + printHelp() + os.Exit(1) +} + +func createContainer(root string, shell string) { + fmt.Printf("Creating container...\n") + + args := []string{"-root=" + root, "-shell=" + shell, "-x-action=launch-shell"} + cmd := exec.Command("/proc/self/exe", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // These flags are what instruct Linux to create a new container + // (notice NEWNS, NEWUTS, ...) as it runs the command. + var flags uintptr + flags = syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | + syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | + syscall.CLONE_NEWNET | syscall.CLONE_NEWUSER + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: flags, + UidMappings: []syscall.SysProcIDMap{ + { + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }, + }, + GidMappings: []syscall.SysProcIDMap{ + { + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }, + }, + } + if err := cmd.Run(); err != nil { + fmt.Printf("Error running the /proc/self/exe container - %s\n", err) + os.Exit(1) + } + + fmt.Printf("Exited container\n") +} + +func runShell(root string, shell string) { + fmt.Printf("Launching shell session...\n") + fmt.Printf("\troot.: %s\n", root) + fmt.Printf("\tshell: %s\n", shell) + + cmd := exec.Command(shell) + + cmd.Env = []string{"tiny_demo=something tiny"} + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Set the hostname + err := syscall.Sethostname([]byte("tinyhost")) + if err != nil { + fmt.Printf("Error setting hostname - %s\n", err) + } + + // Pivot to our new root folder + err = pivotRoot(root) + if err != nil { + fmt.Printf("Error running pivot_root - %s\n", err) + os.Exit(1) + } + + // Launch the new shell session + err = cmd.Run() + if err != nil { + fmt.Printf("Error running the shell %s - %s\n", shell, err) + os.Exit(1) + } + + fmt.Printf("Exited shell session\n") +} + +func pivotRoot(newRoot string) error { + putold := filepath.Join(newRoot, "/.pivot_root") + + // Bind mount `newroot` to itself. + // This is a slight hack needed to satisfy the `pivot_root` + // requirement that `newroot` and `putold` must not be on + // the same filesystem as the current root + err := syscall.Mount(newRoot, newRoot, "", syscall.MS_BIND|syscall.MS_REC, "") + if err != nil { + return err + } + + // create putold directory + err = os.MkdirAll(putold, 0700) + if err != nil { + return err + } + + // call pivot_root + err = syscall.PivotRoot(newRoot, putold) + if err != nil { + return err + } + + // ensure current working directory is set to new root + err = os.Chdir("/") + if err != nil { + return err + } + + //umount putold, which now lives at /.pivot_root + putold = "/.pivot_root" + err = syscall.Unmount(putold, syscall.MNT_DETACH) + if err != nil { + return err + } + + // remove putold + err = os.RemoveAll(putold) + if err != nil { + return err + } + return nil +} + +func printHelp() { + fmt.Println("tinyContainer (tc) parameters:") + flag.PrintDefaults() + fmt.Println("") + fmt.Println("Example:") + fmt.Println("") + fmt.Println(" $ pwd") + fmt.Println(" /root/tiny-container") + fmt.Println(" $ ./tc -root=/root/tiny-container -shell=/ts") + fmt.Println("") +} diff --git a/tinyShell.go b/tinyShell.go new file mode 100644 index 0000000..66fb330 --- /dev/null +++ b/tinyShell.go @@ -0,0 +1,122 @@ +// Implements a tiny shell that we can use to run in our Linux container +// if we don't want to import other Linux binaries. It emulates a few +// basic Linux commands: `cat`, `cd`, `env`, `hostname`, `ls`, and `pwd`. +// +// Compile: +// $ GOOS=linux go build -o ts tinyShell.go +// Run: +// $ ./ts +// +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func main() { + fmt.Println("Tiny shell started") + fmt.Println("Valid commands: cat, cd [dir], env, hostname, ls, pwd, quit") + pwd, _ := filepath.Abs(".") + home := pwd + for true { + cmd, arg := readCommand("ts: ") + if cmd == "quit" || cmd == "exit" { + break + } + + switch { + case cmd == "cat": + cat(pwd, arg) + case cmd == "env": + env() + case cmd == "hostname": + hostname() + case cmd == "ls": + ls(pwd, arg) + case cmd == "pwd": + fmt.Printf("%s\n", pwd) + case cmd == "cd": + if arg == "" { + pwd = cd(pwd, home) + } else { + pwd = cd(pwd, arg) + } + case cmd == "": + // nothing to do + default: + fmt.Printf("Unknown command: %s\n", cmd) + } + } + fmt.Println("Tiny shell ended") +} + +func cat(pwd string, filename string) { + fullname, _ := filepath.Abs(filepath.Join(pwd, filename)) + bytes, err := ioutil.ReadFile(fullname) + if err == nil { + fmt.Printf("%s", string(bytes)) + } else { + fmt.Printf("Error reading %s: %s\n", fullname, err) + } +} + +func cd(pwd string, dir string) string { + if filepath.IsAbs(dir) { + return dir + } + newPwd, _ := filepath.Abs(filepath.Join(pwd, dir)) + return newPwd +} + +func env() { + for _, env := range os.Environ() { + fmt.Printf("%s\n", env) + } +} + +func hostname() { + hostname, err := os.Hostname() + if err != nil { + fmt.Printf("Error: %s\n", err) + } else { + fmt.Printf("Hostname: %s\n", hostname) + } +} + +func ls(pwd string, dir string) { + var path string + if filepath.IsAbs(dir) { + path = dir + } else { + path, _ = filepath.Abs(filepath.Join(pwd, dir)) + } + + fmt.Printf("Files in: %s\n", path) + files, err := ioutil.ReadDir(path) + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + for _, f := range files { + fmt.Println("\t" + f.Name()) + } +} + +func readCommand(prompt string) (string, string) { + fmt.Printf("%s", prompt) + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + + tokens := strings.Split(strings.TrimSpace(text), " ") + cmd := tokens[0] + arg := "" + if len(tokens) == 2 { + arg = tokens[1] + } + return cmd, arg +}