aboutsummaryrefslogtreecommitdiffhomepage
path: root/helper/bwrap
diff options
context:
space:
mode:
authorOphestra Umiker <cat@ophivana.moe>2024-10-07 14:40:35 +0900
committerOphestra Umiker <cat@ophivana.moe>2024-10-07 14:40:35 +0900
commit6a2802cf309fb9758756c56ef05c9354d9cb9003 (patch)
treed1efa27407aa3a2e1337fa4ffafa3df9fd677ec4 /helper/bwrap
parent0fb9e401912f89d25ff96b0dd36aabf43b4fab8f (diff)
helper: move bwrap into helper
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
Diffstat (limited to 'helper/bwrap')
-rw-r--r--helper/bwrap/config.bool.go64
-rw-r--r--helper/bwrap/config.go265
-rw-r--r--helper/bwrap/config.int.go22
-rw-r--r--helper/bwrap/config.interface.go34
-rw-r--r--helper/bwrap/config.pair.go54
-rw-r--r--helper/bwrap/config.string.go35
-rw-r--r--helper/bwrap/config_test.go88
7 files changed, 562 insertions, 0 deletions
diff --git a/helper/bwrap/config.bool.go b/helper/bwrap/config.bool.go
new file mode 100644
index 00000000..81e6ab7b
--- /dev/null
+++ b/helper/bwrap/config.bool.go
@@ -0,0 +1,64 @@
+package bwrap
+
+const (
+ UnshareAll = iota
+ UnshareUser
+ UnshareIPC
+ UnsharePID
+ UnshareNet
+ UnshareUTS
+ UnshareCGroup
+ ShareNet
+
+ UserNS
+ Clearenv
+
+ NewSession
+ DieWithParent
+ AsInit
+
+ boolC
+)
+
+var boolArgs = func() (b [boolC][]string) {
+ b[UnshareAll] = []string{"--unshare-all"}
+ b[UnshareUser] = []string{"--unshare-user"}
+ b[UnshareIPC] = []string{"--unshare-ipc"}
+ b[UnsharePID] = []string{"--unshare-pid"}
+ b[UnshareNet] = []string{"--unshare-net"}
+ b[UnshareUTS] = []string{"--unshare-uts"}
+ b[UnshareCGroup] = []string{"--unshare-cgroup"}
+ b[ShareNet] = []string{"--share-net"}
+
+ b[UserNS] = []string{"--disable-userns", "--assert-userns-disabled"}
+ b[Clearenv] = []string{"--clearenv"}
+
+ b[NewSession] = []string{"--new-session"}
+ b[DieWithParent] = []string{"--die-with-parent"}
+ b[AsInit] = []string{"--as-pid-1"}
+
+ return
+}()
+
+func (c *Config) boolArgs() (b [boolC]bool) {
+ if c.Unshare == nil {
+ b[UnshareAll] = true
+ b[ShareNet] = c.Net
+ } else {
+ b[UnshareUser] = c.Unshare.User
+ b[UnshareIPC] = c.Unshare.IPC
+ b[UnsharePID] = c.Unshare.PID
+ b[UnshareNet] = c.Unshare.Net
+ b[UnshareUTS] = c.Unshare.UTS
+ b[UnshareCGroup] = c.Unshare.CGroup
+ }
+
+ b[UserNS] = !c.UserNS
+ b[Clearenv] = c.Clearenv
+
+ b[NewSession] = c.NewSession
+ b[DieWithParent] = c.DieWithParent
+ b[AsInit] = c.AsInit
+
+ return
+}
diff --git a/helper/bwrap/config.go b/helper/bwrap/config.go
new file mode 100644
index 00000000..c20a33b0
--- /dev/null
+++ b/helper/bwrap/config.go
@@ -0,0 +1,265 @@
+package bwrap
+
+import (
+ "os"
+ "strconv"
+)
+
+func (c *Config) Args() (args []string) {
+ b := c.boolArgs()
+ n := c.intArgs()
+ s := c.stringArgs()
+ p := c.pairArgs()
+ g := c.interfaceArgs()
+
+ argc := 0
+ for i, arg := range b {
+ if arg {
+ argc += len(boolArgs[i])
+ }
+ }
+ for _, arg := range n {
+ if arg != nil {
+ argc += 2
+ }
+ }
+ for _, arg := range s {
+ argc += len(arg) * 2
+ }
+ for _, arg := range p {
+ argc += len(arg) * 3
+ }
+
+ args = make([]string, 0, argc)
+ for i, arg := range b {
+ if arg {
+ args = append(args, boolArgs[i]...)
+ }
+ }
+ for i, arg := range n {
+ if arg != nil {
+ args = append(args, intArgs[i], strconv.Itoa(*arg))
+ }
+ }
+ for i, arg := range s {
+ for _, v := range arg {
+ args = append(args, stringArgs[i], v)
+ }
+ }
+ for i, arg := range p {
+ for _, v := range arg {
+ args = append(args, pairArgs[i], v[0], v[1])
+ }
+ }
+ for i, arg := range g {
+ for _, v := range arg {
+ args = append(args, v.Value(interfaceArgs[i])...)
+ }
+ }
+
+ return
+}
+
+type Config struct {
+ // unshare every namespace we support by default if nil
+ // (--unshare-all)
+ Unshare *UnshareConfig `json:"unshare,omitempty"`
+ // retain the network namespace (can only combine with nil Unshare)
+ // (--share-net)
+ Net bool `json:"net"`
+
+ // disable further use of user namespaces inside sandbox and fail unless
+ // further use of user namespace inside sandbox is disabled if false
+ // (--disable-userns) (--assert-userns-disabled)
+ UserNS bool `json:"userns"`
+
+ // custom uid in the sandbox, requires new user namespace
+ // (--uid UID)
+ UID *int `json:"uid,omitempty"`
+ // custom gid in the sandbox, requires new user namespace
+ // (--gid GID)
+ GID *int `json:"gid,omitempty"`
+ // custom hostname in the sandbox, requires new uts namespace
+ // (--hostname NAME)
+ Hostname string `json:"hostname,omitempty"`
+
+ // change directory
+ // (--chdir DIR)
+ Chdir string `json:"chdir,omitempty"`
+ // unset all environment variables
+ // (--clearenv)
+ Clearenv bool `json:"clearenv"`
+ // set environment variable
+ // (--setenv VAR VALUE)
+ SetEnv map[string]string `json:"setenv,omitempty"`
+ // unset environment variables
+ // (--unsetenv VAR)
+ UnsetEnv []string `json:"unsetenv,omitempty"`
+
+ // take a lock on file while sandbox is running
+ // (--lock-file DEST)
+ LockFile []string `json:"lock_file,omitempty"`
+
+ // bind mount host path on sandbox
+ // (--bind SRC DEST)
+ Bind [][2]string `json:"bind,omitempty"`
+ // equal to Bind but ignores non-existent host path
+ // (--bind-try SRC DEST)
+ BindTry [][2]string `json:"bind_try,omitempty"`
+
+ // bind mount host path on sandbox, allowing device access
+ // (--dev-bind SRC DEST)
+ DevBind [][2]string `json:"dev_bind,omitempty"`
+ // equal to DevBind but ignores non-existent host path
+ // (--dev-bind-try SRC DEST)
+ DevBindTry [][2]string `json:"dev_bind_try,omitempty"`
+
+ // bind mount host path readonly on sandbox
+ // (--ro-bind SRC DEST)
+ ROBind [][2]string `json:"ro_bind,omitempty"`
+ // equal to ROBind but ignores non-existent host path
+ // (--ro-bind-try SRC DEST)
+ ROBindTry [][2]string `json:"ro_bind_try,omitempty"`
+
+ // remount path as readonly; does not recursively remount
+ // (--remount-ro DEST)
+ RemountRO []string `json:"remount_ro,omitempty"`
+
+ // mount new procfs in sandbox
+ // (--proc DEST)
+ Procfs []PermConfig[string] `json:"proc,omitempty"`
+ // mount new dev in sandbox
+ // (--dev DEST)
+ DevTmpfs []PermConfig[string] `json:"dev,omitempty"`
+ // mount new tmpfs in sandbox
+ // (--tmpfs DEST)
+ Tmpfs []PermConfig[TmpfsConfig] `json:"tmpfs,omitempty"`
+ // mount new mqueue in sandbox
+ // (--mqueue DEST)
+ Mqueue []PermConfig[string] `json:"mqueue,omitempty"`
+ // create dir in sandbox
+ // (--dir DEST)
+ Dir []PermConfig[string] `json:"dir,omitempty"`
+ // create symlink within sandbox
+ // (--symlink SRC DEST)
+ Symlink []PermConfig[[2]string] `json:"symlink,omitempty"`
+
+ // change permissions (must already exist)
+ // (--chmod OCTAL PATH)
+ Chmod map[string]os.FileMode `json:"chmod,omitempty"`
+
+ // create a new terminal session
+ // (--new-session)
+ NewSession bool `json:"new_session"`
+ // kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.
+ // (--die-with-parent)
+ DieWithParent bool `json:"die_with_parent"`
+ // do not install a reaper process with PID=1
+ // (--as-pid-1)
+ AsInit bool `json:"as_init"`
+
+ /* unmapped options include:
+ --unshare-user-try Create new user namespace if possible else continue by skipping it
+ --unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
+ --userns FD Use this user namespace (cannot combine with --unshare-user)
+ --userns2 FD After setup switch to this user namespace, only useful with --userns
+ --pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
+ --sync-fd FD Keep this fd open while sandbox is running
+ --exec-label LABEL Exec label for the sandbox
+ --file-label LABEL File label for temporary sandbox content
+ --file FD DEST Copy from FD to destination DEST
+ --bind-data FD DEST Copy from FD to file which is bind-mounted on DEST
+ --ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST
+ --seccomp FD Load and use seccomp rules from FD (not repeatable)
+ --add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
+ --block-fd FD Block on FD until some data to read is available
+ --userns-block-fd FD Block on FD until the user namespace is ready
+ --info-fd FD Write information about the running container to FD
+ --json-status-fd FD Write container status to FD as multiple JSON documents
+ --cap-add CAP Add cap CAP when running as privileged user
+ --cap-drop CAP Drop cap CAP when running as privileged user
+
+ among which --args is used internally for passing arguments */
+}
+
+type UnshareConfig struct {
+ // (--unshare-user)
+ // create new user namespace
+ User bool `json:"user"`
+ // (--unshare-ipc)
+ // create new ipc namespace
+ IPC bool `json:"ipc"`
+ // (--unshare-pid)
+ // create new pid namespace
+ PID bool `json:"pid"`
+ // (--unshare-net)
+ // create new network namespace
+ Net bool `json:"net"`
+ // (--unshare-uts)
+ // create new uts namespace
+ UTS bool `json:"uts"`
+ // (--unshare-cgroup)
+ // create new cgroup namespace
+ CGroup bool `json:"cgroup"`
+}
+
+type TmpfsConfig struct {
+ // set size of tmpfs
+ // (--size BYTES)
+ Size int `json:"size,omitempty"`
+ // mount point of new tmpfs
+ // (--tmpfs DEST)
+ Dir string `json:"dir"`
+}
+
+type argOf interface {
+ Value(arg string) (args []string)
+}
+
+func copyToArgOfSlice[T [2]string | string | TmpfsConfig](src []PermConfig[T]) (dst []argOf) {
+ dst = make([]argOf, len(src))
+ for i, arg := range src {
+ dst[i] = arg
+ }
+ return
+}
+
+type PermConfig[T [2]string | string | TmpfsConfig] struct {
+ // set permissions of next argument
+ // (--perms OCTAL)
+ Mode *os.FileMode `json:"mode,omitempty"`
+ // path to get the new permission
+ // (--bind-data, --file, etc.)
+ Path T
+}
+
+func (p PermConfig[T]) Value(arg string) (args []string) {
+ // max possible size
+ if p.Mode != nil {
+ args = make([]string, 0, 6)
+ args = append(args, "--perms", strconv.Itoa(int(*p.Mode)))
+ } else {
+ args = make([]string, 0, 4)
+ }
+
+ switch v := any(p.Path).(type) {
+ case string:
+ args = append(args, arg, v)
+ return
+ case [2]string:
+ args = append(args, arg, v[0], v[1])
+ return
+ case TmpfsConfig:
+ if arg != "--tmpfs" {
+ panic("unreachable")
+ }
+
+ if v.Size > 0 {
+ args = append(args, "--size", strconv.Itoa(v.Size))
+ }
+ args = append(args, arg, v.Dir)
+ return
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/helper/bwrap/config.int.go b/helper/bwrap/config.int.go
new file mode 100644
index 00000000..27c88c36
--- /dev/null
+++ b/helper/bwrap/config.int.go
@@ -0,0 +1,22 @@
+package bwrap
+
+const (
+ UID = iota
+ GID
+
+ intC
+)
+
+var intArgs = func() (n [intC]string) {
+ n[UID] = "--uid"
+ n[GID] = "--gid"
+
+ return
+}()
+
+func (c *Config) intArgs() (n [intC]*int) {
+ n[UID] = c.UID
+ n[GID] = c.GID
+
+ return
+}
diff --git a/helper/bwrap/config.interface.go b/helper/bwrap/config.interface.go
new file mode 100644
index 00000000..eb559c1d
--- /dev/null
+++ b/helper/bwrap/config.interface.go
@@ -0,0 +1,34 @@
+package bwrap
+
+const (
+ Procfs = iota
+ DevTmpfs
+ Tmpfs
+ Mqueue
+ Dir
+ Symlink
+
+ interfaceC
+)
+
+var interfaceArgs = func() (g [interfaceC]string) {
+ g[Procfs] = "--proc"
+ g[DevTmpfs] = "--dev"
+ g[Tmpfs] = "--tmpfs"
+ g[Mqueue] = "--mqueue"
+ g[Dir] = "--dir"
+ g[Symlink] = "--symlink"
+
+ return
+}()
+
+func (c *Config) interfaceArgs() (g [interfaceC][]argOf) {
+ g[Procfs] = copyToArgOfSlice(c.Procfs)
+ g[DevTmpfs] = copyToArgOfSlice(c.DevTmpfs)
+ g[Tmpfs] = copyToArgOfSlice(c.Tmpfs)
+ g[Mqueue] = copyToArgOfSlice(c.Mqueue)
+ g[Dir] = copyToArgOfSlice(c.Dir)
+ g[Symlink] = copyToArgOfSlice(c.Symlink)
+
+ return
+}
diff --git a/helper/bwrap/config.pair.go b/helper/bwrap/config.pair.go
new file mode 100644
index 00000000..b0b1001b
--- /dev/null
+++ b/helper/bwrap/config.pair.go
@@ -0,0 +1,54 @@
+package bwrap
+
+import "strconv"
+
+const (
+ SetEnv = iota
+
+ Bind
+ BindTry
+ DevBind
+ DevBindTry
+ ROBind
+ ROBindTry
+
+ Chmod
+
+ pairC
+)
+
+var pairArgs = func() (n [pairC]string) {
+ n[SetEnv] = "--setenv"
+
+ n[Bind] = "--bind"
+ n[BindTry] = "--bind-try"
+ n[DevBind] = "--dev-bind"
+ n[DevBindTry] = "--dev-bind-try"
+ n[ROBind] = "--ro-bind"
+ n[ROBindTry] = "--ro-bind-try"
+
+ n[Chmod] = "--chmod"
+
+ return
+}()
+
+func (c *Config) pairArgs() (n [pairC][][2]string) {
+ n[SetEnv] = make([][2]string, 0, len(c.SetEnv))
+ for k, v := range c.SetEnv {
+ n[SetEnv] = append(n[SetEnv], [2]string{k, v})
+ }
+
+ n[Bind] = c.Bind
+ n[BindTry] = c.BindTry
+ n[DevBind] = c.DevBind
+ n[DevBindTry] = c.DevBindTry
+ n[ROBind] = c.ROBind
+ n[ROBindTry] = c.ROBindTry
+
+ n[Chmod] = make([][2]string, 0, len(c.Chmod))
+ for path, octal := range c.Chmod {
+ n[Chmod] = append(n[Chmod], [2]string{strconv.Itoa(int(octal)), path})
+ }
+
+ return
+}
diff --git a/helper/bwrap/config.string.go b/helper/bwrap/config.string.go
new file mode 100644
index 00000000..c5c7a41e
--- /dev/null
+++ b/helper/bwrap/config.string.go
@@ -0,0 +1,35 @@
+package bwrap
+
+const (
+ Hostname = iota
+ Chdir
+ UnsetEnv
+ LockFile
+ RemountRO
+
+ stringC
+)
+
+var stringArgs = func() (n [stringC]string) {
+ n[Hostname] = "--hostname"
+ n[Chdir] = "--chdir"
+ n[UnsetEnv] = "--unsetenv"
+ n[LockFile] = "--lock-file"
+ n[RemountRO] = "--remount-ro"
+
+ return
+}()
+
+func (c *Config) stringArgs() (n [stringC][]string) {
+ if c.Hostname != "" {
+ n[Hostname] = []string{c.Hostname}
+ }
+ if c.Chdir != "" {
+ n[Chdir] = []string{c.Chdir}
+ }
+ n[UnsetEnv] = c.UnsetEnv
+ n[LockFile] = c.LockFile
+ n[RemountRO] = c.RemountRO
+
+ return
+}
diff --git a/helper/bwrap/config_test.go b/helper/bwrap/config_test.go
new file mode 100644
index 00000000..d43c671d
--- /dev/null
+++ b/helper/bwrap/config_test.go
@@ -0,0 +1,88 @@
+package bwrap
+
+import (
+ "slices"
+ "testing"
+)
+
+func TestConfig_Args(t *testing.T) {
+ testCases := []struct {
+ name string
+ conf *Config
+ want []string
+ }{
+ {
+ name: "xdg-dbus-proxy constraint sample",
+ conf: &Config{
+ Unshare: nil,
+ UserNS: false,
+ Clearenv: true,
+ Symlink: []PermConfig[[2]string]{
+ {Path: [2]string{"usr/bin", "/bin"}},
+ {Path: [2]string{"var/home", "/home"}},
+ {Path: [2]string{"usr/lib", "/lib"}},
+ {Path: [2]string{"usr/lib64", "/lib64"}},
+ {Path: [2]string{"run/media", "/media"}},
+ {Path: [2]string{"var/mnt", "/mnt"}},
+ {Path: [2]string{"var/opt", "/opt"}},
+ {Path: [2]string{"sysroot/ostree", "/ostree"}},
+ {Path: [2]string{"var/roothome", "/root"}},
+ {Path: [2]string{"usr/sbin", "/sbin"}},
+ {Path: [2]string{"var/srv", "/srv"}},
+ },
+ Bind: [][2]string{
+ {"/run", "/run"},
+ {"/tmp", "/tmp"},
+ {"/var", "/var"},
+ {"/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/"},
+ },
+ ROBind: [][2]string{
+ {"/boot", "/boot"},
+ {"/dev", "/dev"},
+ {"/proc", "/proc"},
+ {"/sys", "/sys"},
+ {"/sysroot", "/sysroot"},
+ {"/usr", "/usr"},
+ {"/etc", "/etc"},
+ },
+ DieWithParent: true,
+ },
+ want: []string{
+ "--unshare-all",
+ "--disable-userns",
+ "--assert-userns-disabled",
+ "--clearenv",
+ "--die-with-parent",
+ "--bind", "/run", "/run",
+ "--bind", "/tmp", "/tmp",
+ "--bind", "/var", "/var",
+ "--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/",
+ "--ro-bind", "/boot", "/boot",
+ "--ro-bind", "/dev", "/dev",
+ "--ro-bind", "/proc", "/proc",
+ "--ro-bind", "/sys", "/sys",
+ "--ro-bind", "/sysroot", "/sysroot",
+ "--ro-bind", "/usr", "/usr",
+ "--ro-bind", "/etc", "/etc",
+ "--symlink", "usr/bin", "/bin",
+ "--symlink", "var/home", "/home",
+ "--symlink", "usr/lib", "/lib",
+ "--symlink", "usr/lib64", "/lib64",
+ "--symlink", "run/media", "/media",
+ "--symlink", "var/mnt", "/mnt",
+ "--symlink", "var/opt", "/opt",
+ "--symlink", "sysroot/ostree", "/ostree",
+ "--symlink", "var/roothome", "/root",
+ "--symlink", "usr/sbin", "/sbin",
+ "--symlink", "var/srv", "/srv"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := tc.conf.Args(); !slices.Equal(got, tc.want) {
+ t.Errorf("Args() = %#v, want %#v", got, tc.want)
+ }
+ })
+ }
+}