diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go new file mode 100644 index 0000000..5e9d573 --- /dev/null +++ b/cmd/hakurei/command.go @@ -0,0 +1,258 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/signal" + "os/user" + "strconv" + "sync" + "syscall" + "time" + + "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" + "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app/instance" + "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/state" + "git.gensokyo.uk/security/hakurei/command" + "git.gensokyo.uk/security/hakurei/dbus" + "git.gensokyo.uk/security/hakurei/hst" + "git.gensokyo.uk/security/hakurei/internal" + "git.gensokyo.uk/security/hakurei/internal/hlog" + "git.gensokyo.uk/security/hakurei/system" +) + +func buildCommand(out io.Writer) command.Command { + var ( + flagVerbose bool + flagJSON bool + ) + c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); 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 { instance.ShimMain(); return errSuccess }) + + c.Command("app", "Load app from configuration file", func(args []string) error { + if len(args) < 1 { + log.Fatal("app requires at least 1 argument") + } + + // config extraArgs... + config := tryPath(args[0]) + config.Args = append(config.Args, args[1:]...) + + runApp(config) + panic("unreachable") + }) + + { + var ( + dbusConfigSession string + dbusConfigSystem string + mpris bool + dbusVerbose bool + + fid string + aid int + groups command.RepeatableFlag + homeDir string + userName string + + wayland, x11, dBus, pulse bool + ) + + c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error { + // initialise config from flags + config := &hst.Config{ + ID: fid, + Args: args, + } + + if aid < 0 || aid > 9999 { + log.Fatalf("aid %d out of range", aid) + } + + // resolve home/username from os when flag is unset + var ( + passwd *user.User + passwdOnce sync.Once + passwdFunc = func() { + var us string + if uid, err := std.Uid(aid); err != nil { + hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:") + os.Exit(1) + } else { + us = strconv.Itoa(uid) + } + + if u, err := user.LookupId(us); err != nil { + hlog.Verbosef("cannot look up uid %s", us) + passwd = &user.User{ + Uid: us, + Gid: us, + Username: "chronos", + Name: "Hakurei Permissive Default", + HomeDir: "/var/empty", + } + } else { + passwd = u + } + } + ) + + if homeDir == "os" { + passwdOnce.Do(passwdFunc) + homeDir = passwd.HomeDir + } + + if userName == "chronos" { + passwdOnce.Do(passwdFunc) + userName = passwd.Username + } + + config.Identity = aid + config.Groups = groups + config.Data = homeDir + config.Username = userName + + if wayland { + config.Enablements |= system.EWayland + } + if x11 { + config.Enablements |= system.EX11 + } + if dBus { + config.Enablements |= system.EDBus + } + if pulse { + config.Enablements |= system.EPulse + } + + // parse D-Bus config file from flags if applicable + if dBus { + if dbusConfigSession == "builtin" { + config.SessionBus = dbus.NewConfig(fid, true, mpris) + } else { + if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil { + log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err) + } else { + config.SessionBus = conf + } + } + + // system bus proxy is optional + if dbusConfigSystem != "nil" { + if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil { + log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err) + } else { + config.SystemBus = conf + } + } + + // override log from configuration + if dbusVerbose { + config.SessionBus.Log = true + config.SystemBus.Log = true + } + } + + // invoke app + runApp(config) + panic("unreachable") + }). + Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"), + "Path to session bus proxy config file, or \"builtin\" for defaults"). + Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"), + "Path to system bus proxy config file, or \"nil\" to disable"). + Flag(&mpris, "mpris", command.BoolFlag(false), + "Allow owning MPRIS D-Bus path, has no effect if custom config is available"). + Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false), + "Force buffered logging in the D-Bus proxy"). + Flag(&fid, "id", command.StringFlag(""), + "Reverse-DNS style Application identifier, leave empty to inherit instance identifier"). + Flag(&aid, "a", command.IntFlag(0), + "Application identity"). + Flag(nil, "g", &groups, + "Groups inherited by all container processes"). + Flag(&homeDir, "d", command.StringFlag("os"), + "Container home directory"). + Flag(&userName, "u", command.StringFlag("chronos"), + "Passwd user name within sandbox"). + Flag(&wayland, "wayland", command.BoolFlag(false), + "Enable connection to Wayland via security-context-v1"). + Flag(&x11, "X", command.BoolFlag(false), + "Enable direct connection to X11"). + Flag(&dBus, "dbus", command.BoolFlag(false), + "Enable proxied connection to D-Bus"). + Flag(&pulse, "pulse", command.BoolFlag(false), + "Enable direct connection to PulseAudio") + } + + var showFlagShort bool + c.NewCommand("show", "Show live or local app configuration", func(args []string) error { + switch len(args) { + case 0: // system + printShowSystem(os.Stdout, showFlagShort, flagJSON) + + case 1: // instance + name := args[0] + config, entry := tryShort(name) + if config == nil { + config = tryPath(name) + } + printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON) + + default: + log.Fatal("show requires 1 argument") + } + return errSuccess + }).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information") + + var psFlagShort bool + c.NewCommand("ps", "List active instances", func(args []string) error { + printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON) + return errSuccess + }).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id") + + c.Command("version", "Display version information", func(args []string) error { + fmt.Println(internal.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 { + printJSON(os.Stdout, false, hst.Template()) + return errSuccess + }) + + c.Command("help", "Show this help message", func([]string) error { + c.PrintHelp() + return errSuccess + }) + + return c +} + +func runApp(config *hst.Config) { + ctx, stop := signal.NotifyContext(context.Background(), + syscall.SIGINT, syscall.SIGTERM) + defer stop() // unreachable + a := instance.MustNew(instance.ISetuid, ctx, std) + + rs := new(app.RunState) + if sa, err := a.Seal(config); err != nil { + hlog.PrintBaseError(err, "cannot seal app:") + internal.Exit(1) + } else { + internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))) + } + + *(*int)(nil) = 0 // not reached +} diff --git a/cmd/hakurei/main_test.go b/cmd/hakurei/command_test.go similarity index 100% rename from cmd/hakurei/main_test.go rename to cmd/hakurei/command_test.go diff --git a/cmd/hakurei/main.go b/cmd/hakurei/main.go index 143ce9c..4cfe7eb 100644 --- a/cmd/hakurei/main.go +++ b/cmd/hakurei/main.go @@ -4,31 +4,15 @@ package main //go:generate cp ../../LICENSE . import ( - "context" _ "embed" "errors" - "fmt" - "io" "log" "os" - "os/signal" - "os/user" - "strconv" - "sync" - "syscall" - "time" - "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" - "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app/instance" - "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/state" - "git.gensokyo.uk/security/hakurei/command" - "git.gensokyo.uk/security/hakurei/dbus" - "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" "git.gensokyo.uk/security/hakurei/internal/sys" "git.gensokyo.uk/security/hakurei/sandbox" - "git.gensokyo.uk/security/hakurei/system" ) var ( @@ -61,239 +45,7 @@ func main() { hlog.BeforeExit() os.Exit(0) } + // this catches faulty command handlers that fail to return before this point }) log.Fatal("unreachable") } - -func buildCommand(out io.Writer) command.Command { - var ( - flagVerbose bool - flagJSON bool - ) - c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); 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 { instance.ShimMain(); return errSuccess }) - - c.Command("app", "Load app from configuration file", func(args []string) error { - if len(args) < 1 { - log.Fatal("app requires at least 1 argument") - } - - // config extraArgs... - config := tryPath(args[0]) - config.Args = append(config.Args, args[1:]...) - - runApp(config) - panic("unreachable") - }) - - { - var ( - dbusConfigSession string - dbusConfigSystem string - mpris bool - dbusVerbose bool - - fid string - aid int - groups command.RepeatableFlag - homeDir string - userName string - - wayland, x11, dBus, pulse bool - ) - - c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error { - // initialise config from flags - config := &hst.Config{ - ID: fid, - Args: args, - } - - if aid < 0 || aid > 9999 { - log.Fatalf("aid %d out of range", aid) - } - - // resolve home/username from os when flag is unset - var ( - passwd *user.User - passwdOnce sync.Once - passwdFunc = func() { - var us string - if uid, err := std.Uid(aid); err != nil { - hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:") - os.Exit(1) - } else { - us = strconv.Itoa(uid) - } - - if u, err := user.LookupId(us); err != nil { - hlog.Verbosef("cannot look up uid %s", us) - passwd = &user.User{ - Uid: us, - Gid: us, - Username: "chronos", - Name: "Hakurei Permissive Default", - HomeDir: "/var/empty", - } - } else { - passwd = u - } - } - ) - - if homeDir == "os" { - passwdOnce.Do(passwdFunc) - homeDir = passwd.HomeDir - } - - if userName == "chronos" { - passwdOnce.Do(passwdFunc) - userName = passwd.Username - } - - config.Identity = aid - config.Groups = groups - config.Data = homeDir - config.Username = userName - - if wayland { - config.Enablements |= system.EWayland - } - if x11 { - config.Enablements |= system.EX11 - } - if dBus { - config.Enablements |= system.EDBus - } - if pulse { - config.Enablements |= system.EPulse - } - - // parse D-Bus config file from flags if applicable - if dBus { - if dbusConfigSession == "builtin" { - config.SessionBus = dbus.NewConfig(fid, true, mpris) - } else { - if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil { - log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err) - } else { - config.SessionBus = conf - } - } - - // system bus proxy is optional - if dbusConfigSystem != "nil" { - if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil { - log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err) - } else { - config.SystemBus = conf - } - } - - // override log from configuration - if dbusVerbose { - config.SessionBus.Log = true - config.SystemBus.Log = true - } - } - - // invoke app - runApp(config) - panic("unreachable") - }). - Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"), - "Path to session bus proxy config file, or \"builtin\" for defaults"). - Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"), - "Path to system bus proxy config file, or \"nil\" to disable"). - Flag(&mpris, "mpris", command.BoolFlag(false), - "Allow owning MPRIS D-Bus path, has no effect if custom config is available"). - Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false), - "Force buffered logging in the D-Bus proxy"). - Flag(&fid, "id", command.StringFlag(""), - "Reverse-DNS style Application identifier, leave empty to inherit instance identifier"). - Flag(&aid, "a", command.IntFlag(0), - "Application identity"). - Flag(nil, "g", &groups, - "Groups inherited by all container processes"). - Flag(&homeDir, "d", command.StringFlag("os"), - "Container home directory"). - Flag(&userName, "u", command.StringFlag("chronos"), - "Passwd user name within sandbox"). - Flag(&wayland, "wayland", command.BoolFlag(false), - "Enable connection to Wayland via security-context-v1"). - Flag(&x11, "X", command.BoolFlag(false), - "Enable direct connection to X11"). - Flag(&dBus, "dbus", command.BoolFlag(false), - "Enable proxied connection to D-Bus"). - Flag(&pulse, "pulse", command.BoolFlag(false), - "Enable direct connection to PulseAudio") - } - - var showFlagShort bool - c.NewCommand("show", "Show live or local app configuration", func(args []string) error { - switch len(args) { - case 0: // system - printShowSystem(os.Stdout, showFlagShort, flagJSON) - - case 1: // instance - name := args[0] - config, entry := tryShort(name) - if config == nil { - config = tryPath(name) - } - printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON) - - default: - log.Fatal("show requires 1 argument") - } - return errSuccess - }).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information") - - var psFlagShort bool - c.NewCommand("ps", "List active instances", func(args []string) error { - printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON) - return errSuccess - }).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id") - - c.Command("version", "Display version information", func(args []string) error { - fmt.Println(internal.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 { - printJSON(os.Stdout, false, hst.Template()) - return errSuccess - }) - - c.Command("help", "Show this help message", func([]string) error { - c.PrintHelp() - return errSuccess - }) - - return c -} - -func runApp(config *hst.Config) { - ctx, stop := signal.NotifyContext(context.Background(), - syscall.SIGINT, syscall.SIGTERM) - defer stop() // unreachable - a := instance.MustNew(instance.ISetuid, ctx, std) - - rs := new(app.RunState) - if sa, err := a.Seal(config); err != nil { - hlog.PrintBaseError(err, "cannot seal app:") - internal.Exit(1) - } else { - internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))) - } - - *(*int)(nil) = 0 // not reached -}