go-sandbox/container/environment_linux.go

415 lines
9.2 KiB
Go

package container
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"sync"
"syscall"
"github.com/criyle/go-sandbox/pkg/forkexec"
"github.com/criyle/go-sandbox/pkg/mount"
"github.com/criyle/go-sandbox/pkg/unixsocket"
"github.com/criyle/go-sandbox/runner"
"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
// TmpRoot defines the tmp dir pattern if not nil. Temp directory will be created as container root dir
TmpRoot string
// Mounts defines container mount points, empty uses default mounts
Mounts []mount.Mount
// SymbolicLinks defines symlinks to be created after mount file system
SymbolicLinks []SymbolicLink
// MaskPaths defines paths to be masked to avoid reading information from
// outside of the container
MaskPaths []string
// WorkDir defines container default work directory (default: /w)
WorkDir string
// Stderr defines whether to dup container stderr to stderr for debug
Stderr io.Writer
// ExecFile defines executable that called Init, otherwise defer current
// executable (/proc/self/exe)
ExecFile string
// CredGenerator defines a credential generator used to create new container
CredGenerator CredGenerator
// Clone flags defines unshare clone flag to create container
CloneFlags uintptr
// HostName set container hostname (default: go-sandbox)
HostName string
// DomainName set container domainname (default: go-sandbox)
DomainName string
// InitCommand defines command that runs after the initialization of the container
// to do additional setups (for example, loopback network)
InitCommand []string
// ContainerUID & ContainerGID set the container uid / gid mapping
ContainerUID int
ContainerGID int
// UnshareCgroupBeforeExec calls unshare cgroup before execution
UnshareCgroupBeforeExec bool
}
// SymbolicLink defines symlinks to be created after mount
type SymbolicLink struct {
LinkPath string
Target string
}
// 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 containerized environment
type Environment interface {
Ping() error
Open([]OpenCmd) ([]*os.File, error)
Delete(p string) error
Reset() error
Execve(context.Context, ExecveParam) runner.Result
Destroy() error
}
// container manages single pre-forked container environment
type container struct {
process *os.Process // underlying container init pid
socket *socket // host - container communication
mu sync.Mutex // lock to avoid race condition
done chan struct{}
err error
doneOnce sync.Once
recvCh chan recvReply
sendCh chan sendCmd
}
type recvReply struct {
Reply reply
Msg unixsocket.Msg
}
type sendCmd struct {
Cmd cmd
Msg unixsocket.Msg
}
// Build creates new environment with underlying container
func (b *Builder) Build() (Environment, error) {
c, err := b.startContainer()
if err != nil {
return nil, err
}
// avoid non cinit enabled executable running as container init process
if err = c.Ping(); err != nil {
c.Destroy()
return nil, fmt.Errorf("container: init not responding to ping: %w", err)
}
// container mount points
mounts := b.Mounts
if len(mounts) == 0 {
mounts = mount.NewDefaultBuilder().
WithTmpfs("w", ""). // work dir
WithTmpfs("tmp", ""). // tmp
FilterNotExist().Mounts
}
// container symbolic links
links := b.SymbolicLinks
if len(links) == 0 {
links = defaultSymLinks
}
maskPaths := b.MaskPaths
if len(maskPaths) == 0 {
maskPaths = defaultMaskPaths
}
// container root directory on the host
root := b.Root
if b.TmpRoot != "" {
if root, err = os.MkdirTemp(b.Root, b.TmpRoot); err != nil {
return nil, fmt.Errorf("container: failed to make tmp container root at %s: %w", b.Root, err)
}
defer os.Remove(root)
}
if root == "" {
if root, err = os.Getwd(); err != nil {
return nil, fmt.Errorf("container: failed to get work directory: %w", err)
}
}
workDir := containerWD
if b.WorkDir != "" {
workDir = b.WorkDir
}
hostName := containerName
if b.HostName != "" {
hostName = b.HostName
}
domainName := containerName
if b.DomainName != "" {
domainName = b.DomainName
}
// set configuration and check if container creation successful
if err = c.conf(&containerConfig{
WorkDir: workDir,
HostName: hostName,
DomainName: domainName,
ContainerRoot: root,
Mounts: mounts,
SymbolicLinks: links,
MaskPaths: maskPaths,
InitCommand: b.InitCommand,
Cred: b.CredGenerator != nil,
ContainerUID: b.ContainerUID,
ContainerGID: b.ContainerGID,
UnshareCgroup: b.UnshareCgroupBeforeExec,
}); err != nil {
c.Destroy()
return nil, err
}
return c, nil
}
func (b *Builder) startContainer() (*container, error) {
var (
err error
cred syscall.Credential
uidMap, gidMap []syscall.SysProcIDMap
)
// prepare host <-> container unix socket
ins, outs, err := newPassCredSocketPair()
if err != nil {
return nil, fmt.Errorf("container: failed to create socket: %w", err)
}
defer outs.Close()
outf, err := outs.File()
if err != nil {
ins.Close()
return nil, fmt.Errorf("container: failed to dup container socket fd: %w", err)
}
defer outf.Close()
// prepare container running credential
if b.CredGenerator != nil {
cred = b.CredGenerator.Get()
uidMap, gidMap = b.getIDMapping(&cred)
} else {
uidMap = []syscall.SysProcIDMap{{HostID: os.Geteuid(), Size: 1}}
gidMap = []syscall.SysProcIDMap{{HostID: os.Getegid(), Size: 1}}
}
var cloneFlag uintptr
if b.CloneFlags == 0 {
cloneFlag = forkexec.UnshareFlags
} else {
cloneFlag = b.CloneFlags & forkexec.UnshareFlags
}
exe := "/proc/self/exe"
if b.ExecFile != "" {
exe = b.ExecFile
}
args := []string{exe, initArg}
r := exec.Cmd{
Path: exe,
Args: args,
Env: []string{PathEnv},
Stderr: b.Stderr,
ExtraFiles: []*os.File{outf},
SysProcAttr: &syscall.SysProcAttr{
Cloneflags: cloneFlag,
UidMappings: uidMap,
GidMappings: gidMap,
AmbientCaps: []uintptr{
unix.CAP_SYS_ADMIN,
unix.CAP_SYS_RESOURCE,
},
Pdeathsig: syscall.SIGKILL,
},
}
if err = r.Start(); err != nil {
ins.Close()
return nil, fmt.Errorf("container: failed to start container: %w", err)
}
c := &container{
process: r.Process,
socket: newSocket(ins),
recvCh: make(chan recvReply, 1),
sendCh: make(chan sendCmd, 1),
done: make(chan struct{}),
}
go c.sendLoop()
go c.recvLoop()
return c, nil
}
func (c *container) sendLoop() {
for {
select {
case <-c.done:
return
case cmd, ok := <-c.sendCh:
if !ok {
return
}
if err := c.socket.SendMsg(cmd.Cmd, cmd.Msg); err != nil {
c.socketError(err)
return
}
}
}
}
func (c *container) recvLoop() {
for {
var reply reply
msg, err := c.socket.RecvMsg(&reply)
if err != nil {
c.socketError(err)
return
}
c.recvCh <- recvReply{
Reply: reply,
Msg: msg,
}
}
}
func (c *container) socketError(err error) {
c.doneOnce.Do(func() {
c.err = err
close(c.done)
})
}
// 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
c.process.Kill()
_, err := c.process.Wait()
return err
}
// newPassCredSocketPair creates socket pair and let the first socket to receive credential information
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
}
return ins, outs, nil
}
func (b *Builder) getIDMapping(cred *syscall.Credential) ([]syscall.SysProcIDMap, []syscall.SysProcIDMap) {
cUID := b.ContainerUID
if cUID == 0 {
cUID = containerUID
}
cGID := b.ContainerGID
if cGID == 0 {
cGID = containerGID
}
uidMap := []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Geteuid(),
Size: 1,
},
{
ContainerID: cUID,
HostID: int(cred.Uid),
Size: 1,
},
}
gidMap := []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getegid(),
Size: 1,
},
{
ContainerID: cGID,
HostID: int(cred.Gid),
Size: 1,
},
}
return uidMap, gidMap
}
func (c *container) recvAckReply(name string) error {
reply, _, err := c.recvReply()
if err != nil {
return fmt.Errorf("%s: recv ack: %w", name, err)
}
if reply.Error != nil {
return fmt.Errorf("%s: container error: %v", name, reply.Error)
}
return nil
}
func (c *container) recvReply() (reply, unixsocket.Msg, error) {
select {
case <-c.done:
return reply{}, unixsocket.Msg{}, c.err
case recv := <-c.recvCh:
return recv.Reply, recv.Msg, nil
}
}
func (c *container) sendCmd(cmd cmd, msg unixsocket.Msg) error {
select {
case <-c.done:
return c.err
case c.sendCh <- sendCmd{Cmd: cmd, Msg: msg}:
return nil
}
}