Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorcorrea committed Mar 8, 2021
0 parents commit eefbab4
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
copy.sh
tc
ts
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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: _
```

2 changes: 2 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GOOS=linux go build -o tc tinyContainer.go
GOOS=linux go build -o ts tinyShell.go
24 changes: 24 additions & 0 deletions createBusybox.sh
Original file line number Diff line number Diff line change
@@ -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"
204 changes: 204 additions & 0 deletions tinyContainer.go
Original file line number Diff line number Diff line change
@@ -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("")
}
122 changes: 122 additions & 0 deletions tinyShell.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit eefbab4

Please sign in to comment.