aboutsummaryrefslogtreecommitdiffhomepage
path: root/container/container.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-07-03 02:59:43 +0900
committerOphestra <cat@gensokyo.uk>2025-07-03 02:59:43 +0900
commit1b5ecd9eaf3289d164d8ed1bce6013e0e4ef8e86 (patch)
tree047fe5fabdfb37d5c0088f7f572cebc8b13853bb /container/container.go
parent82561d62b66f17c05604e87f18187bb3a91f00d2 (diff)
container: move out of toplevel
This allows slightly easier use of the vanity url. This also provides some disambiguation between low level containers and hakurei app containers. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'container/container.go')
-rw-r--r--container/container.go229
1 files changed, 229 insertions, 0 deletions
diff --git a/container/container.go b/container/container.go
new file mode 100644
index 00000000..62aa16d7
--- /dev/null
+++ b/container/container.go
@@ -0,0 +1,229 @@
+// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
+package container
+
+import (
+ "context"
+ "encoding/gob"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path"
+ "strconv"
+ . "syscall"
+ "time"
+
+ "git.gensokyo.uk/security/hakurei/container/seccomp"
+)
+
+type (
+ // Container represents a container environment being prepared or run.
+ // None of [Container] methods are safe for concurrent use.
+ Container struct {
+ // Name of initial process in the container.
+ name string
+ // Cgroup fd, nil to disable.
+ Cgroup *int
+ // ExtraFiles passed through to initial process in the container,
+ // with behaviour identical to its [exec.Cmd] counterpart.
+ ExtraFiles []*os.File
+
+ // Custom [exec.Cmd] initialisation function.
+ CommandContext func(ctx context.Context) (cmd *exec.Cmd)
+
+ // param encoder for shim and init
+ setup *gob.Encoder
+ // cancels cmd
+ cancel context.CancelFunc
+
+ Stdin io.Reader
+ Stdout io.Writer
+ Stderr io.Writer
+
+ Cancel func(cmd *exec.Cmd) error
+ WaitDelay time.Duration
+
+ cmd *exec.Cmd
+ ctx context.Context
+ Params
+ }
+
+ // Params holds container configuration and is safe to serialise.
+ Params struct {
+ // Working directory in the container.
+ Dir string
+ // Initial process environment.
+ Env []string
+ // Absolute path of initial process in the container. Overrides name.
+ Path string
+ // Initial process argv.
+ Args []string
+
+ // Mapped Uid in user namespace.
+ Uid int
+ // Mapped Gid in user namespace.
+ Gid int
+ // Hostname value in UTS namespace.
+ Hostname string
+ // Sequential container setup ops.
+ *Ops
+ // Seccomp system call filter rules.
+ SeccompRules []seccomp.NativeRule
+ // Extra seccomp flags.
+ SeccompFlags seccomp.ExportFlag
+ // Seccomp presets. Has no effect unless SeccompRules is zero-length.
+ SeccompPresets seccomp.FilterPreset
+ // Do not load seccomp program.
+ SeccompDisable bool
+ // Permission bits of newly created parent directories.
+ // The zero value is interpreted as 0755.
+ ParentPerm os.FileMode
+ // Do not syscall.Setsid.
+ RetainSession bool
+ // Do not [syscall.CLONE_NEWNET].
+ HostNet bool
+ // Retain CAP_SYS_ADMIN.
+ Privileged bool
+ }
+)
+
+func (p *Container) Start() error {
+ if p.cmd != nil {
+ return errors.New("sandbox: already started")
+ }
+ if p.Ops == nil || len(*p.Ops) == 0 {
+ return errors.New("sandbox: starting an empty container")
+ }
+
+ ctx, cancel := context.WithCancel(p.ctx)
+ p.cancel = cancel
+
+ var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
+ if !p.HostNet {
+ cloneFlags |= CLONE_NEWNET
+ }
+
+ // map to overflow id to work around ownership checks
+ if p.Uid < 1 {
+ p.Uid = OverflowUid()
+ }
+ if p.Gid < 1 {
+ p.Gid = OverflowGid()
+ }
+
+ if !p.RetainSession {
+ p.SeccompPresets |= seccomp.PresetDenyTTY
+ }
+
+ if p.CommandContext != nil {
+ p.cmd = p.CommandContext(ctx)
+ } else {
+ p.cmd = exec.CommandContext(ctx, MustExecutable())
+ p.cmd.Args = []string{"init"}
+ }
+
+ p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
+ p.cmd.WaitDelay = p.WaitDelay
+ if p.Cancel != nil {
+ p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
+ } else {
+ p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
+ }
+ p.cmd.Dir = "/"
+ p.cmd.SysProcAttr = &SysProcAttr{
+ Setsid: !p.RetainSession,
+ Pdeathsig: SIGKILL,
+ Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS,
+
+ // remain privileged for setup
+ AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
+
+ UseCgroupFD: p.Cgroup != nil,
+ }
+ if p.cmd.SysProcAttr.UseCgroupFD {
+ p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
+ }
+
+ // place setup pipe before user supplied extra files, this is later restored by init
+ if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
+ return wrapErrSuffix(err,
+ "cannot create shim setup pipe:")
+ } else {
+ p.setup = e
+ p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
+ }
+ p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
+
+ msg.Verbose("starting container init")
+ if err := p.cmd.Start(); err != nil {
+ return msg.WrapErr(err, err.Error())
+ }
+ return nil
+}
+
+func (p *Container) Serve() error {
+ if p.setup == nil {
+ panic("invalid serve")
+ }
+
+ setup := p.setup
+ p.setup = nil
+
+ if p.Path != "" && !path.IsAbs(p.Path) {
+ p.cancel()
+ return msg.WrapErr(EINVAL,
+ fmt.Sprintf("invalid executable path %q", p.Path))
+ }
+
+ if p.Path == "" {
+ if p.name == "" {
+ p.Path = os.Getenv("SHELL")
+ if !path.IsAbs(p.Path) {
+ p.cancel()
+ return msg.WrapErr(EBADE,
+ "no command specified and $SHELL is invalid")
+ }
+ p.name = path.Base(p.Path)
+ } else if path.IsAbs(p.name) {
+ p.Path = p.name
+ } else if v, err := exec.LookPath(p.name); err != nil {
+ p.cancel()
+ return msg.WrapErr(err, err.Error())
+ } else {
+ p.Path = v
+ }
+ }
+
+ if p.SeccompRules == nil {
+ // do not transmit nil
+ p.SeccompRules = make([]seccomp.NativeRule, 0)
+ }
+
+ err := setup.Encode(
+ &initParams{
+ p.Params,
+ Getuid(),
+ Getgid(),
+ len(p.ExtraFiles),
+ msg.IsVerbose(),
+ },
+ )
+ if err != nil {
+ p.cancel()
+ }
+ return err
+}
+
+func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
+
+func (p *Container) String() string {
+ return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
+ p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
+}
+
+func New(ctx context.Context, name string, args ...string) *Container {
+ return &Container{name: name, ctx: ctx,
+ Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
+ }
+}