diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-07-03 02:59:43 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-07-03 02:59:43 +0900 |
| commit | 1b5ecd9eaf3289d164d8ed1bce6013e0e4ef8e86 (patch) | |
| tree | 047fe5fabdfb37d5c0088f7f572cebc8b13853bb /container/container_test.go | |
| parent | 82561d62b66f17c05604e87f18187bb3a91f00d2 (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_test.go')
| -rw-r--r-- | container/container_test.go | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/container/container_test.go b/container/container_test.go new file mode 100644 index 00000000..95f53619 --- /dev/null +++ b/container/container_test.go @@ -0,0 +1,281 @@ +package container_test + +import ( + "bytes" + "context" + "encoding/gob" + "log" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" + "git.gensokyo.uk/security/hakurei/container/vfs" + "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" +) + +const ( + ignore = "\x00" + ignoreV = -1 +) + +func TestContainer(t *testing.T) { + { + oldVerbose := hlog.Load() + oldOutput := container.GetOutput() + internal.InstallOutput(true) + t.Cleanup(func() { hlog.Store(oldVerbose) }) + t.Cleanup(func() { container.SetOutput(oldOutput) }) + } + + testCases := []struct { + name string + filter bool + session bool + net bool + ops *container.Ops + mnt []*vfs.MountInfoEntry + host string + rules []seccomp.NativeRule + flags seccomp.ExportFlag + presets seccomp.FilterPreset + }{ + {"minimal", true, false, false, + new(container.Ops), nil, "test-minimal", + nil, 0, seccomp.PresetStrict}, + {"allow", true, true, true, + new(container.Ops), nil, "test-minimal", + nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, + {"no filter", false, true, true, + new(container.Ops), nil, "test-no-filter", + nil, 0, seccomp.PresetExt}, + {"custom rules", true, true, true, + new(container.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(container.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(container.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() + + c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v", + "-test.run=TestHelperCheckContainer", "--", "check", tc.host) + c.Uid = 1000 + c.Gid = 100 + c.Hostname = tc.host + c.CommandContext = commandContext + c.Stdout, c.Stderr = os.Stdout, os.Stderr + c.Ops = tc.ops + c.SeccompRules = tc.rules + c.SeccompFlags = tc.flags | seccomp.AllowMultiarch + c.SeccompPresets = tc.presets + c.SeccompDisable = !tc.filter + c.RetainSession = tc.session + c.HostNet = tc.net + if c.Args[5] == "" { + if name, err := os.Hostname(); err != nil { + t.Fatalf("cannot get hostname: %v", err) + } else { + c.Args[5] = name + } + } + + c. + 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(c.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 { + c.Bind(name, name, 0) + } + // needs /proc to check mountinfo + c.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) + } + c.Stdin = want + + if err := c.Start(); err != nil { + hlog.PrintBaseError(err, "start:") + t.Fatalf("cannot start container: %v", err) + } else if err = c.Serve(); err != nil { + hlog.PrintBaseError(err, "serve:") + t.Errorf("cannot serve setup params: %v", err) + } + if err := c.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) { + c := container.New(t.Context(), "ldd", "/usr/bin/env") + c.SeccompFlags |= seccomp.AllowMultiarch + c.SeccompRules = seccomp.Preset( + seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, + c.SeccompFlags) + c.SeccompPresets = seccomp.PresetStrict + want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` + if got := c.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 + } + container.SetOutput(hlog.Output{}) + container.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") +} |
