├── .gitignore ├── README.md ├── go.mod ├── main.go └── ubuntu-base-22.04-base-amd64.tar.gz /.gitignore: -------------------------------------------------------------------------------- 1 | containers 2 | focker -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Focker 2 | 3 | Focker is a toy container runtime written in Go, designed to create and manage lightweight Linux containers. 4 | 5 | ## Features 6 | 7 | - Namespace Isolation: Uses Linux namespaces to isolate processes, mount points, and hostname. 8 | - Filesystem Handling: Extracts a base Ubuntu 22.04 filesystem tarball for container use. 9 | - Process Management: Runs specified commands inside isolated containers. 10 | - Bind Mounts: Easy file and directory sharing between host and containers 11 | 12 | ## Requirements 13 | 14 | - Go (tested with go1.21.5) 15 | - Linux kernel with support for namespaces (tested on Ubuntu 22.04, Pop!\_OS) 16 | - Requires root privileges to operate due to its use of Linux namespaces. 17 | 18 | ## Usage 19 | 20 | 1. Building 21 | 22 | ```bash 23 | go build -o focker 24 | ``` 25 | 26 | 2. Running Containers 27 | ```bash 28 | sudo ./focker run [args...] 29 | ``` 30 | 31 | ## Resources 32 | 33 | - [Containers From Scratch • Liz Rice • GOTO 2018](https://www.youtube.com/watch?v=8fi7uSYlOdc) 34 | - [Ubuntu 22.04 Base (rootfs)](https://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/) 35 | - [pivot_root(2) — Linux manual page](https://man7.org/linux/man-pages/man2/pivot_root.2.html) 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/biraj21/focker 2 | 3 | go 1.21.5 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | const containersDir = "./containers" 18 | const rootFsTarball = "./ubuntu-base-22.04-base-amd64.tar.gz" 19 | 20 | func init() { 21 | exitIfError(os.MkdirAll(containersDir, 0700), "init containersDir") 22 | } 23 | 24 | func main() { 25 | if len(os.Args) < 2 { 26 | log.Fatal("a command is required") 27 | } 28 | 29 | command := os.Args[1] 30 | switch command { 31 | case "run", "_child": 32 | // the run command will just init a new isolated process (i.e the container) with _child command, 33 | // in which we will actually run the command. so we first create a container and then inside 34 | // it we run the command that user specified 35 | 36 | var volumes []string 37 | var args []string 38 | 39 | if len(os.Args) > 2 { 40 | for _, arg := range os.Args[2:] { 41 | if strings.HasPrefix(arg, "-v=") { 42 | volumes = append(volumes, strings.TrimPrefix(arg, "-v=")) 43 | } else { 44 | args = append(args, arg) 45 | } 46 | } 47 | } 48 | 49 | run(args, volumes, command == "_child") 50 | 51 | case "ps": 52 | ps() 53 | 54 | default: 55 | log.Fatal("bad command") 56 | } 57 | } 58 | 59 | func run(args []string, volumes []string, isChild bool) { 60 | if len(args) == 0 { 61 | log.Fatal("at least 1 argument is required") 62 | } 63 | 64 | // if isChild is true, then it means that we're inside the container 65 | 66 | var commandName string 67 | var commandArgs []string 68 | if isChild { 69 | // if this is the child process, then we run the command that the user passed 70 | commandName = args[0] 71 | if len(args) > 1 { 72 | commandArgs = args[1:] 73 | } 74 | } else { 75 | // otherwise we'll run this program itself in a separate process with an internal 76 | // _child command and it will be responsible for running user specified command 77 | path, err := os.Executable() 78 | exitIfError(err, "os.Executable()") 79 | commandName = path 80 | commandArgs = append(commandArgs, "_child") 81 | 82 | // pass the volumes again with -v= add command-line arguments 83 | if len(volumes) > 0 { 84 | volumeArgs := make([]string, len(volumes)) 85 | for i, vol := range volumes { 86 | volumeArgs[i] = "-v=" + vol 87 | } 88 | 89 | commandArgs = append(commandArgs, volumeArgs...) 90 | } 91 | 92 | commandArgs = append(commandArgs, args...) 93 | } 94 | 95 | // create Cmd struct to execute the given command 96 | cmd := exec.Command(commandName, commandArgs...) 97 | 98 | // wire child process's stdin, stdout & stderr to that of current process 99 | cmd.Stdin = os.Stdin 100 | cmd.Stdout = os.Stdout 101 | cmd.Stderr = os.Stderr 102 | 103 | if isChild { 104 | containerId := "b-" + randomString(16) 105 | 106 | // set hostname inside container to a random string 107 | exitIfError(syscall.Sethostname([]byte(containerId)), "set hostname") 108 | 109 | // extract the rootfs tarball 110 | rootfsDir := filepath.Join(containersDir, containerId) 111 | unzipRootFsTarball(rootfsDir, rootFsTarball) 112 | 113 | // map volumes to share storage between host & container 114 | mountedVolumes := make([]string, len(volumes)) 115 | for i, volume := range volumes { 116 | parts := strings.Split(volume, ":") 117 | if len(parts) != 2 { 118 | log.Fatalf("invalid volume mapping: %s", volume) 119 | } 120 | 121 | source := parts[0] 122 | target := filepath.Join(rootfsDir, parts[1]) 123 | 124 | exitIfError(os.MkdirAll(target, 0700), "mkdir target") 125 | exitIfError(syscall.Mount(source, target, "", syscall.MS_BIND|syscall.MS_REC, ""), "mount volume") 126 | 127 | // add to the list of mounted volumes 128 | mountedVolumes[i] = parts[1] 129 | } 130 | 131 | // defer the unmounting of all volumes 132 | defer func() { 133 | for _, target := range mountedVolumes { 134 | if err := syscall.Unmount(target, 0); err != nil { 135 | log.Printf("failed to unmount %s: %v", target, err) 136 | } 137 | } 138 | }() 139 | 140 | // set the root directory inside the container to the extracted rootfs 141 | // abortIfError(syscall.Chroot(rootfsDir), "chroot") 142 | pivotRoot(rootfsDir) 143 | 144 | // set procfs: tell kernel that for this process (& it's children), use this new /proc directory as procfs 145 | // for procfs, first arg can be anything ig because the kernal ignores it (based on chat with claude & my experiments) 146 | exitIfError(syscall.Mount("proc", "/proc", "proc", 0, ""), "mount procfs") 147 | defer syscall.Unmount("/proc", 0) 148 | 149 | // if we were to configure the above things in the main process, then it would have 150 | // modified the system's hostname, root etc. 151 | 152 | fmt.Println("pid", os.Getpid(), "running", commandName) 153 | } else { 154 | // we want the child process that we're about to fork to be isolated 155 | cmd.SysProcAttr = &syscall.SysProcAttr{ 156 | Cloneflags: 157 | // UTS namespace: isolates hostname and domain name 158 | syscall.CLONE_NEWUTS | 159 | // PID namespace: isolates process IDs 160 | syscall.CLONE_NEWPID | 161 | // Mount namespace: isolates mount points 162 | syscall.CLONE_NEWNS, 163 | 164 | // unshare container's mount points with the host 165 | // basically, i've created a new mount namespace for my container about 166 | // & i don't want it to be shared with the host 167 | Unshareflags: syscall.CLONE_NEWNS, 168 | } 169 | } 170 | 171 | err := cmd.Run() 172 | if err != nil { 173 | fmt.Fprintln(os.Stderr, err) 174 | } 175 | 176 | os.Exit(cmd.ProcessState.ExitCode()) 177 | } 178 | 179 | func ps() { 180 | files, err := os.ReadDir(containersDir) 181 | exitIfError(err, "ps(): os.ReadDir()") 182 | 183 | for _, file := range files { 184 | fileInfo, err := file.Info() 185 | exitIfError(err, "ps(): file.Info()") 186 | 187 | fmt.Println(file.Name(), fileInfo.ModTime().Format(time.UnixDate)) 188 | } 189 | } 190 | 191 | func exitIfError(err error, label string) { 192 | if err != nil { 193 | if len(label) > 0 { 194 | log.Fatal(label, ": ", err) 195 | } else { 196 | log.Fatal(err) 197 | } 198 | } 199 | } 200 | 201 | const randomStringChars string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 202 | 203 | func randomString(length int) string { 204 | if length < 1 { 205 | return "" 206 | } 207 | 208 | availableRunes := []rune(randomStringChars) 209 | 210 | r := make([]rune, length) 211 | 212 | for i := 0; i < length; i++ { 213 | r[i] = availableRunes[rand.Intn(len(randomStringChars))] 214 | } 215 | 216 | return string(r) 217 | } 218 | 219 | func unzipRootFsTarball(dest string, src string) { 220 | exitIfError(os.MkdirAll(dest, 0700), "unzipRootFsTarball(): os.MkdirAll()") 221 | 222 | cmd := exec.Command("tar", []string{"-xzf", src, "-C", dest}...) 223 | exitIfError(cmd.Run(), "unzipRootFsTarball(): tar cmd.Run()") 224 | } 225 | 226 | func pivotRoot(newRoot string) { 227 | // pivot_root system call requires new_root arg to be a mount point. here's a line from man pages 228 | // new_root must be a path to a mount point, but can't be "/". A path that is not already a mount point can be converted into one by bind mounting the path onto itself. 229 | exitIfError( 230 | syscall.Mount(newRoot, newRoot, "", syscall.MS_BIND|syscall.MS_REC, ""), 231 | "pivotRoot(): syscall.Mount", 232 | ) 233 | 234 | // put_old must be a subdirectory inside new_root 235 | putOld := filepath.Join(newRoot, ".put_old") 236 | exitIfError(os.MkdirAll(putOld, 0700), "pivotRoot(): putold os.MkdirAll") 237 | 238 | // use pivot_root system call to set the root directory inside the container to the extracted rootfs 239 | exitIfError(syscall.PivotRoot(newRoot, putOld), "pivotRoot(): pivot_root") 240 | 241 | // set current working directory to the new root directory 242 | exitIfError(syscall.Chdir("/"), "chdir") 243 | } 244 | -------------------------------------------------------------------------------- /ubuntu-base-22.04-base-amd64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biraj21/focker/43a5aedae6acf06b9af326d95474cbd24f38c8a9/ubuntu-base-22.04-base-amd64.tar.gz --------------------------------------------------------------------------------