Files
hakurei/cmd/hakurei/command.go
Ophestra 560cb626a1
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m46s
Test / Hakurei (push) Successful in 3m48s
Test / ShareFS (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m11s
Test / Hakurei (race detector) (push) Successful in 6m20s
Test / Flake checks (push) Successful in 1m23s
hst: remove enablement json adapter
The go116 behaviour of built-in new function makes this cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 20:47:30 +09:00

392 lines
11 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/user"
"strconv"
"sync"
"time"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/dbus"
"hakurei.app/internal/env"
"hakurei.app/internal/info"
"hakurei.app/internal/outcome"
"hakurei.app/message"
)
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
var errSuccess = errors.New("success")
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
flagVerbose bool
flagInsecure bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose)
if early.yamaLSM != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
// not fatal
}
if early.dumpable != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
// not fatal
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
{
var (
flagIdentifierFile int
)
c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("run requires at least 1 argument")
}
config := tryPath(msg, args[0])
if config != nil && config.Container != nil {
config.Container.Args = append(config.Container.Args, args[1:]...)
}
var flags int
if flagInsecure {
flags |= hst.VAllowInsecure
}
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
panic("unreachable")
}).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
"Write identifier of current instance to fd after successful startup")
}
{
var (
flagDBusConfigSession string
flagDBusConfigSystem string
flagDBusMpris bool
flagDBusVerbose bool
flagID string
flagIdentity int
flagGroups command.RepeatableFlag
flagHomeDir string
flagUserName string
flagSchedPolicy string
flagSchedPriority int
flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
)
c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
log.Fatalf("identity %d out of range", flagIdentity)
}
// resolve home/username from os when flag is unset
var (
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil {
msg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Hakurei Permissive Default",
HomeDir: fhs.VarEmpty,
}
} else {
passwd = u
}
}
)
// paths are identical, resolve inner shell and program path
shell := fhs.AbsRoot.Append("bin", "sh")
if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
shell = a
}
progPath := shell
if len(args) > 0 {
if p, err := exec.LookPath(args[0]); err != nil {
log.Fatal(optionalErrorUnwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err)
return err
}
}
var et hst.Enablements
if flagWayland {
et |= hst.EWayland
}
if flagX11 {
et |= hst.EX11
}
if flagDBus {
et |= hst.EDBus
}
if flagPipeWire || flagPulse {
et |= hst.EPipeWire
}
config := hst.Config{
ID: flagID,
Identity: flagIdentity,
Groups: flagGroups,
Enablements: &et,
Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsRoot,
Write: true,
Special: true,
}},
},
Username: flagUserName,
Shell: shell,
Path: progPath,
Args: args,
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
},
}
if err := config.SchedPolicy.UnmarshalText(
[]byte(flagSchedPolicy),
); err != nil {
log.Fatal(err)
}
config.SchedPriority = ext.Int(flagSchedPriority)
// bind GPU stuff
if et&(hst.EX11|hst.EWayland) != 0 {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}})
}
config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("kvm"),
Device: true,
Optional: true,
}},
// do autoetc last
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsEtc,
Source: fhs.AbsEtc,
Special: true,
}},
)
if config.Container.Username == "chronos" {
passwdOnce.Do(passwdFunc)
config.Container.Username = passwd.Username
}
{
homeDir := flagHomeDir
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if a, err := check.NewAbs(homeDir); err != nil {
log.Fatal(err)
return err
} else {
config.Container.Home = a
}
}
if !flagPrivateRuntime {
config.Container.Flags |= hst.FShareRuntime
}
if !flagPrivateTmpdir {
config.Container.Flags |= hst.FShareTmpdir
}
// parse D-Bus config file from flags if applicable
if flagDBus {
if flagDBusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else {
if f, err := os.Open(flagDBusConfigSession); err != nil {
log.Fatal(err)
} else {
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
if err = f.Close(); err != nil {
log.Fatal(err)
}
}
}
// system bus proxy is optional
if flagDBusConfigSystem != "nil" {
if f, err := os.Open(flagDBusConfigSystem); err != nil {
log.Fatal(err)
} else {
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
if err = f.Close(); err != nil {
log.Fatal(err)
}
}
}
// override log from configuration
if flagDBusVerbose {
if config.SessionBus != nil {
config.SessionBus.Log = true
}
if config.SystemBus != nil {
config.SystemBus.Log = true
}
}
}
outcome.Main(ctx, msg, &config, 0, -1)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system bus proxy config file, or \"nil\" to disable").
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
"Force buffered logging in the D-Bus proxy").
Flag(&flagID, "id", command.StringFlag(""),
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&flagIdentity, "a", command.IntFlag(0),
"Application identity").
Flag(nil, "g", &flagGroups,
"Groups inherited by all container processes").
Flag(&flagHomeDir, "d", command.StringFlag("os"),
"Container home directory").
Flag(&flagUserName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox").
Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
"Scheduling policy to set for the container").
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
"Scheduling priority to set for the container").
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
"Do not share TMPDIR between containers under the same identity").
Flag(&flagWayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1").
Flag(&flagX11, "X", command.BoolFlag(false),
"Enable direct connection to X11").
Flag(&flagDBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus").
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
"Enable connection to PipeWire via SecurityContext").
Flag(&flagPulse, "pulse", command.BoolFlag(false),
"Enable PulseAudio compatibility daemon")
}
{
var (
flagShort bool
flagNoStore bool
)
c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON)
case 1: // instance
name := args[0]
var (
config *hst.Config
entry *hst.State
)
if !flagNoStore {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
}
if entry == nil {
config = tryPath(msg, name)
} else {
config = entry.Config
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
}
default:
log.Fatal("show requires 1 argument")
}
return errSuccess
}).
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
}
{
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
return c
}