aboutsummaryrefslogtreecommitdiffhomepage
path: root/container_test.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-07-02 21:23:55 +0900
committerOphestra <cat@gensokyo.uk>2025-07-02 21:23:55 +0900
commita1d98823f8d17b49b432508d071b62ccd426f1ce (patch)
treecacbe2d42f9ff3fd49b455e849770840e34d076a /container_test.go
parent255b77d91dc40736b7bbd7d96b22500076b098e5 (diff)
hakurei: move container toplevel
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'container_test.go')
-rw-r--r--container_test.go281
1 files changed, 281 insertions, 0 deletions
diff --git a/container_test.go b/container_test.go
new file mode 100644
index 00000000..aa1ed0af
--- /dev/null
+++ b/container_test.go
@@ -0,0 +1,281 @@
+package hakurei_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/gob"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "git.gensokyo.uk/security/hakurei"
+ "git.gensokyo.uk/security/hakurei/hst"
+ "git.gensokyo.uk/security/hakurei/internal"
+ "git.gensokyo.uk/security/hakurei/internal/hlog"
+ "git.gensokyo.uk/security/hakurei/ldd"
+ "git.gensokyo.uk/security/hakurei/sandbox/seccomp"
+ "git.gensokyo.uk/security/hakurei/sandbox/vfs"
+)
+
+const (
+ ignore = "\x00"
+ ignoreV = -1
+)
+
+func TestContainer(t *testing.T) {
+ {
+ oldVerbose := hlog.Load()
+ oldOutput := hakurei.GetOutput()
+ internal.InstallOutput(true)
+ t.Cleanup(func() { hlog.Store(oldVerbose) })
+ t.Cleanup(func() { hakurei.SetOutput(oldOutput) })
+ }
+
+ testCases := []struct {
+ name string
+ filter bool
+ session bool
+ net bool
+ ops *hakurei.Ops
+ mnt []*vfs.MountInfoEntry
+ host string
+ rules []seccomp.NativeRule
+ flags seccomp.ExportFlag
+ presets seccomp.FilterPreset
+ }{
+ {"minimal", true, false, false,
+ new(hakurei.Ops), nil, "test-minimal",
+ nil, 0, seccomp.PresetStrict},
+ {"allow", true, true, true,
+ new(hakurei.Ops), nil, "test-minimal",
+ nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
+ {"no filter", false, true, true,
+ new(hakurei.Ops), nil, "test-no-filter",
+ nil, 0, seccomp.PresetExt},
+ {"custom rules", true, true, true,
+ new(hakurei.Ops), nil, "test-no-filter",
+ []seccomp.NativeRule{
+ {seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
+ }, 0, seccomp.PresetExt},
+ {"tmpfs", true, false, false,
+ new(hakurei.Ops).
+ Tmpfs(hst.Tmp, 0, 0755),
+ []*vfs.MountInfoEntry{
+ e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
+ }, "test-tmpfs",
+ nil, 0, seccomp.PresetStrict},
+ {"dev", true, true /* go test output is not a tty */, false,
+ new(hakurei.Ops).
+ Dev("/dev").
+ Mqueue("/dev/mqueue"),
+ []*vfs.MountInfoEntry{
+ e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
+ e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
+ e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
+ e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
+ e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
+ e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
+ e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
+ e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
+ e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
+ }, "",
+ nil, 0, seccomp.PresetStrict},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
+ defer cancel()
+
+ container := hakurei.New(ctx, "/usr/bin/sandbox.test", "-test.v",
+ "-test.run=TestHelperCheckContainer", "--", "check", tc.host)
+ container.Uid = 1000
+ container.Gid = 100
+ container.Hostname = tc.host
+ container.CommandContext = commandContext
+ container.Stdout, container.Stderr = os.Stdout, os.Stderr
+ container.Ops = tc.ops
+ container.SeccompRules = tc.rules
+ container.SeccompFlags = tc.flags | seccomp.AllowMultiarch
+ container.SeccompPresets = tc.presets
+ container.SeccompDisable = !tc.filter
+ container.RetainSession = tc.session
+ container.HostNet = tc.net
+ if container.Args[5] == "" {
+ if name, err := os.Hostname(); err != nil {
+ t.Fatalf("cannot get hostname: %v", err)
+ } else {
+ container.Args[5] = name
+ }
+ }
+
+ container.
+ Tmpfs("/tmp", 0, 0755).
+ Bind(os.Args[0], os.Args[0], 0).
+ Mkdir("/usr/bin", 0755).
+ Link(os.Args[0], "/usr/bin/sandbox.test").
+ Place("/etc/hostname", []byte(container.Args[5]))
+ // in case test has cgo enabled
+ var libPaths []string
+ if entries, err := ldd.ExecFilter(ctx,
+ commandContext,
+ func(v []byte) []byte {
+ return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
+ }, os.Args[0]); err != nil {
+ log.Fatalf("ldd: %v", err)
+ } else {
+ libPaths = ldd.Path(entries)
+ }
+ for _, name := range libPaths {
+ container.Bind(name, name, 0)
+ }
+ // needs /proc to check mountinfo
+ container.Proc("/proc")
+
+ mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
+ mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
+ mnt = append(mnt, tc.mnt...)
+ mnt = append(mnt,
+ e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
+ e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
+ e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
+ )
+ for _, name := range libPaths {
+ mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
+ }
+ mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
+ want := new(bytes.Buffer)
+ if err := gob.NewEncoder(want).Encode(mnt); err != nil {
+ t.Fatalf("cannot serialise expected mount points: %v", err)
+ }
+ container.Stdin = want
+
+ if err := container.Start(); err != nil {
+ hlog.PrintBaseError(err, "start:")
+ t.Fatalf("cannot start container: %v", err)
+ } else if err = container.Serve(); err != nil {
+ hlog.PrintBaseError(err, "serve:")
+ t.Errorf("cannot serve setup params: %v", err)
+ }
+ if err := container.Wait(); err != nil {
+ hlog.PrintBaseError(err, "wait:")
+ t.Fatalf("wait: %v", err)
+ }
+ })
+ }
+}
+
+func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
+ return &vfs.MountInfoEntry{
+ ID: ignoreV,
+ Parent: ignoreV,
+ Devno: vfs.DevT{ignoreV, ignoreV},
+ Root: root,
+ Target: target,
+ VfsOptstr: vfsOptstr,
+ OptFields: []string{ignore},
+ FsType: fsType,
+ Source: source,
+ FsOptstr: fsOptstr,
+ }
+}
+
+func TestContainerString(t *testing.T) {
+ container := hakurei.New(t.Context(), "ldd", "/usr/bin/env")
+ container.SeccompFlags |= seccomp.AllowMultiarch
+ container.SeccompRules = seccomp.Preset(
+ seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
+ container.SeccompFlags)
+ container.SeccompPresets = seccomp.PresetStrict
+ want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
+ if got := container.String(); got != want {
+ t.Errorf("String: %s, want %s", got, want)
+ }
+}
+
+func TestHelperInit(t *testing.T) {
+ if len(os.Args) != 5 || os.Args[4] != "init" {
+ return
+ }
+ hakurei.SetOutput(hlog.Output{})
+ hakurei.Init(hlog.Prepare, internal.InstallOutput)
+}
+
+func TestHelperCheckContainer(t *testing.T) {
+ if len(os.Args) != 6 || os.Args[4] != "check" {
+ return
+ }
+
+ t.Run("user", func(t *testing.T) {
+ if uid := syscall.Getuid(); uid != 1000 {
+ t.Errorf("Getuid: %d, want 1000", uid)
+ }
+ if gid := syscall.Getgid(); gid != 100 {
+ t.Errorf("Getgid: %d, want 100", gid)
+ }
+ })
+ t.Run("hostname", func(t *testing.T) {
+ if name, err := os.Hostname(); err != nil {
+ t.Fatalf("cannot get hostname: %v", err)
+ } else if name != os.Args[5] {
+ t.Errorf("Hostname: %q, want %q", name, os.Args[5])
+ }
+
+ if p, err := os.ReadFile("/etc/hostname"); err != nil {
+ t.Fatalf("%v", err)
+ } else if string(p) != os.Args[5] {
+ t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
+ }
+ })
+ t.Run("mount", func(t *testing.T) {
+ var mnt []*vfs.MountInfoEntry
+ if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
+ t.Fatalf("cannot receive expected mount points: %v", err)
+ }
+
+ var d *vfs.MountInfoDecoder
+ if f, err := os.Open("/proc/self/mountinfo"); err != nil {
+ t.Fatalf("cannot open mountinfo: %v", err)
+ } else {
+ d = vfs.NewMountInfoDecoder(f)
+ }
+
+ i := 0
+ for cur := range d.Entries() {
+ if i == len(mnt) {
+ t.Errorf("got more than %d entries", len(mnt))
+ break
+ }
+
+ // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
+ cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
+ cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
+ mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
+ mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
+
+ if !cur.EqualWithIgnore(mnt[i], "\x00") {
+ t.Errorf("[FAIL] %s", cur)
+ } else {
+ t.Logf("[ OK ] %s", cur)
+ }
+
+ i++
+ }
+ if err := d.Err(); err != nil {
+ t.Errorf("cannot parse mountinfo: %v", err)
+ }
+
+ if i != len(mnt) {
+ t.Errorf("got %d entries, want %d", i, len(mnt))
+ }
+ })
+}
+
+func commandContext(ctx context.Context) *exec.Cmd {
+ return exec.CommandContext(ctx, os.Args[0], "-test.v",
+ "-test.run=TestHelperInit", "--", "init")
+}