package daemon import ( "fmt" "os" "sync" "syscall" "github.com/criyle/go-sandbox/pkg/forkexec" "github.com/criyle/go-sandbox/pkg/memfd" "github.com/criyle/go-sandbox/pkg/mount" "github.com/criyle/go-sandbox/pkg/unixsocket" "golang.org/x/sys/unix" ) // Builder builds instance of container masters type Builder struct { // Root is container root mount path, empty uses current work path Root string // Mounts defines container mount points, empty uses default mounts Mounts []mount.SyscallParams // Stderr defines whether to dup container stderr to stderr for debug Stderr bool // ExecFile defines executable that called Init, otherwise defer current // executable (/proc/self/exe) ExecFile *os.File // CredGenerator defines a credential generator used to create new container CredGenerator CredGenerator } // CredGenerator generates uid / gid credential used by container // to isolate process and file system access type CredGenerator interface { Get() syscall.Credential } // Master manages single pre-forked container type Master struct { pid int // underlying container init pid socket *unixsocket.Socket // master - container communication mu sync.Mutex // lock to avoid race condition } // Build creates new master with underlying container func (b *Builder) Build() (*Master, error) { var ( err error cred syscall.Credential uidMap, gidMap []syscall.SysProcIDMap ) // container mount points mounts := b.Mounts if len(mounts) == 0 { if mounts, err = mount.NewBuilder().WithMounts(DefaultMounts).Build(true); err != nil { return nil, fmt.Errorf("daemon: failed to build rootfs mount %v", err) } } root := b.Root if root == "" { if root, err = os.Getwd(); err != nil { return nil, fmt.Errorf("daemon: failed to get work directory %v", err) } } // prepare stdin / stdout / stderr devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, os.ModePerm) if err != nil { return nil, fmt.Errorf("daemon: failed to open devNull(%v)", err) } defer devNull.Close() files := make([]uintptr, 0, 4) files = append(files, devNull.Fd(), devNull.Fd()) if b.Stderr { files = append(files, os.Stderr.Fd()) } else { files = append(files, devNull.Fd()) } // prepare self memfd execFile, err := b.exec() if err != nil { return nil, fmt.Errorf("deamon: %v", err) } defer execFile.Close() // prepare socket ins, outs, err := newPassCredSocketPair() if err != nil { return nil, fmt.Errorf("daemon: failed to create socket: %v", err) } defer outs.Close() outf, err := outs.File() if err != nil { ins.Close() return nil, fmt.Errorf("daemon: failed to dup file outs(%v)", err) } defer outf.Close() files = append(files, uintptr(outf.Fd())) // prepare credential if b.CredGenerator != nil { cred = b.CredGenerator.Get() uidMap, gidMap = getIDMapping(&cred) } r := &forkexec.Runner{ Args: []string{os.Args[0], initArg}, Env: []string{DefaultPath}, ExecFile: execFile.Fd(), Files: files, WorkDir: containerWD, CloneFlags: forkexec.UnshareFlags, Mounts: mounts, HostName: containerName, DomainName: containerName, PivotRoot: root, UIDMappings: uidMap, GIDMappings: gidMap, } pid, err := r.Start() if err != nil { ins.Close() return nil, fmt.Errorf("daemon: failed to execve(%v)", err) } m := &Master{ pid: pid, socket: ins, } // set configuration and check if container creation successful if err = m.conf(&containerConfig{ Cred: b.CredGenerator != nil, }); err != nil { m.Destroy() return nil, err } return m, nil } // Destroy kill the daemon process (with container) // if stderr enabled, collect the output as error func (m *Master) Destroy() error { // close socket (abort any ongoing command) m.socket.Close() // wait commands terminates m.mu.Lock() defer m.mu.Unlock() // kill process var wstatus unix.WaitStatus unix.Kill(m.pid, unix.SIGKILL) // wait for container process to exit _, err := unix.Wait4(m.pid, &wstatus, 0, nil) for err == unix.EINTR { _, err = unix.Wait4(m.pid, &wstatus, 0, nil) } return err } // exec prepares executable func (b *Builder) exec() (*os.File, error) { if b.ExecFile != nil { return b.ExecFile, nil } return OpenCurrentExec() } // OpenCurrentExec opens current executable and dup it to memfd func OpenCurrentExec() (*os.File, error) { self, err := os.Open(currentExec) if err != nil { return nil, fmt.Errorf("failed to open %v: %v", currentExec, err) } defer self.Close() execFile, err := memfd.DupToMemfd("daemon", self) if err != nil { return nil, fmt.Errorf("failed to create memfd: %v", err) } return execFile, nil } func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) { ins, outs, err := unixsocket.NewSocketPair() if err != nil { return nil, nil, err } if err = ins.SetPassCred(1); err != nil { ins.Close() outs.Close() return nil, nil, err } if err = outs.SetPassCred(1); err != nil { ins.Close() outs.Close() return nil, nil, err } return ins, outs, nil } func getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap, []syscall.SysProcIDMap) { uidMap := []syscall.SysProcIDMap{ syscall.SysProcIDMap{ ContainerID: 0, HostID: os.Geteuid(), Size: 1, }, syscall.SysProcIDMap{ ContainerID: containerUID, HostID: int(cred.Uid), Size: 1, }, } gidMap := []syscall.SysProcIDMap{ syscall.SysProcIDMap{ ContainerID: 0, HostID: os.Getegid(), Size: 1, }, syscall.SysProcIDMap{ ContainerID: containerGID, HostID: int(cred.Gid), Size: 1, }, } return uidMap, gidMap }