package main import ( "context" "fmt" "io" "log" "os" "os/exec" "os/user" "strconv" "sync" "time" _ "unsafe" // for go:linkname "hakurei.app/command" "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/container/std" "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. // //go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap func optionalErrorUnwrap(err error) error func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { var ( flagVerbose 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(&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("app", "Load and start container from configuration file", func(args []string) error { if len(args) < 1 { log.Fatal("app 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:]...) } outcome.Main(ctx, msg, config, 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("run", "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.Enablement 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: hst.NewEnablements(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 = std.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, -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 app 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 }