├── go.mod ├── .gitignore ├── .travis.yml ├── arch.go ├── LICENSE ├── README.md ├── main.go └── container.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muesli/scratchy 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Binaries 15 | scratchy 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - linux 5 | 6 | go: 7 | - 1.11.x 8 | - 1.12.x 9 | - 1.13.x 10 | - tip 11 | 12 | matrix: 13 | allow_failures: 14 | - go: tip 15 | 16 | env: 17 | global: 18 | GO111MODULE=on 19 | 20 | notifications: 21 | email: 22 | on_success: change 23 | on_failure: always 24 | 25 | script: go build 26 | -------------------------------------------------------------------------------- /arch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func archBootstrap(dst string) error { 10 | fmt.Println("Bootstrapping arch:", dst) 11 | 12 | cmd := exec.Command("pacstrap", "-c", dst, "base") 13 | cmd.Stdout = os.Stdout 14 | cmd.Stdin = os.Stdin 15 | cmd.Stderr = os.Stderr 16 | return cmd.Run() 17 | } 18 | 19 | func archInstall(dst string, packages ...string) error { 20 | if len(packages) == 0 { 21 | return nil 22 | } 23 | 24 | fmt.Printf("Installing in root (%s): %s\n", dst, packages) 25 | 26 | args := []string{"--root", dst, "-S", "--needed", "--noconfirm"} 27 | args = append(args, packages...) 28 | cmd := exec.Command("pacman", args...) 29 | cmd.Stdout = os.Stdout 30 | cmd.Stdin = os.Stdin 31 | cmd.Stderr = os.Stderr 32 | return cmd.Run() 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scratchy 2 | ======== 3 | 4 | Quickly bootstrap a Linux distro in a (non-Docker) container and interactively 5 | execute something in it. 6 | 7 | Note: this is early stage and automatic bootstrapping only supports ArchLinux 8 | as of now. 9 | 10 | ## Installation 11 | 12 | ### From Source 13 | 14 | Make sure you have a working Go environment (Go 1.11 or higher is required). 15 | See the [install instructions](http://golang.org/doc/install.html). 16 | 17 | Compiling scratchy is easy, simply run: 18 | 19 | git clone https://github.com/muesli/scratchy.git 20 | cd scratchy 21 | go build 22 | 23 | ## Usage 24 | 25 | Bootstrap a new base ArchLinux install into a directory: 26 | (make sure package `arch-install-scripts` is installed on your host!) 27 | 28 | ``` 29 | $ sudo scratchy bootstrap /some/root 30 | ``` 31 | 32 | ``` 33 | Bootstrapping arch: /some/root 34 | ==> Creating install root at /some/root 35 | ==> Installing packages to /some/root 36 | 37 | ... 38 | 39 | Successfully bootstrapped root: /some/root 40 | ``` 41 | 42 | Start a bash shell inside the container: 43 | 44 | ``` 45 | $ sudo scratchy run /some/root /bin/bash 46 | ``` 47 | 48 | ``` 49 | Copying /etc/resolv.conf to /some/root/etc/resolv.conf 50 | Executing in container (/some/root): /bin/bash 51 | [root@container /]# 52 | ``` 53 | 54 | Start with a specific uid/gid inside container: 55 | 56 | ``` 57 | $ sudo scratchy -uid 1000 -gid 1000 run /some/root /bin/ps ax 58 | ``` 59 | 60 | ``` 61 | Executing in container (/some/root): /bin/ps ax 62 | PID TTY STAT TIME COMMAND 63 | 1 ? Sl+ 0:00 /proc/self/exe -uid 1000 -gid 1000 child /some/root /bin/ps ax 64 | 6 ? R+ 0:00 /bin/ps ax 65 | ``` 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strconv" 10 | "syscall" 11 | ) 12 | 13 | var ( 14 | uid = flag.Uint("uid", 0, "uid to run as (inside container)") 15 | gid = flag.Uint("gid", 0, "gid to run as (inside container)") 16 | ) 17 | 18 | func printUsage() { 19 | fmt.Println(`Usage: 20 | scratch bootstrap 21 | scratch run `) 22 | os.Exit(1) 23 | } 24 | 25 | func main() { 26 | flag.Parse() 27 | if len(flag.Args()) < 2 { 28 | printUsage() 29 | } 30 | 31 | root, err := filepath.Abs(flag.Args()[1]) 32 | if err != nil { 33 | fmt.Println("Invalid root:", err) 34 | os.Exit(1) 35 | } 36 | 37 | switch flag.Args()[0] { 38 | case "bootstrap": 39 | err := os.MkdirAll(root, 0755) 40 | if err != nil { 41 | fmt.Println("Cannot create root:", err) 42 | os.Exit(1) 43 | } 44 | if err := archBootstrap(root); err != nil { 45 | fmt.Println("Bootstrap failed:", err) 46 | os.Exit(1) 47 | } 48 | if err := archInstall(root, flag.Args()[2:]...); err != nil { 49 | fmt.Println("Installing packages failed:", err) 50 | os.Exit(1) 51 | } 52 | 53 | fmt.Println("Successfully bootstrapped root:", root) 54 | 55 | case "run": 56 | if err := hostCopy(root, "/etc/resolv.conf"); err != nil { 57 | fmt.Println("Cannot copy resolv.conf:", err) 58 | os.Exit(1) 59 | } 60 | 61 | if err := run(root); err != nil { 62 | fmt.Println(err) 63 | os.Exit(1) 64 | } 65 | 66 | case "child": 67 | if err := child(root); err != nil { 68 | if exiterr, ok := err.(*exec.ExitError); ok { 69 | // exited with exit code != 0 70 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 71 | // fmt.Println(err) 72 | os.Exit(status.ExitStatus()) 73 | } 74 | } 75 | 76 | os.Exit(1) 77 | } 78 | 79 | default: 80 | printUsage() 81 | } 82 | } 83 | 84 | func run(root string) error { 85 | // fmt.Printf("Running %v\n", flag.Args()[1:]) 86 | 87 | // bind-mount root so we have a proper mtab in the container 88 | if err := syscall.Mount(root, root, "", syscall.MS_BIND, ""); err != nil { 89 | return err 90 | } 91 | defer func() { 92 | fmt.Println("Unmounting root:", root) 93 | if err := syscall.Unmount(root, 0); err != nil { 94 | panic(err) 95 | } 96 | }() 97 | 98 | // collect params we pass to the child 99 | params := []string{ 100 | "-uid", strconv.FormatUint(uint64(*uid), 10), 101 | "-gid", strconv.FormatUint(uint64(*gid), 10), 102 | "child", 103 | } 104 | params = append(params, flag.Args()[1:]...) 105 | 106 | // restart ourselves within our own namespace 107 | cmd := exec.Command("/proc/self/exe", params...) 108 | cmd.Stdin = os.Stdin 109 | cmd.Stdout = os.Stdout 110 | cmd.Stderr = os.Stderr 111 | cmd.SysProcAttr = &syscall.SysProcAttr{ 112 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, 113 | Unshareflags: syscall.CLONE_NEWNS, 114 | } 115 | return cmd.Run() 116 | } 117 | -------------------------------------------------------------------------------- /container.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | "syscall" 14 | ) 15 | 16 | func cg() error { 17 | cgroups := "/sys/fs/cgroup/" 18 | pids := filepath.Join(cgroups, "pids") 19 | os.Mkdir(filepath.Join(pids, "scratch"), 0755) 20 | 21 | if err := ioutil.WriteFile(filepath.Join(pids, "scratch/pids.max"), []byte("1024"), 0700); err != nil { 22 | return err 23 | } 24 | 25 | // Removes the new cgroup in place after the container exits 26 | if err := ioutil.WriteFile(filepath.Join(pids, "scratch/notify_on_release"), []byte("1"), 0700); err != nil { 27 | return err 28 | } 29 | 30 | return ioutil.WriteFile(filepath.Join(pids, "scratch/cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0700) 31 | } 32 | 33 | func importEnv() []string { 34 | env := os.Environ() 35 | 36 | // TODO: filter environment variables 37 | return env 38 | } 39 | 40 | func child(root string) error { 41 | if err := cg(); err != nil { 42 | return err 43 | } 44 | 45 | args := flag.Args() 46 | if len(args) < 3 { 47 | args = append(args, "/bin/bash") 48 | } 49 | fmt.Printf("Executing in container (%s): %s\n", root, strings.Join(args[2:], " ")) 50 | 51 | // set hostname 52 | if err := syscall.Sethostname([]byte("container")); err != nil { 53 | return err 54 | } 55 | 56 | // bind-mount /dev from outside 57 | if err := syscall.Mount("/dev", filepath.Join(root, "dev"), "devtmpfs", 58 | syscall.MS_BIND|syscall.MS_NOSUID, ""); err != nil { 59 | return err 60 | } 61 | defer func() { 62 | fmt.Println("Unmounting /dev") 63 | if err := syscall.Unmount("dev", 0); err != nil { 64 | panic(err) 65 | } 66 | }() 67 | 68 | // setup chroot 69 | if err := syscall.Chroot(root); err != nil { 70 | return err 71 | } 72 | if err := os.Chdir("/"); err != nil { 73 | return err 74 | } 75 | 76 | // mount /proc 77 | if err := syscall.Mount("proc", "proc", "proc", 0, ""); err != nil { 78 | return err 79 | } 80 | defer func() { 81 | fmt.Println("Unmounting /proc") 82 | if err := syscall.Unmount("proc", 0); err != nil { 83 | panic(err) 84 | } 85 | }() 86 | 87 | // mount /tmp 88 | if err := syscall.Mount("tmpfs", "tmp", "tmpfs", 0, ""); err != nil { 89 | return err 90 | } 91 | defer func() { 92 | fmt.Println("Unmounting /tmp") 93 | if err := syscall.Unmount("tmp", 0); err != nil { 94 | panic(err) 95 | } 96 | }() 97 | 98 | // execute command in container 99 | return execWithCredentials(uint32(*uid), uint32(*gid), args[2], args[3:]...) 100 | } 101 | 102 | func execWithCredentials(uid, gid uint32, name string, arg ...string) error { 103 | cmd := exec.Command(name, arg...) 104 | cmd.Stdin = os.Stdin 105 | cmd.Stdout = os.Stdout 106 | cmd.Stderr = os.Stderr 107 | cmd.Env = importEnv() 108 | cmd.SysProcAttr = &syscall.SysProcAttr{ 109 | Credential: &syscall.Credential{Uid: uid, Gid: gid}, 110 | } 111 | 112 | return cmd.Run() 113 | } 114 | 115 | func hostCopy(dst string, src string) (err error) { 116 | dst = filepath.Join(dst, src) 117 | fmt.Println("Copying", src, "to", dst) 118 | 119 | in, err := os.Open(src) 120 | if err != nil { 121 | return 122 | } 123 | defer in.Close() 124 | 125 | stat, err := os.Stat(src) 126 | if err != nil { 127 | return 128 | } 129 | out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, stat.Mode()) 130 | if err != nil { 131 | return 132 | } 133 | defer func() { 134 | cerr := out.Close() 135 | if err == nil { 136 | err = cerr 137 | } 138 | }() 139 | if _, err = io.Copy(out, in); err != nil { 140 | return 141 | } 142 | err = out.Sync() 143 | return 144 | } 145 | --------------------------------------------------------------------------------