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/hst" "hakurei.app/internal/env" "hakurei.app/internal/info" "hakurei.app/internal/outcome" "hakurei.app/internal/system/dbus" "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 flagPrivateRuntime, flagPrivateTmpdir bool flagWayland, flagX11, flagDBus, 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.Error()) return err } } var et hst.Enablement if flagWayland { et |= hst.EWayland } if flagX11 { et |= hst.EX11 } if flagDBus { et |= hst.EDBus } if flagPulse { et |= hst.EPulse } 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, }, } // 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.Error()) 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.Error()) } else { decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus) if err = f.Close(); err != nil { log.Fatal(err.Error()) } } } // system bus proxy is optional if flagDBusConfigSystem != "nil" { if f, err := os.Open(flagDBusConfigSystem); err != nil { log.Fatal(err.Error()) } else { decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus) if err = f.Close(); err != nil { log.Fatal(err.Error()) } } } // 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(&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(&flagPulse, "pulse", command.BoolFlag(false), "Enable direct connection to PulseAudio") } { 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 }