bwrap: implement argument builder
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
parent
df29068d16
commit
ba76e2919b
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue