go-sandbox/container/environment.go
2020-02-27 23:30:32 -05:00

261 lines
6.2 KiB
Go

package container
import (
"context"
"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"
"github.com/criyle/go-sandbox/types"
"golang.org/x/sys/unix"
)
// PathEnv defines path environment variable for the container init process
const PathEnv = "PATH=/usr/local/bin:/usr/bin:/bin"
// Builder builds instance of container environment
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
}
// Environment holds single progrem containered environment
type Environment interface {
Ping() error
Open([]OpenCmd) ([]*os.File, error)
Delete(p string) error
Reset() error
Execve(context.Context, ExecveParam) <-chan types.Result
Destroy() error
}
// container manages single pre-forked container environment
type container struct {
pid int // underlying container init pid
socket *socket // host - container communication
mu sync.Mutex // lock to avoid race condition
}
// Build creates new environment with underlying container
func (b *Builder) Build() (Environment, 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.NewDefaultBuilder().
WithTmpfs("w", ""). // work dir
WithTmpfs("tmp", ""). // tmp
Build(true); err != nil {
return nil, fmt.Errorf("container: failed to build rootfs mount %v", err)
}
}
// container root directory on the host
root := b.Root
if root == "" {
if root, err = os.Getwd(); err != nil {
return nil, fmt.Errorf("container: 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("container: 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 container exec file
execFile, err := b.exec()
if err != nil {
return nil, fmt.Errorf("container: prepare exec %v", err)
}
defer execFile.Close()
// prepare host <-> container unix socket
ins, outs, err := newPassCredSocketPair()
if err != nil {
return nil, fmt.Errorf("container: failed to create socket: %v", err)
}
defer outs.Close()
outf, err := outs.File()
if err != nil {
ins.Close()
return nil, fmt.Errorf("container: failed to dup container socket fd %v", err)
}
defer outf.Close()
files = append(files, uintptr(outf.Fd()))
// prepare container running credential
if b.CredGenerator != nil {
cred = b.CredGenerator.Get()
uidMap, gidMap = getIDMapping(&cred)
}
r := &forkexec.Runner{
Args: []string{os.Args[0], initArg},
Env: []string{PathEnv},
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("container: failed to start container %v", err)
}
c := &container{
pid: pid,
socket: newSocket(ins),
}
// set configuration and check if container creation successful
if err = c.conf(&containerConfig{
Cred: b.CredGenerator != nil,
}); err != nil {
c.Destroy()
return nil, err
}
return c, nil
}
// Destroy kill the container process (with its children)
// if stderr enabled, collect the output as error
func (c *container) Destroy() error {
// close socket (abort any ongoing command)
c.socket.Close()
// wait commands terminates
c.mu.Lock()
defer c.mu.Unlock()
// kill process
var wstatus unix.WaitStatus
unix.Kill(c.pid, unix.SIGKILL)
// wait for container process to exit
_, err := unix.Wait4(c.pid, &wstatus, 0, nil)
for err == unix.EINTR {
_, err = unix.Wait4(c.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 and seal 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("init", 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
}