diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-10-14 06:37:24 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-10-14 06:39:00 +0900 |
| commit | 4c647add0db06cc7a28571aee7502737f0d66b27 (patch) | |
| tree | 7446453d4380ed21c3ad3c3f8026c708fc53e729 /hst | |
| parent | a3414669428d4de37b68aff3f2db14e5768e1f59 (diff) | |
hst/container: pack boolean options
The memory saving is relatively insignificant, however this increases serialisation efficiency.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'hst')
| -rw-r--r-- | hst/container.go | 151 | ||||
| -rw-r--r-- | hst/container_test.go | 76 | ||||
| -rw-r--r-- | hst/hst.go | 17 | ||||
| -rw-r--r-- | hst/hst_test.go | 51 |
4 files changed, 244 insertions, 51 deletions
diff --git a/hst/container.go b/hst/container.go index ca75c17b..69909389 100644 --- a/hst/container.go +++ b/hst/container.go @@ -1,6 +1,8 @@ package hst import ( + "encoding/json" + "syscall" "time" "hakurei.app/container/check" @@ -29,6 +31,37 @@ const ( ShimExitOrphan = 3 ) +const ( + // FMultiarch unblocks syscalls required for multiarch to work on applicable targets. + FMultiarch uintptr = 1 << iota + + // FSeccompCompat causes emitted seccomp filter programs to be identical to Flatpak. + FSeccompCompat + // FDevel unblocks ptrace and friends. + FDevel + // FUserns unblocks userns creation and container setup syscalls. + FUserns + // FHostNet skips net namespace creation. + FHostNet + // FHostAbstract skips setting up abstract unix socket scope. + FHostAbstract + // FTty unblocks dangerous terminal I/O (faking input). + FTty + + // FMapRealUID maps the target user uid to the privileged user uid in the container user namespace. + // Some programs fail to connect to dbus session running as a different uid, + // this option works around it by mapping priv-side caller uid in container. + FMapRealUID + + // FDevice mount /dev/ from the init mount namespace as-is in the container mount namespace. + FDevice + + fMax + + // FAll is [ContainerConfig.Flags] with all currently defined bits set. + FAll = fMax - 1 +) + // ContainerConfig describes the container configuration to be applied to an underlying [container]. type ContainerConfig struct { // Container UTS namespace hostname. @@ -39,33 +72,9 @@ type ContainerConfig struct { // Values lesser than zero is equivalent to zero, bypassing [WaitDelayDefault]. WaitDelay time.Duration `json:"wait_delay,omitempty"` - // Emit Flatpak-compatible seccomp filter programs. - SeccompCompat bool `json:"seccomp_compat,omitempty"` - // Allow ptrace and friends. - Devel bool `json:"devel,omitempty"` - // Allow userns creation and container setup syscalls. - Userns bool `json:"userns,omitempty"` - // Share host net namespace. - HostNet bool `json:"host_net,omitempty"` - // Share abstract unix socket scope. - HostAbstract bool `json:"host_abstract,omitempty"` - // Allow dangerous terminal I/O (faking input). - Tty bool `json:"tty,omitempty"` - // Allow multiarch. - Multiarch bool `json:"multiarch,omitempty"` - // Initial process environment variables. Env map[string]string `json:"env"` - /* Map target user uid to privileged user uid in the container user namespace. - - Some programs fail to connect to dbus session running as a different uid, - this option works around it by mapping priv-side caller uid in container. */ - MapRealUID bool `json:"map_real_uid"` - - // Mount /dev/ from the init mount namespace as-is in the container mount namespace. - Device bool `json:"device,omitempty"` - /* Container mount points. If the first element targets /, it is inserted early and excluded from path hiding. */ @@ -83,4 +92,98 @@ type ContainerConfig struct { Path *check.Absolute `json:"path,omitempty"` // Final args passed to the initial program. Args []string `json:"args"` + + // Flags holds boolean options of [ContainerConfig]. + Flags uintptr `json:"-"` +} + +// ContainerConfigF is [ContainerConfig] stripped of its methods. +// The [ContainerConfig.Flags] field does not survive a [json] round trip. +type ContainerConfigF ContainerConfig + +// containerConfigJSON is the [json] representation of [ContainerConfig]. +type containerConfigJSON = struct { + *ContainerConfigF + + // Corresponds to [FSeccompCompat]. + SeccompCompat bool `json:"seccomp_compat,omitempty"` + // Corresponds to [FDevel]. + Devel bool `json:"devel,omitempty"` + // Corresponds to [FUserns]. + Userns bool `json:"userns,omitempty"` + // Corresponds to [FHostNet]. + HostNet bool `json:"host_net,omitempty"` + // Corresponds to [FHostAbstract]. + HostAbstract bool `json:"host_abstract,omitempty"` + // Corresponds to [FTty]. + Tty bool `json:"tty,omitempty"` + + // Corresponds to [FMultiarch]. + Multiarch bool `json:"multiarch,omitempty"` + + // Corresponds to [FMapRealUID]. + MapRealUID bool `json:"map_real_uid"` + + // Corresponds to [FDevice]. + Device bool `json:"device,omitempty"` +} + +func (c *ContainerConfig) MarshalJSON() ([]byte, error) { + if c == nil { + return nil, syscall.EINVAL + } + return json.Marshal(&containerConfigJSON{ + ContainerConfigF: (*ContainerConfigF)(c), + + SeccompCompat: c.Flags&FSeccompCompat != 0, + Devel: c.Flags&FDevel != 0, + Userns: c.Flags&FUserns != 0, + HostNet: c.Flags&FHostNet != 0, + HostAbstract: c.Flags&FHostAbstract != 0, + Tty: c.Flags&FTty != 0, + Multiarch: c.Flags&FMultiarch != 0, + MapRealUID: c.Flags&FMapRealUID != 0, + Device: c.Flags&FDevice != 0, + }) +} + +func (c *ContainerConfig) UnmarshalJSON(data []byte) error { + if c == nil { + return syscall.EINVAL + } + + v := new(containerConfigJSON) + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + *c = *(*ContainerConfig)(v.ContainerConfigF) + if v.SeccompCompat { + c.Flags |= FSeccompCompat + } + if v.Devel { + c.Flags |= FDevel + } + if v.Userns { + c.Flags |= FUserns + } + if v.HostNet { + c.Flags |= FHostNet + } + if v.HostAbstract { + c.Flags |= FHostAbstract + } + if v.Tty { + c.Flags |= FTty + } + if v.Multiarch { + c.Flags |= FMultiarch + } + if v.MapRealUID { + c.Flags |= FMapRealUID + } + if v.Device { + c.Flags |= FDevice + } + return nil } diff --git a/hst/container_test.go b/hst/container_test.go new file mode 100644 index 00000000..049d3540 --- /dev/null +++ b/hst/container_test.go @@ -0,0 +1,76 @@ +package hst_test + +import ( + "encoding/json" + "errors" + "reflect" + "syscall" + "testing" + + "hakurei.app/hst" +) + +func TestContainerConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + c *hst.ContainerConfig + data string + }{ + {"nil", nil, "null"}, + {"zero", new(hst.ContainerConfig), + `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"map_real_uid":false}`}, + {"seccomp compat", &hst.ContainerConfig{Flags: hst.FSeccompCompat}, + `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"map_real_uid":false}`}, + {"hostnet hostabstract", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract}, + `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":false}`}, + {"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID}, + `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`}, + {"all", &hst.ContainerConfig{Flags: hst.FAll}, + `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true}`}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("marshal", func(t *testing.T) { + t.Parallel() + if got, err := json.Marshal(tc.c); err != nil { + t.Fatalf("Marshal: error = %v", err) + } else if string(got) != tc.data { + t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.data) + } + }) + + t.Run("unmarshal", func(t *testing.T) { + t.Parallel() + + { + got := new(hst.ContainerConfig) + if err := json.Unmarshal([]byte(tc.data), &got); err != nil { + t.Fatalf("Unmarshal: error = %v", err) + } + if !reflect.DeepEqual(got, tc.c) { + t.Errorf("Unmarshal: %v, want %v", got, tc.c) + } + } + }) + }) + } + + t.Run("passthrough", func(t *testing.T) { + t.Parallel() + + if _, err := (*hst.ContainerConfig)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) { + t.Errorf("MarshalJSON: error = %v", err) + } + if err := (*hst.ContainerConfig)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) { + t.Errorf("UnmarshalJSON: error = %v", err) + } + if err := new(hst.ContainerConfig).UnmarshalJSON([]byte{}); err == nil { + t.Errorf("UnmarshalJSON: error = %v", err) + } + }) +} @@ -3,6 +3,7 @@ package hst import ( "errors" + "math" "net" "os" @@ -96,17 +97,8 @@ func Template() *Config { Groups: []string{"video", "dialout", "plugdev"}, Container: &ContainerConfig{ - Hostname: "localhost", - Devel: true, - Userns: true, - HostNet: true, - HostAbstract: true, - Device: true, - WaitDelay: -1, - SeccompCompat: true, - Tty: true, - Multiarch: true, - MapRealUID: true, + Hostname: "localhost", + WaitDelay: -1, // example API credentials pulled from Google Chrome // DO NOT USE THESE IN A REAL BROWSER Env: map[string]string{ @@ -143,6 +135,9 @@ func Template() *Config { "--enable-features=UseOzonePlatform", "--ozone-platform=wayland", }, + + // Set all bits here so new flags trip the template test. + Flags: math.MaxUint, }, } } diff --git a/hst/hst_test.go b/hst/hst_test.go index 8bca8d6e..0dd74886 100644 --- a/hst/hst_test.go +++ b/hst/hst_test.go @@ -5,6 +5,7 @@ import ( "errors" "net" "os" + "reflect" "syscall" "testing" @@ -164,20 +165,11 @@ func TestTemplate(t *testing.T) { "container": { "hostname": "localhost", "wait_delay": -1, - "seccomp_compat": true, - "devel": true, - "userns": true, - "host_net": true, - "host_abstract": true, - "tty": true, - "multiarch": true, "env": { "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" }, - "map_real_uid": true, - "device": true, "filesystem": [ { "type": "bind", @@ -243,14 +235,41 @@ func TestTemplate(t *testing.T) { "--disable-smooth-scrolling", "--enable-features=UseOzonePlatform", "--ozone-platform=wayland" - ] + ], + "seccomp_compat": true, + "devel": true, + "userns": true, + "host_net": true, + "host_abstract": true, + "tty": true, + "multiarch": true, + "map_real_uid": true, + "device": true } }` - if p, err := json.MarshalIndent(hst.Template(), "", "\t"); err != nil { - t.Fatalf("cannot marshal: %v", err) - } else if s := string(p); s != want { - t.Fatalf("Template:\n%s\nwant:\n%s", - s, want) - } + t.Run("marshal", func(t *testing.T) { + t.Parallel() + if p, err := json.MarshalIndent(hst.Template(), "", "\t"); err != nil { + t.Fatalf("cannot marshal: %v", err) + } else if s := string(p); s != want { + t.Fatalf("Template:\n%s\nwant:\n%s", + s, want) + } + }) + + t.Run("unmarshal", func(t *testing.T) { + t.Parallel() + + var got *hst.Config + if err := json.Unmarshal([]byte(want), &got); err != nil { + t.Fatalf("Unmarshal: error = %v", err) + } + + wantVal := hst.Template() + wantVal.Container.Flags = hst.FAll + if !reflect.DeepEqual(got, wantVal) { + t.Fatalf("Unmarshal: %#v, want %#v", got, wantVal) + } + }) } |
