From d8f76f3b2594db4687c0203c4f2be8d3e4ef7740 Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Wed, 4 Sep 2024 01:20:12 +0900 Subject: rename to fortify and restructure More sandbox features will be added and this will no longer track ego's features and behaviour. Signed-off-by: Ophestra Umiker --- main.go | 342 ++++++++++++++++++---------------------------------------------- 1 file changed, 93 insertions(+), 249 deletions(-) (limited to 'main.go') diff --git a/main.go b/main.go index 4a719d85..102252b4 100644 --- a/main.go +++ b/main.go @@ -6,15 +6,22 @@ import ( "fmt" "io/fs" "os" - "os/exec" - "os/user" "path" "strconv" - "strings" "syscall" + + "git.ophivana.moe/cat/fortify/internal/acl" + "git.ophivana.moe/cat/fortify/internal/app" + "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/util" + "git.ophivana.moe/cat/fortify/internal/xcb" ) -var Version = "impure" +var ( + Version = "impure" + a *app.App +) func tryVersion() { if printVersion { @@ -23,25 +30,9 @@ func tryVersion() { } } -var ( - ego *user.User - uid int - env []string - command []string - verbose bool - runtime string - runDir string -) - const ( - term = "TERM" - home = "HOME" - sudoAskPass = "SUDO_ASKPASS" - xdgRuntimeDir = "XDG_RUNTIME_DIR" - xdgConfigHome = "XDG_CONFIG_HOME" - display = "DISPLAY" - pulseServer = "PULSE_SERVER" - pulseCookie = "PULSE_COOKIE" + term = "TERM" + display = "DISPLAY" // https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html waylandDisplay = "WAYLAND_DISPLAY" @@ -49,315 +40,168 @@ const ( func main() { flag.Parse() - copyArgs() - if u, err := strconv.Atoi(ego.Uid); err != nil { - // usually unreachable - panic("ego uid parse") - } else { - uid = u - } + // launcher payload early exit + app.Early(printVersion) - if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { - fatal("Env variable", xdgRuntimeDir, "unset") - } else { - runtime = r - runDir = path.Join(runtime, "ego") + // version/license command early exit + tryVersion() + tryLicense() + + system.Retrieve(flagVerbose) + a = app.New(userName, flag.Args()) + state.Set(*a.User, a.Command(), a.UID()) + + // ensure RunDir (e.g. `/run/user/%d/fortify`) + if err := os.Mkdir(system.V.RunDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) { + state.Fatal("Error creating runtime directory:", err) } - // state query command - tryState() + // state query command early exit + state.Early() + + // ensure Share (e.g. `/tmp/fortify.%d`) + // acl is unnecessary as this directory is world executable + if err := os.Mkdir(system.V.Share, 0701); err != nil && !errors.Is(err, fs.ErrExist) { + state.Fatal("Error creating shared directory:", err) + } - // Report warning if user home directory does not exist or has wrong ownership - if stat, err := os.Stat(ego.HomeDir); err != nil { - if verbose { + // warn about target user home directory ownership + if stat, err := os.Stat(a.HomeDir); err != nil { + if system.V.Verbose { switch { case errors.Is(err, fs.ErrPermission): - fmt.Printf("User %s home directory %s is not accessible", ego.Username, ego.HomeDir) + fmt.Printf("User %s home directory %s is not accessible", a.Username, a.HomeDir) case errors.Is(err, fs.ErrNotExist): - fmt.Printf("User %s home directory %s does not exist", ego.Username, ego.HomeDir) + fmt.Printf("User %s home directory %s does not exist", a.Username, a.HomeDir) default: - fmt.Printf("Error stat user %s home directory %s: %s", ego.Username, ego.HomeDir, err) + fmt.Printf("Error stat user %s home directory %s: %s", a.Username, a.HomeDir, err) } } return } else { // FreeBSD: not cross-platform - if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != ego.Uid { - fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", ego.Username, ego.HomeDir, ego.Uid, u) + if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != a.Uid { + fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", a.Username, a.HomeDir, a.Uid, u) } } - // Add execute perm to runtime dir, e.g. `/run/user/%d` - if s, err := os.Stat(runtime); err != nil { + // ensure runtime directory ACL (e.g. `/run/user/%d`) + if s, err := os.Stat(system.V.Runtime); err != nil { if errors.Is(err, fs.ErrNotExist) { - fatal("Runtime directory does not exist") + state.Fatal("Runtime directory does not exist") } - fatal("Error accessing runtime directory:", err) + state.Fatal("Error accessing runtime directory:", err) } else if !s.IsDir() { - fatal(fmt.Sprintf("Path '%s' is not a directory", runtime)) + state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime)) } else { - if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil { - fatal("Error preparing runtime dir:", err) + if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil { + state.Fatal("Error preparing runtime dir:", err) } else { - registerRevertPath(runtime) + state.RegisterRevertPath(system.V.Runtime) } - if verbose { - fmt.Printf("Runtime data dir '%s' configured\n", runtime) + if system.V.Verbose { + fmt.Printf("Runtime data dir '%s' configured\n", system.V.Runtime) } } - // Create runtime dir for Ego itself (e.g. `/run/user/%d/ego`) and make it readable for target - if err := os.Mkdir(runDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) { - fatal("Error creating Ego runtime dir:", err) - } - if err := aclUpdatePerm(runDir, uid, aclExecute); err != nil { - fatal("Error preparing Ego runtime dir:", err) - } else { - registerRevertPath(runDir) - } - - // Add rwx permissions to Wayland socket (e.g. `/run/user/%d/wayland-0`) + // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) if w, ok := os.LookupEnv(waylandDisplay); !ok { - if verbose { + if system.V.Verbose { fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping") } } else { // add environment variable for new process - env = append(env, waylandDisplay+"="+path.Join(runtime, w)) - wp := path.Join(runtime, w) - if err := aclUpdatePerm(wp, uid, aclRead, aclWrite, aclExecute); err != nil { - fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err) + wp := path.Join(system.V.Runtime, w) + a.AppendEnv(waylandDisplay, wp) + if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil { + state.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err) } else { - registerRevertPath(wp) + state.RegisterRevertPath(wp) } - if verbose { + if system.V.Verbose { fmt.Printf("Wayland socket '%s' configured\n", w) } } - // Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command + // discovery X11 and grant user permission via the `ChangeHosts` command if d, ok := os.LookupEnv(display); !ok { - if verbose { + if system.V.Verbose { fmt.Println("X11: DISPLAY not set, skipping") } } else { // add environment variable for new process - env = append(env, display+"="+d) + a.AppendEnv(display, d) - if verbose { - fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d) + if system.V.Verbose { + fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d) } - if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil { - fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err) + if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+a.Username); err != nil { + state.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err) } else { - xcbActionComplete = true + state.XcbActionComplete() } } - // Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`) - pulse := path.Join(runtime, "pulse") + // ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`) + pulse := path.Join(system.V.Runtime, "pulse") pulseS := path.Join(pulse, "native") if s, err := os.Stat(pulse); err != nil { if !errors.Is(err, fs.ErrNotExist) { - fatal("Error accessing PulseAudio directory:", err) + state.Fatal("Error accessing PulseAudio directory:", err) } if mustPulse { - fatal("PulseAudio is unavailable") + state.Fatal("PulseAudio is unavailable") } - if verbose { + if system.V.Verbose { fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse) } } else { // add environment variable for new process - env = append(env, pulseServer+"=unix:"+pulseS) - if err = aclUpdatePerm(pulse, uid, aclExecute); err != nil { - fatal("Error preparing PulseAudio:", err) + a.AppendEnv(util.PulseServer, "unix:"+pulseS) + if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil { + state.Fatal("Error preparing PulseAudio:", err) } else { - registerRevertPath(pulse) + state.RegisterRevertPath(pulse) } - // Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native` + // ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`) if s, err = os.Stat(pulseS); err != nil { if errors.Is(err, fs.ErrNotExist) { - fatal("PulseAudio directory found but socket does not exist") + state.Fatal("PulseAudio directory found but socket does not exist") } - fatal("Error accessing PulseAudio socket:", err) + state.Fatal("Error accessing PulseAudio socket:", err) } else { if m := s.Mode(); m&0o006 != 0o006 { - fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m) + state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m) } } // Publish current user's pulse-cookie for target user - pulseCookieSource := discoverPulseCookie() - env = append(env, pulseCookie+"="+pulseCookieSource) - pulseCookieFinal := path.Join(runDir, "pulse-cookie") - if verbose { + pulseCookieSource := util.DiscoverPulseCookie() + pulseCookieFinal := path.Join(system.V.Share, "pulse-cookie") + a.AppendEnv(util.PulseCookie, pulseCookieFinal) + if system.V.Verbose { fmt.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal) } - if err = copyFile(pulseCookieFinal, pulseCookieSource); err != nil { - fatal("Error copying PulseAudio cookie:", err) + if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil { + state.Fatal("Error copying PulseAudio cookie:", err) } - if err = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil { - fatal("Error publishing PulseAudio cookie:", err) + if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil { + state.Fatal("Error publishing PulseAudio cookie:", err) } else { - registerRevertPath(pulseCookieFinal) + state.RegisterRevertPath(pulseCookieFinal) } - if verbose { + if system.V.Verbose { fmt.Printf("PulseAudio dir '%s' configured\n", pulse) } } // pass $TERM to launcher if t, ok := os.LookupEnv(term); ok { - env = append(env, term+"="+t) - } - - f := launchBySudo - m, b := false, false - switch { - case methodFlags[0]: // sudo - case methodFlags[1]: // bare - m, b = true, true - default: // machinectl - m, b = true, false - } - - var toolPath string - - // dependency checks - const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work" - if m { - if !sdBooted() { - fmt.Println("This system was not booted through systemd") - fmt.Println(sudoFallback) - } else if tp, ok := which("machinectl"); !ok { - fmt.Println("Did not find 'machinectl' in PATH") - fmt.Println(sudoFallback) - } else { - toolPath = tp - f = func() []string { return launchByMachineCtl(b) } - } - } else if tp, ok := which("sudo"); !ok { - fatal("Did not find 'sudo' in PATH") - } else { - toolPath = tp - } - - if verbose { - fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b) - } - - cmd := exec.Command(toolPath, f()...) - cmd.Env = env - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Dir = runDir - - if verbose { - fmt.Println("Executing:", cmd) - } - - if err := cmd.Start(); err != nil { - fatal("Error starting process:", err) - } - - if err := registerProcess(ego.Uid, cmd); err != nil { - // process already started, shouldn't be fatal - fmt.Println("Error registering process:", err) - } - - var r int - if err := cmd.Wait(); err != nil { - var exitError *exec.ExitError - if !errors.As(err, &exitError) { - fatal("Error running process:", err) - } - } - - if verbose { - fmt.Println("Process exited with exit code", r) - } - beforeExit() - os.Exit(r) -} - -func launchBySudo() (args []string) { - args = make([]string, 0, 4+len(env)+len(command)) - - // -Hiu $USER - args = append(args, "-Hiu", ego.Username) - - // -A? - if _, ok := os.LookupEnv(sudoAskPass); ok { - if verbose { - fmt.Printf("%s set, adding askpass flag\n", sudoAskPass) - } - args = append(args, "-A") - } - - // environ - args = append(args, env...) - - // -- $@ - args = append(args, "--") - args = append(args, command...) - - return -} - -func launchByMachineCtl(bare bool) (args []string) { - args = make([]string, 0, 9+len(env)) - - // shell --uid=$USER - args = append(args, "shell", "--uid="+ego.Username) - - // --quiet - if !verbose { - args = append(args, "--quiet") - } - - // environ - envQ := make([]string, len(env)+1) - for i, e := range env { - envQ[i] = "-E" + e - } - envQ[len(env)] = "-E" + launcherPayloadEnv() - args = append(args, envQ...) - - // -- .host - args = append(args, "--", ".host") - - // /bin/sh -c - if sh, ok := which("sh"); !ok { - fatal("Did not find 'sh' in PATH") - } else { - args = append(args, sh, "-c") - } - - if len(command) == 0 { // execute shell if command is not provided - command = []string{"$SHELL"} - } - - innerCommand := strings.Builder{} - - if !bare { - innerCommand.WriteString("dbus-update-activation-environment --systemd") - for _, e := range env { - innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0]) - } - innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ") - } - - if executable, err := os.Executable(); err != nil { - fatal("Error reading executable path:", err) - } else { - innerCommand.WriteString("exec " + executable + " -V") + a.AppendEnv(term, t) } - args = append(args, innerCommand.String()) - return + a.Run() } -- cgit v1.3.1