Compare commits

..

No commits in common. "c1bfe2cd74f062e63f0d475dce4602a89f9b5c26" and "58d3a1fbc70349dbaf24a5c91946e303731634f9" have entirely different histories.

22 changed files with 333 additions and 1063 deletions

View File

@ -75,32 +75,16 @@ This adds the `environment.fortify` option:
chronos = {
launchers = {
weechat.method = "sudo";
claws-mail.capability.pulse = false;
claws-mail.pulse = false;
discord = {
command = "vesktop --ozone-platform-hint=wayland";
share = pkgs.vesktop;
};
chromium.dbus.config = {
talk = [
"org.freedesktop.DBus"
"org.freedesktop.portal.*"
"org.freedesktop.FileManager1"
"org.freedesktop.Notifications"
"org.freedesktop.ScreenSaver"
];
own = [
"org.chromium.Chromium"
"org.mpris.MediaPlayer2.chromium.*"
];
};
};
packages = with pkgs; [
weechat
claws-mail
vesktop
chromium
];
persistence.directories = [
".config/weechat"
@ -141,19 +125,7 @@ This adds the `environment.fortify` option:
* `command`, the command to run as the target user. Defaults to launcher name.
* `dbus.config`, D-Bus proxy custom configuration.
* `dbus.id`, D-Bus application id, has no effect if `dbus.config` is set.
* `dbus.mpris`, whether to enable MPRIS defaults, has no effect if `dbus.config` is set.
* `capability.wayland`, whether to share the Wayland socket.
* `capability.x11`, whether to share the X11 socket and allow connection.
* `capability.dbus`, whether to proxy D-Bus.
* `capability.pulse`, whether to share the PulseAudio socket and cookie.
* `pulse`, whether to share the PulseAudio socket and cookie.
* `share`, package containing desktop/icon files. Defaults to launcher name.

23
cli.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"flag"
"git.ophivana.moe/cat/fortify/internal/system"
)
var (
userName string
printVersion bool
mustPulse bool
flagVerbose bool
)
func init() {
flag.StringVar(&userName, "u", "chronos", "Specify a username")
flag.BoolVar(&system.MethodFlags[0], "sudo", false, "Use 'sudo' to change user")
flag.BoolVar(&system.MethodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup")
flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal")
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
flag.BoolVar(&printVersion, "V", false, "Print version")
}

View File

@ -1,59 +0,0 @@
package dbus
type Config struct {
See []string `json:"see"`
Talk []string `json:"talk"`
Own []string `json:"own"`
Log bool `json:"log,omitempty"`
Filter bool `json:"filter"`
}
func (c *Config) Args(address, path string) (args []string) {
argc := 2 + len(c.See) + len(c.Talk) + len(c.Own)
if c.Log {
argc++
}
if c.Filter {
argc++
}
args = make([]string, 0, argc)
args = append(args, address, path)
for _, name := range c.See {
args = append(args, "--see="+name)
}
for _, name := range c.Talk {
args = append(args, "--talk="+name)
}
for _, name := range c.Own {
args = append(args, "--own="+name)
}
if c.Log {
args = append(args, "--log")
}
if c.Filter {
args = append(args, "--filter")
}
return
}
// NewConfig returns a reference to a Config struct with optional defaults.
// If id is an empty string own defaults are omitted.
func NewConfig(id string, defaults, mpris bool) (c *Config) {
c = &Config{Filter: true}
if defaults {
c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.portal.*", "org.freedesktop.Notifications"}
if id != "" {
c.Own = []string{id}
if mpris {
c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id)
}
}
}
return
}

View File

@ -1,134 +0,0 @@
package dbus
import (
"errors"
"os"
"os/exec"
)
// Start launches the D-Bus proxy and sets up the Wait method.
// ready should be buffered and should only be received from once.
func (p *Proxy) Start(ready *chan bool) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal == nil {
return errors.New("proxy not sealed")
}
// acquire pipes
if pr, pw, err := os.Pipe(); err != nil {
return err
} else {
p.statP[0], p.statP[1] = pr, pw
}
if pr, pw, err := os.Pipe(); err != nil {
return err
} else {
p.argsP[0], p.argsP[1] = pr, pw
}
p.cmd = exec.Command(p.path,
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
"--fd=3",
"--args=4",
)
p.cmd.Env = []string{}
p.cmd.ExtraFiles = []*os.File{p.statP[1], p.argsP[0]}
p.cmd.Stdout = os.Stdout
p.cmd.Stderr = os.Stderr
if err := p.cmd.Start(); err != nil {
return err
}
statsP, argsP := p.statP[0], p.argsP[1]
if _, err := argsP.Write([]byte(*p.seal)); err != nil {
if err1 := p.cmd.Process.Kill(); err1 != nil {
panic(err1)
}
return err
} else {
if err = argsP.Close(); err != nil {
if err1 := p.cmd.Process.Kill(); err1 != nil {
panic(err1)
}
return err
}
}
wait := make(chan error)
go func() {
// live out the lifespan of the process
wait <- p.cmd.Wait()
}()
read := make(chan error)
go func() {
n, err := statsP.Read(make([]byte, 1))
switch n {
case -1:
if err1 := p.cmd.Process.Kill(); err1 != nil {
panic(err1)
}
read <- err
case 0:
read <- err
case 1:
*ready <- true
read <- nil
default:
panic("unreachable") // unexpected read count
}
}()
p.wait = &wait
p.read = &read
p.ready = ready
return nil
}
// Wait waits for xdg-dbus-proxy to exit or fault.
func (p *Proxy) Wait() error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.wait == nil || p.read == nil {
return errors.New("proxy not running")
}
defer func() {
if err1 := p.statP[0].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) {
panic(err1)
}
if err1 := p.statP[1].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) {
panic(err1)
}
if err1 := p.argsP[0].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) {
panic(err1)
}
if err1 := p.argsP[1].Close(); err1 != nil && !errors.Is(err1, os.ErrClosed) {
panic(err1)
}
}()
select {
case err := <-*p.wait:
*p.ready <- false
return err
case err := <-*p.read:
if err != nil {
*p.ready <- false
return err
}
return <-*p.wait
}
}
// Close closes the status file descriptor passed to xdg-dbus-proxy, causing it to stop.
func (p *Proxy) Close() error {
return p.statP[0].Close()
}

View File

@ -1,73 +0,0 @@
package dbus
import (
"errors"
"os"
"os/exec"
"strings"
"sync"
)
// Proxy holds references to a xdg-dbus-proxy process, and should never be copied.
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
type Proxy struct {
cmd *exec.Cmd
statP [2]*os.File
argsP [2]*os.File
address [2]string
path string
wait *chan error
read *chan error
ready *chan bool
seal *string
lock sync.RWMutex
}
func (p *Proxy) String() string {
if p.cmd != nil {
return p.cmd.String()
}
if p.seal != nil {
return *p.seal
}
return "(unsealed dbus proxy)"
}
// Seal seals the Proxy instance.
func (p *Proxy) Seal(c *Config) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal != nil {
panic("dbus proxy sealed twice")
}
args := c.Args(p.address[0], p.address[1])
seal := strings.Builder{}
for _, arg := range args {
// reject argument strings containing null
for _, b := range arg {
if b == '\x00' {
return errors.New("argument contains null")
}
}
// write null terminated argument
seal.WriteString(arg)
seal.WriteByte('\x00')
}
v := seal.String()
p.seal = &v
return nil
}
// New returns a reference to a new unsealed Proxy.
func New(binPath, address, path string) *Proxy {
return &Proxy{path: binPath, address: [2]string{address, path}}
}

40
flag.go
View File

@ -1,40 +0,0 @@
package main
import (
"flag"
"git.ophivana.moe/cat/fortify/internal/app"
)
var (
userName string
dbusConfig string
dbusID string
mpris bool
mustWayland bool
mustX bool
mustDBus bool
mustPulse bool
flagVerbose bool
printVersion bool
)
func init() {
flag.StringVar(&userName, "u", "chronos", "Passwd name of user to run as")
flag.StringVar(&dbusConfig, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
flag.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
flag.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
flag.BoolVar(&mustWayland, "wayland", false, "Share Wayland socket")
flag.BoolVar(&mustX, "X", false, "Share X11 socket and allow connection")
flag.BoolVar(&mustDBus, "dbus", false, "Proxy D-Bus connection")
flag.BoolVar(&mustPulse, "pulse", false, "Share PulseAudio socket and cookie")
flag.BoolVar(&app.LaunchOptions[app.LaunchMethodSudo], "sudo", false, "Use 'sudo' to switch user")
flag.BoolVar(&app.LaunchOptions[app.LaunchMethodMachineCtl], "machinectl", true, "Use 'machinectl' to switch user")
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
flag.BoolVar(&printVersion, "V", false, "Print version")
}

View File

@ -1,92 +0,0 @@
package app
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal/acl"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
)
const dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
var dbusAddress string
func (a *App) ShareDBus(c *dbus.Config) {
a.setEnablement(state.EnableDBus)
var binPath, address string
target := path.Join(system.V.Share, strconv.Itoa(os.Getpid()))
dbusAddress = "unix:path=" + target
if b, ok := util.Which("xdg-dbus-proxy"); !ok {
state.Fatal("D-Bus: Did not find 'xdg-dbus-proxy' in PATH")
} else {
binPath = b
}
if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
state.Fatal("D-Bus: DBUS_SESSION_BUS_ADDRESS not set")
} else {
address = addr
}
c.Log = system.V.Verbose
p := dbus.New(binPath, address, target)
if system.V.Verbose {
fmt.Println("D-Bus: sealing proxy", c.Args(address, target))
}
if err := p.Seal(c); err != nil {
state.Fatal("D-Bus: invalid config when sealing proxy,", err)
}
ready := make(chan bool, 1)
done := make(chan struct{})
if system.V.Verbose {
fmt.Printf("Starting session bus proxy '%s' for address '%s'\n", dbusAddress, address)
}
if err := p.Start(&ready); err != nil {
state.Fatal("D-Bus: error starting proxy,", err)
}
if system.V.Verbose {
fmt.Println("D-Bus proxy launch:", p)
}
go func() {
if err := p.Wait(); err != nil {
fmt.Println("warn: D-Bus proxy returned error,", err)
} else {
if system.V.Verbose {
fmt.Println("D-Bus proxy uneventful wait")
}
}
if err := os.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("Error removing dangling D-Bus socket:", err)
}
done <- struct{}{}
}()
// register early to enable Fatal cleanup
state.RegisterDBus(p, &done)
if !<-ready {
state.Fatal("D-Bus: proxy did not start correctly")
}
a.AppendEnv(dbusSessionBusAddress, dbusAddress)
if err := acl.UpdatePerm(target, a.UID(), acl.Read, acl.Write); err != nil {
state.Fatal(fmt.Sprintf("Error preparing D-Bus proxy '%s':", dbusAddress), err)
} else {
state.RegisterRevertPath(target)
}
if system.V.Verbose {
fmt.Printf("Session bus proxy '%s' for address '%s' configured\n", dbusAddress, address)
}
}

View File

@ -10,10 +10,14 @@ import (
"syscall"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
)
const launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD"
const (
sudoAskPass = "SUDO_ASKPASS"
launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD"
)
func (a *App) launcherPayloadEnv() string {
r := &bytes.Buffer{}
@ -71,3 +75,80 @@ func Early(printVersion bool) {
}
}
}
func (a *App) launchBySudo() (args []string) {
args = make([]string, 0, 4+len(a.env)+len(a.command))
// -Hiu $USER
args = append(args, "-Hiu", a.Username)
// -A?
if _, ok := os.LookupEnv(sudoAskPass); ok {
if system.V.Verbose {
fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
}
args = append(args, "-A")
}
// environ
args = append(args, a.env...)
// -- $@
args = append(args, "--")
args = append(args, a.command...)
return
}
func (a *App) launchByMachineCtl(bare bool) (args []string) {
args = make([]string, 0, 9+len(a.env))
// shell --uid=$USER
args = append(args, "shell", "--uid="+a.Username)
// --quiet
if !system.V.Verbose {
args = append(args, "--quiet")
}
// environ
envQ := make([]string, len(a.env)+1)
for i, e := range a.env {
envQ[i] = "-E" + e
}
envQ[len(a.env)] = "-E" + a.launcherPayloadEnv()
args = append(args, envQ...)
// -- .host
args = append(args, "--", ".host")
// /bin/sh -c
if sh, ok := util.Which("sh"); !ok {
state.Fatal("Did not find 'sh' in PATH")
} else {
args = append(args, sh, "-c")
}
if len(a.command) == 0 { // execute shell if command is not provided
a.command = []string{"$SHELL"}
}
innerCommand := strings.Builder{}
if !bare {
innerCommand.WriteString("dbus-update-activation-environment --systemd")
for _, e := range a.env {
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
}
innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ")
}
if executable, err := os.Executable(); err != nil {
state.Fatal("Error reading executable path:", err)
} else {
innerCommand.WriteString("exec " + executable + " -V")
}
args = append(args, innerCommand.String())
return
}

View File

@ -1,68 +0,0 @@
package app
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"git.ophivana.moe/cat/fortify/internal/acl"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
)
func (a *App) SharePulse() {
a.setEnablement(state.EnablePulse)
// 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) {
state.Fatal("Error accessing PulseAudio directory:", err)
}
state.Fatal(fmt.Sprintf("PulseAudio dir '%s' not found", pulse))
} else {
// add environment variable for new process
a.AppendEnv(util.PulseServer, "unix:"+pulseS)
if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
state.Fatal("Error preparing PulseAudio:", err)
} else {
state.RegisterRevertPath(pulse)
}
// 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) {
state.Fatal("PulseAudio directory found but socket does not exist")
}
state.Fatal("Error accessing PulseAudio socket:", err)
} else {
if m := s.Mode(); m&0o006 != 0o006 {
state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
}
}
// Publish current user's pulse-cookie for target user
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 = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
state.Fatal("Error copying PulseAudio cookie:", err)
}
if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
state.Fatal("Error publishing PulseAudio cookie:", err)
} else {
state.RegisterRevertPath(pulseCookieFinal)
}
if system.V.Verbose {
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
}
}
}

View File

@ -1,177 +0,0 @@
package app
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
)
const (
term = "TERM"
sudoAskPass = "SUDO_ASKPASS"
)
const (
LaunchMethodSudo = iota
LaunchMethodMachineCtl
launchOptionLength
)
var (
// LaunchOptions is set in main's cli.go
LaunchOptions [launchOptionLength]bool
)
func (a *App) Run() {
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
a.AppendEnv(term, t)
}
commandBuilder := a.commandBuilderSudo
var toolPath string
// dependency checks
const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work"
if LaunchOptions[LaunchMethodMachineCtl] && !LaunchOptions[LaunchMethodSudo] { // sudo argument takes priority
if !util.SdBooted() {
fmt.Println("This system was not booted through systemd")
fmt.Println(sudoFallback)
} else if machineCtlPath, ok := util.Which("machinectl"); !ok {
fmt.Println("Did not find 'machinectl' in PATH")
fmt.Println(sudoFallback)
} else {
toolPath = machineCtlPath
commandBuilder = a.commandBuilderMachineCtl
}
} else if sudoPath, ok := util.Which("sudo"); !ok {
state.Fatal("Did not find 'sudo' in PATH")
} else {
toolPath = sudoPath
}
if system.V.Verbose {
fmt.Printf("Selected launcher '%s'\n", toolPath)
}
cmd := exec.Command(toolPath, commandBuilder()...)
cmd.Env = a.env
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = system.V.RunDir
if system.V.Verbose {
fmt.Println("Executing:", cmd)
}
if err := cmd.Start(); err != nil {
state.Fatal("Error starting process:", err)
}
state.RegisterEnablement(a.enablements)
if err := state.SaveProcess(a.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) {
state.Fatal("Error running process:", err)
}
}
if system.V.Verbose {
fmt.Println("Process exited with exit code", r)
}
state.BeforeExit()
os.Exit(r)
}
func (a *App) commandBuilderSudo() (args []string) {
args = make([]string, 0, 4+len(a.env)+len(a.command))
// -Hiu $USER
args = append(args, "-Hiu", a.Username)
// -A?
if _, ok := os.LookupEnv(sudoAskPass); ok {
if system.V.Verbose {
fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
}
args = append(args, "-A")
}
// environ
args = append(args, a.env...)
// -- $@
args = append(args, "--")
args = append(args, a.command...)
return
}
func (a *App) commandBuilderMachineCtl() (args []string) {
args = make([]string, 0, 9+len(a.env))
// shell --uid=$USER
args = append(args, "shell", "--uid="+a.Username)
// --quiet
if !system.V.Verbose {
args = append(args, "--quiet")
}
// environ
envQ := make([]string, len(a.env)+1)
for i, e := range a.env {
envQ[i] = "-E" + e
}
envQ[len(a.env)] = "-E" + a.launcherPayloadEnv()
args = append(args, envQ...)
// -- .host
args = append(args, "--", ".host")
// /bin/sh -c
if sh, ok := util.Which("sh"); !ok {
state.Fatal("Did not find 'sh' in PATH")
} else {
args = append(args, sh, "-c")
}
if len(a.command) == 0 { // execute shell if command is not provided
a.command = []string{"$SHELL"}
}
innerCommand := strings.Builder{}
innerCommand.WriteString("dbus-update-activation-environment --systemd")
for _, e := range a.env {
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
}
innerCommand.WriteString("; ")
if executable, err := os.Executable(); err != nil {
state.Fatal("Error reading executable path:", err)
} else {
if a.enablements.Has(state.EnableDBus) {
innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + dbusAddress + "' ")
}
innerCommand.WriteString("exec " + executable + " -V")
}
args = append(args, innerCommand.String())
return
}

View File

@ -4,11 +4,13 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/util"
)
type App struct {
@ -16,19 +18,78 @@ type App struct {
env []string
command []string
enablements state.Enablements
*user.User
// absolutely *no* method of this type is thread-safe
// so don't treat it as if it is
}
func (a *App) setEnablement(e state.Enablement) {
if a.enablements.Has(e) {
panic("enablement " + e.String() + " set twice")
func (a *App) Run() {
f := a.launchBySudo
m, b := false, false
switch {
case system.MethodFlags[0]: // sudo
case system.MethodFlags[1]: // bare
m, b = true, true
default: // machinectl
m, b = true, false
}
a.enablements |= e.Mask()
var toolPath string
// dependency checks
const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work"
if m {
if !util.SdBooted() {
fmt.Println("This system was not booted through systemd")
fmt.Println(sudoFallback)
} else if tp, ok := util.Which("machinectl"); !ok {
fmt.Println("Did not find 'machinectl' in PATH")
fmt.Println(sudoFallback)
} else {
toolPath = tp
f = func() []string { return a.launchByMachineCtl(b) }
}
} else if tp, ok := util.Which("sudo"); !ok {
state.Fatal("Did not find 'sudo' in PATH")
} else {
toolPath = tp
}
if system.V.Verbose {
fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b)
}
cmd := exec.Command(toolPath, f()...)
cmd.Env = a.env
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = system.V.RunDir
if system.V.Verbose {
fmt.Println("Executing:", cmd)
}
if err := cmd.Start(); err != nil {
state.Fatal("Error starting process:", err)
}
if err := state.SaveProcess(a.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) {
state.Fatal("Error running process:", err)
}
}
if system.V.Verbose {
fmt.Println("Process exited with exit code", r)
}
state.BeforeExit()
os.Exit(r)
}
func New(userName string, args []string) *App {

View File

@ -1,37 +0,0 @@
package app
import (
"fmt"
"os"
"path"
"git.ophivana.moe/cat/fortify/internal/acl"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
)
const (
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
)
func (a *App) ShareWayland() {
a.setEnablement(state.EnableWayland)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
if w, ok := os.LookupEnv(waylandDisplay); !ok {
state.Fatal("Wayland: WAYLAND_DISPLAY not set")
} else {
// add environment variable for new process
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 {
state.RegisterRevertPath(wp)
}
if system.V.Verbose {
fmt.Printf("Wayland socket '%s' configured\n", w)
}
}
}

View File

@ -1,33 +0,0 @@
package app
import (
"fmt"
"os"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/xcb"
)
const display = "DISPLAY"
func (a *App) ShareX() {
a.setEnablement(state.EnableX)
// discovery X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
state.Fatal("X11: DISPLAY not set")
} else {
// add environment variable for new process
a.AppendEnv(display, d)
if system.V.Verbose {
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
}
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 {
state.XcbActionComplete()
}
}
}

View File

@ -1,34 +0,0 @@
package state
type (
Enablement uint8
Enablements uint64
)
const (
EnableWayland Enablement = iota
EnableX
EnableDBus
EnablePulse
enableLength
)
var enablementString = [enableLength]string{
"Wayland",
"X11",
"D-Bus",
"PulseAudio",
}
func (e Enablement) String() string {
return enablementString[e]
}
func (e Enablement) Mask() Enablements {
return 1 << e
}
func (es Enablements) Has(e Enablement) bool {
return es&e.Mask() != 0
}

View File

@ -33,7 +33,7 @@ func BeforeExit() {
}
}
if d, err := readLaunchers(u.Uid); err != nil {
if d, err := readLaunchers(); err != nil {
fmt.Println("Error reading active launchers:", err)
os.Exit(1)
} else if len(d) > 0 {
@ -65,23 +65,4 @@ func BeforeExit() {
fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", u.Username, candidate)
}
}
if dbusProxy != nil {
if system.V.Verbose {
fmt.Println("D-Bus proxy registered, cleaning up")
}
if err := dbusProxy.Close(); err != nil {
if errors.Is(err, os.ErrClosed) {
if system.V.Verbose {
fmt.Println("D-Bus proxy already closed")
}
} else {
fmt.Println("Error closing D-Bus proxy:", err)
}
}
// wait for Proxy.Wait to return
<-*dbusDone
}
}

View File

@ -1,104 +0,0 @@
package state
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"git.ophivana.moe/cat/fortify/internal/system"
)
var (
stateActionEarly bool
stateActionEarlyC bool
)
func init() {
flag.BoolVar(&stateActionEarly, "state", false, "print state information of active launchers")
flag.BoolVar(&stateActionEarlyC, "state-current", false, "print state information of active launchers for the specified user")
}
func Early() {
var w *tabwriter.Writer
switch {
case stateActionEarly:
if runDir, err := os.ReadDir(system.V.RunDir); err != nil {
fmt.Println("Error reading runtime directory:", err)
} else {
for _, e := range runDir {
if !e.IsDir() {
if system.V.Verbose {
fmt.Println("Skipped non-directory entry", e.Name())
}
continue
}
if _, err = strconv.Atoi(e.Name()); err != nil {
if system.V.Verbose {
fmt.Println("Skipped non-uid entry", e.Name())
}
continue
}
printLauncherState(e.Name(), &w)
}
}
case stateActionEarlyC:
printLauncherState(u.Uid, &w)
default:
return
}
if w != nil {
if err := w.Flush(); err != nil {
fmt.Println("warn: error formatting output:", err)
}
} else {
fmt.Println("No information available.")
}
os.Exit(0)
}
func printLauncherState(uid string, w **tabwriter.Writer) {
launchers, err := readLaunchers(uid)
if err != nil {
fmt.Println("Error reading launchers:", err)
os.Exit(1)
}
if *w == nil {
*w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
if !system.V.Verbose {
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tEnablements\tLauncher\tCommand")
} else {
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
}
}
for _, state := range launchers {
enablementsDescription := strings.Builder{}
for i := Enablement(0); i < enableLength; i++ {
if state.Capability.Has(i) {
enablementsDescription.WriteString(", " + i.String())
}
}
if enablementsDescription.Len() == 0 {
enablementsDescription.WriteString("none")
}
if !system.V.Verbose {
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\n",
uid, state.PID, strings.TrimPrefix(enablementsDescription.String(), ", "), state.Launcher,
state.Command)
} else {
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n",
uid, state.PID, state.Argv)
}
}
}

View File

@ -1,35 +1,12 @@
package state
import "git.ophivana.moe/cat/fortify/dbus"
var (
cleanupCandidate []string
enablements *Enablements
xcbActionComplete bool
dbusProxy *dbus.Proxy
dbusDone *chan struct{}
)
func RegisterRevertPath(p string) {
cleanupCandidate = append(cleanupCandidate, p)
}
func RegisterEnablement(e Enablements) {
if enablements != nil {
panic("enablement state set twice")
}
enablements = &e
}
func XcbActionComplete() {
if xcbActionComplete {
Fatal("xcb inserted twice")
}
xcbActionComplete = true
}
func RegisterDBus(p *dbus.Proxy, done *chan struct{}) {
dbusProxy = p
dbusDone = done
}

View File

@ -3,6 +3,8 @@ package state
import (
"encoding/gob"
"errors"
"flag"
"fmt"
"io/fs"
"os"
"os/exec"
@ -16,7 +18,10 @@ import (
// this and launcher should eventually be replaced by a server process
var (
stateActionEarly bool
statePath string
cleanupCandidate []string
xcbActionComplete bool
)
type launcherState struct {
@ -24,7 +29,29 @@ type launcherState struct {
Launcher string
Argv []string
Command []string
Capability Enablements
}
func init() {
flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers")
}
func Early() {
if !stateActionEarly {
return
}
launchers, err := readLaunchers()
if err != nil {
fmt.Println("Error reading launchers:", err)
os.Exit(1)
}
fmt.Println("\tPID\tLauncher")
for _, state := range launchers {
fmt.Printf("\t%d\t%s\nCommand: %s\nArgv: %s\n", state.PID, state.Launcher, state.Command, state.Argv)
}
os.Exit(0)
}
// SaveProcess called after process start, before wait
@ -35,7 +62,6 @@ func SaveProcess(uid string, cmd *exec.Cmd) error {
Launcher: cmd.Path,
Argv: cmd.Args,
Command: command,
Capability: *enablements,
}
if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
@ -55,10 +81,10 @@ func SaveProcess(uid string, cmd *exec.Cmd) error {
}
}
func readLaunchers(uid string) ([]*launcherState, error) {
func readLaunchers() ([]*launcherState, error) {
var f *os.File
var r []*launcherState
launcherPrefix := path.Join(system.V.RunDir, uid)
launcherPrefix := path.Join(system.V.RunDir, u.Uid)
if pl, err := os.ReadDir(launcherPrefix); err != nil {
return nil, err

View File

@ -11,4 +11,7 @@ type Values struct {
Verbose bool
}
var V *Values
var (
V *Values
MethodFlags [2]bool
)

128
main.go
View File

@ -1,27 +1,26 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/fs"
"os"
"path"
"strconv"
"syscall"
"git.ophivana.moe/cat/fortify/dbus"
"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"
a *app.App
c *dbus.Config
)
func tryVersion() {
@ -31,6 +30,14 @@ func tryVersion() {
}
}
const (
term = "TERM"
display = "DISPLAY"
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
)
func main() {
flag.Parse()
@ -45,21 +52,6 @@ func main() {
a = app.New(userName, flag.Args())
state.Set(*a.User, a.Command(), a.UID())
// parse D-Bus config file if applicable
if mustDBus {
if dbusConfig == "builtin" {
c = dbus.NewConfig(dbusID, true, mpris)
} else {
if f, err := os.Open(dbusConfig); err != nil {
state.Fatal("Error opening D-Bus proxy config file:", err)
} else {
if err = json.NewDecoder(f).Decode(&c); err != nil {
state.Fatal("Error parsing D-Bus proxy config file:", err)
}
}
}
}
// 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)
@ -113,20 +105,102 @@ func main() {
}
}
if mustWayland {
a.ShareWayland()
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
if w, ok := os.LookupEnv(waylandDisplay); !ok {
if system.V.Verbose {
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
}
} else {
// add environment variable for new process
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 {
state.RegisterRevertPath(wp)
}
if system.V.Verbose {
fmt.Printf("Wayland socket '%s' configured\n", w)
}
}
if mustX {
a.ShareX()
// discovery X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
if system.V.Verbose {
fmt.Println("X11: DISPLAY not set, skipping")
}
} else {
// add environment variable for new process
a.AppendEnv(display, d)
if system.V.Verbose {
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
}
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 {
state.XcbActionComplete()
}
}
if mustDBus {
a.ShareDBus(c)
// 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) {
state.Fatal("Error accessing PulseAudio directory:", err)
}
if mustPulse {
a.SharePulse()
state.Fatal("PulseAudio is unavailable")
}
if system.V.Verbose {
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
}
} else {
// add environment variable for new process
a.AppendEnv(util.PulseServer, "unix:"+pulseS)
if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
state.Fatal("Error preparing PulseAudio:", err)
} else {
state.RegisterRevertPath(pulse)
}
// 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) {
state.Fatal("PulseAudio directory found but socket does not exist")
}
state.Fatal("Error accessing PulseAudio socket:", err)
} else {
if m := s.Mode(); m&0o006 != 0o006 {
state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
}
}
// Publish current user's pulse-cookie for target user
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 = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
state.Fatal("Error copying PulseAudio cookie:", err)
}
if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
state.Fatal("Error publishing PulseAudio cookie:", err)
} else {
state.RegisterRevertPath(pulseCookieFinal)
}
if system.V.Verbose {
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
}
}
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
a.AppendEnv(term, t)
}
a.Run()

View File

@ -63,60 +63,6 @@ in
'';
};
dbus = {
config = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus custom configuration.
Setting this to null will enable built-in defaults.
'';
};
id = mkOption {
type = nullOr str;
default = null;
description = ''
D-Bus application id.
Setting this to null will disable own path in defaults.
Has no effect if custom configuration is set.
'';
};
mpris = mkOption {
type = bool;
default = false;
description = ''
Whether to enable MPRIS in D-Bus defaults.
'';
};
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
@ -124,7 +70,6 @@ in
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
@ -219,23 +164,8 @@ in
user: launchers:
mapAttrsToList (
name: launcher:
with launcher.capability;
let
command = if launcher.command == null then name else launcher.command;
dbusConfig =
if launcher.dbus.config != null then
pkgs.writeText "${name}-dbus.json" (builtins.toJSON launcher.dbus.config)
else
null;
capArgs =
(if wayland then " -wayland" else "")
+ (if x11 then " -X" else "")
+ (if dbus then " -dbus" else "")
+ (if pulse then " -pulse" else "")
+ (if launcher.dbus.mpris then " -mpris" else "")
+ (if launcher.dbus.id != null then " -dbus-id ${dbus.id}" else "")
+ (if dbusConfig != null then " -dbus-config ${dbusConfig}" else "")
+ (if launcher.method == "fortify-sudo" then " -sudo" else "");
in
pkgs.writeShellScriptBin name (
if launcher.method == "sudo" then
@ -244,7 +174,9 @@ in
''
else
''
exec fortify${capArgs} -u ${user} ${cfg.shell} -c "exec ${command} $@"
exec fortify${if launcher.pulse then " -pulse" else ""} -u ${user}${
if launcher.method == "fortify-sudo" then " -sudo" else ""
} ${cfg.shell} -c "exec ${command} $@"
''
)
) launchers;

View File

@ -1,15 +1,12 @@
{
lib,
buildGoModule,
makeBinaryWrapper,
xdg-dbus-proxy,
acl,
xorg,
buildGoModule,
}:
buildGoModule rec {
pname = "fortify";
version = "1.1.0";
version = "1.0.4";
src = ./.;
vendorHash = null;
@ -25,10 +22,4 @@ buildGoModule rec {
acl
xorg.libxcb
];
nativeBuildInputs = [ makeBinaryWrapper ];
postInstall = ''
wrapProgram $out/bin/${pname} --prefix PATH : ${lib.makeBinPath [ xdg-dbus-proxy ]}
'';
}