Compare commits
38 Commits
Author | SHA1 | Date |
---|---|---|
Ophestra Umiker | 2763ec730e | |
Ophestra Umiker | 3d963b9f67 | |
Ophestra Umiker | 4b7d616862 | |
Ophestra Umiker | 6a6f62efa6 | |
Ophestra Umiker | 03c24c5122 | |
Ophestra Umiker | 8bdae74ebe | |
Ophestra Umiker | d49b97b1d4 | |
Ophestra Umiker | 40d0550ad3 | |
Ophestra Umiker | da6d238d8a | |
Ophestra Umiker | b0aff89166 | |
Ophestra Umiker | 8223a9ee66 | |
Ophestra Umiker | 88ac05be6d | |
Ophestra Umiker | 0ef321ad6f | |
Ophestra Umiker | 52f986559c | |
Ophestra Umiker | 396066de7b | |
Ophestra Umiker | 44301cd979 | |
Ophestra Umiker | 20c0e66d8f | |
Ophestra Umiker | e5918ba3b3 | |
Ophestra Umiker | 35d040590b | |
Ophestra Umiker | c1bfe2cd74 | |
Ophestra Umiker | d813f8e44e | |
Ophestra Umiker | 0e5b85fd42 | |
Ophestra Umiker | cdc08817a7 | |
Ophestra Umiker | e5b3fa02f9 | |
Ophestra Umiker | 8e848366cd | |
Ophestra Umiker | 38ef2b4d0c | |
Ophestra Umiker | 357cc4ce4d | |
Ophestra Umiker | 3242ce3406 | |
Ophestra Umiker | 7450b0b0bb | |
Ophestra Umiker | 83af555c97 | |
Ophestra Umiker | 60e4846542 | |
Ophestra Umiker | 1906853382 | |
Ophestra Umiker | 58d3a1fbc7 | |
Ophestra Umiker | 1b5fce5ccb | |
Ophestra Umiker | 945cce2f5e | |
Ophestra Umiker | 5c3e7cf664 | |
Ophestra Umiker | 743b6afbbb | |
Ophestra Umiker | d8f76f3b25 |
|
@ -27,8 +27,8 @@ jobs:
|
|||
if: ${{ runner.os == 'Linux' }}
|
||||
- name: Build for Linux
|
||||
run: >-
|
||||
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/ego &&
|
||||
sha256sum --tag -b bin/ego > bin/ego.sha256"
|
||||
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify &&
|
||||
sha256sum --tag -b bin/fortify > bin/fortify.sha256"
|
||||
- name: Release
|
||||
id: use-go-action
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/ego
|
||||
/fortify
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
|
233
README.md
233
README.md
|
@ -1,83 +1,182 @@
|
|||
ego (the Go side)
|
||||
=================
|
||||
Fortify
|
||||
=======
|
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/ego.svg)](https://pkg.go.dev/git.ophivana.moe/cat/ego)
|
||||
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/cat/fortify)
|
||||
|
||||
> Do all your games need access to your documents, browser history, SSH private keys?
|
||||
>
|
||||
> ... No? Just run `ego steam`!
|
||||
Lets you run graphical applications as another user ~~in an Android-like sandbox environment~~ (WIP) with a nice NixOS
|
||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
||||
|
||||
**Ego** is a tool to run Linux desktop applications under a different local user. Currently
|
||||
integrates with Wayland, Xorg, PulseAudio and xdg-desktop-portal. You may think of it as `xhost`
|
||||
for Wayland and PulseAudio. This is done using filesystem ACLs and X11 host access control.
|
||||
Why would you want this?
|
||||
|
||||
Disclaimer: **DO NOT RUN UNTRUSTED PROGRAMS VIA EGO.** However, using ego is more secure than
|
||||
running applications directly under your primary user.
|
||||
- It protects the desktop environment from applications.
|
||||
|
||||
Differences
|
||||
-----------
|
||||
* Written in Go
|
||||
* Tracks process states
|
||||
* Cleans up after last process exits
|
||||
* Argv preservation in machinectl mode
|
||||
* Has no dependencies other than the two C libraries
|
||||
- It protects applications from each other.
|
||||
|
||||
Manual setup
|
||||
------------
|
||||
Ego aims to come with sane defaults and be easy to set up.
|
||||
- It provides UID isolation on top of ~~the standard application sandbox~~ (WIP).
|
||||
|
||||
**Requirements:**
|
||||
* Sudo
|
||||
* A C compiler
|
||||
* [Go](https://go.dev/doc/install)
|
||||
* `libacl.so` library (Debian/Ubuntu: libacl1-dev; Fedora: libacl-devel; Arch: acl)
|
||||
* `libxcb.so` library (Debian/Ubuntu: libxcb1-dev; Fedora: libxcb-devel; Arch: libxcb)
|
||||
There are a few different things to set up for this to work:
|
||||
|
||||
**Recommended:** (Not needed when using `--sudo` mode, but some desktop functionality may not work).
|
||||
* `machinectl` command (Debian/Ubuntu/Fedora: systemd-container; Arch: systemd)
|
||||
* `xdg-desktop-portal-gtk` (Debian/Ubuntu/Fedora/Arch: xdg-desktop-portal-gtk)
|
||||
- A set of users, each for a group of applications that should be allowed access to each other
|
||||
|
||||
**Installation:**
|
||||
- A tool to switch users, currently sudo and machinectl are supported.
|
||||
|
||||
1. Run in repository worktree:
|
||||
- If you are running NixOS, the module in this repository can take care of launchers and desktop files in the privileged
|
||||
user's environment, as well as packages and extra home-manager configuration for target users.
|
||||
|
||||
go build -v -ldflags '-s -w'
|
||||
sudo cp ego /usr/local/bin/
|
||||
If you have a flakes-enabled nix environment, you can try out the tool by running:
|
||||
|
||||
2. Create local user named "ego": <sup>[1]</sup>
|
||||
|
||||
sudo useradd ego --uid 155 --create-home
|
||||
|
||||
3. That's all, try it:
|
||||
|
||||
ego xdg-open .
|
||||
|
||||
[1] No extra groups are needed by the ego user.
|
||||
UID below 1000 hides this user on the login screen.
|
||||
|
||||
### Avoid password prompt
|
||||
If using "machinectl" mode (default if available), you need the rather new systemd version >=247
|
||||
and polkit >=0.106 to do this securely.
|
||||
|
||||
Create file `/etc/polkit-1/rules.d/50-ego-machinectl.rules`, polkit will automatically load it
|
||||
(replace `$USER` with your own username):
|
||||
|
||||
```js
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.machine1.host-shell" &&
|
||||
action.lookup("user") == "ego" &&
|
||||
subject.user == "$USER") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
```shell
|
||||
nix run git+https://git.ophivana.moe/cat/fortify -- -h
|
||||
```
|
||||
|
||||
##### sudo mode
|
||||
For sudo, add the following to `/etc/sudoers` (replace `$USER` with your own username):
|
||||
## Module usage
|
||||
|
||||
$USER ALL=(ego) NOPASSWD:ALL
|
||||
The NixOS module currently requires home-manager and impermanence to function correctly.
|
||||
|
||||
Appendix
|
||||
--------
|
||||
Ego is licensed under the MIT License (see the `LICENSE` file).
|
||||
The original Ego was created by Marti Raudsepp under the repository https://github.com/intgr/ego
|
||||
To use the module, import it into your configuration with
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
|
||||
fortify = {
|
||||
url = "git+https://git.ophivana.moe/cat/fortify";
|
||||
|
||||
# Optional but recommended to limit the size of your system closure.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, fortify, ... }:
|
||||
{
|
||||
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
fortify.nixosModules.fortify
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This adds the `environment.fortify` option:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
environment.fortify = {
|
||||
enable = true;
|
||||
user = "nixos";
|
||||
shell = "zsh";
|
||||
stateDir = "/var/lib/persist/module";
|
||||
target = {
|
||||
chronos = {
|
||||
launchers = {
|
||||
weechat.method = "sudo";
|
||||
claws-mail.capability.pulse = false;
|
||||
|
||||
discord = {
|
||||
command = "vesktop --ozone-platform-hint=wayland";
|
||||
share = pkgs.vesktop;
|
||||
};
|
||||
|
||||
chromium.dbus = {
|
||||
configSystem = {
|
||||
filter = true;
|
||||
talk = [
|
||||
"org.bluez"
|
||||
"org.freedesktop.Avahi"
|
||||
"org.freedesktop.UPower"
|
||||
];
|
||||
};
|
||||
config = {
|
||||
filter = true;
|
||||
talk = [
|
||||
"org.freedesktop.DBus"
|
||||
"org.freedesktop.FileManager1"
|
||||
"org.freedesktop.Notifications"
|
||||
"org.freedesktop.ScreenSaver"
|
||||
"org.freedesktop.secrets"
|
||||
"org.kde.kwalletd5"
|
||||
"org.kde.kwalletd6"
|
||||
];
|
||||
own = [
|
||||
"org.chromium.Chromium.*"
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*"
|
||||
"org.mpris.MediaPlayer2.chromium.*"
|
||||
];
|
||||
call = {
|
||||
"org.freedesktop.portal.*" = "*";
|
||||
};
|
||||
broadcast = {
|
||||
"org.freedesktop.portal.*" = "@/org/freedesktop/portal/*";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
packages = with pkgs; [
|
||||
weechat
|
||||
claws-mail
|
||||
vesktop
|
||||
chromium
|
||||
];
|
||||
persistence.directories = [
|
||||
".config/weechat"
|
||||
".claws-mail"
|
||||
".config/vesktop"
|
||||
];
|
||||
extraConfig = {
|
||||
programs.looking-glass-client.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
* `enable` determines whether the module should be enabled or not. Useful when sharing configurations between graphical
|
||||
and headless systems. Defaults to `false`.
|
||||
|
||||
* `user` specifies the privileged user with access to fortified applications.
|
||||
|
||||
* `shell` is the shell used to run the launch command, required for sourcing the home-manager environment.
|
||||
|
||||
* `stateDir` is the path to your persistent storage location. It is directly passed through to the impermanence module.
|
||||
|
||||
* `target` is an attribute set of submodules, where the attribute name is the username of the unprivileged target user.
|
||||
|
||||
The available options are:
|
||||
|
||||
* `packages`, the list of packages to make available in the target user's environment.
|
||||
|
||||
* `persistence`, user persistence attribute set passed to impermanence.
|
||||
|
||||
* `extraConfig`, extra home-manager configuration for the target user.
|
||||
|
||||
* `launchers`, attribute set where the attribute name is the name of the launcher.
|
||||
|
||||
The available options are:
|
||||
|
||||
* `command`, the command to run as the target user. Defaults to launcher name.
|
||||
|
||||
* `dbus.config`, D-Bus proxy custom configuration.
|
||||
|
||||
* `dbus.configSystem`, D-Bus system bus custom configuration, null to disable.
|
||||
|
||||
* `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.
|
||||
|
||||
* `share`, package containing desktop/icon files. Defaults to launcher name.
|
||||
|
||||
* `method`, the launch method for the sandboxed program, can be `"fortify"`, `"fortify-sudo"`, `"sudo"`.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package acl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -13,88 +13,11 @@ import (
|
|||
//#cgo linux LDFLAGS: -lacl
|
||||
import "C"
|
||||
|
||||
const (
|
||||
aclRead = C.ACL_READ
|
||||
aclWrite = C.ACL_WRITE
|
||||
aclExecute = C.ACL_EXECUTE
|
||||
|
||||
aclTypeDefault = C.ACL_TYPE_DEFAULT
|
||||
aclTypeAccess = C.ACL_TYPE_ACCESS
|
||||
|
||||
aclUndefinedTag = C.ACL_UNDEFINED_TAG
|
||||
aclUserObj = C.ACL_USER_OBJ
|
||||
aclUser = C.ACL_USER
|
||||
aclGroupObj = C.ACL_GROUP_OBJ
|
||||
aclGroup = C.ACL_GROUP
|
||||
aclMask = C.ACL_MASK
|
||||
aclOther = C.ACL_OTHER
|
||||
)
|
||||
|
||||
type acl struct {
|
||||
val C.acl_t
|
||||
freed bool
|
||||
}
|
||||
|
||||
func aclUpdatePerm(path string, uid int, perms ...C.acl_perm_t) error {
|
||||
// read acl from file
|
||||
a, err := aclGetFile(path, aclTypeAccess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// free acl on return if get is successful
|
||||
defer a.free()
|
||||
|
||||
// remove existing entry
|
||||
if err = a.removeEntry(aclUser, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new entry if perms are passed
|
||||
if len(perms) > 0 {
|
||||
// create new acl entry
|
||||
var e C.acl_entry_t
|
||||
if _, err = C.acl_create_entry(&a.val, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get perm set of new entry
|
||||
var p C.acl_permset_t
|
||||
if _, err = C.acl_get_permset(e, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add target perms
|
||||
for _, perm := range perms {
|
||||
if _, err = C.acl_add_perm(p, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set perm set to new entry
|
||||
if _, err = C.acl_set_permset(e, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set user tag to new entry
|
||||
if _, err = C.acl_set_tag_type(e, aclUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set qualifier (uid) to new entry
|
||||
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// calculate mask after update
|
||||
if _, err = C.acl_calc_mask(&a.val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write acl to file
|
||||
return a.setFile(path, aclTypeAccess)
|
||||
}
|
||||
|
||||
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
|
||||
p := C.CString(path)
|
||||
a, err := C.acl_get_file(p, t)
|
|
@ -0,0 +1,86 @@
|
|||
package acl
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <sys/acl.h>
|
||||
//#include <acl/libacl.h>
|
||||
//#cgo linux LDFLAGS: -lacl
|
||||
import "C"
|
||||
|
||||
const (
|
||||
Read = C.ACL_READ
|
||||
Write = C.ACL_WRITE
|
||||
Execute = C.ACL_EXECUTE
|
||||
|
||||
TypeDefault = C.ACL_TYPE_DEFAULT
|
||||
TypeAccess = C.ACL_TYPE_ACCESS
|
||||
|
||||
UndefinedTag = C.ACL_UNDEFINED_TAG
|
||||
UserObj = C.ACL_USER_OBJ
|
||||
User = C.ACL_USER
|
||||
GroupObj = C.ACL_GROUP_OBJ
|
||||
Group = C.ACL_GROUP
|
||||
Mask = C.ACL_MASK
|
||||
Other = C.ACL_OTHER
|
||||
)
|
||||
|
||||
func UpdatePerm(path string, uid int, perms ...C.acl_perm_t) error {
|
||||
// read acl from file
|
||||
a, err := aclGetFile(path, TypeAccess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// free acl on return if get is successful
|
||||
defer a.free()
|
||||
|
||||
// remove existing entry
|
||||
if err = a.removeEntry(User, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new entry if perms are passed
|
||||
if len(perms) > 0 {
|
||||
// create new acl entry
|
||||
var e C.acl_entry_t
|
||||
if _, err = C.acl_create_entry(&a.val, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get perm set of new entry
|
||||
var p C.acl_permset_t
|
||||
if _, err = C.acl_get_permset(e, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add target perms
|
||||
for _, perm := range perms {
|
||||
if _, err = C.acl_add_perm(p, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set perm set to new entry
|
||||
if _, err = C.acl_set_permset(e, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set user tag to new entry
|
||||
if _, err = C.acl_set_tag_type(e, User); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set qualifier (uid) to new entry
|
||||
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// calculate mask after update
|
||||
if _, err = C.acl_calc_mask(&a.val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write acl to file
|
||||
return a.setFile(path, TypeAccess)
|
||||
}
|
50
cli.go
50
cli.go
|
@ -1,50 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
)
|
||||
|
||||
var (
|
||||
userName string
|
||||
methodFlags [2]bool
|
||||
printVersion bool
|
||||
mustPulse bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&userName, "u", "ego", "Specify a username")
|
||||
flag.BoolVar(&methodFlags[0], "sudo", false, "Use 'sudo' to change user")
|
||||
flag.BoolVar(&methodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup")
|
||||
flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal")
|
||||
flag.BoolVar(&verbose, "v", false, "Verbose output")
|
||||
flag.BoolVar(&printVersion, "V", false, "Print version")
|
||||
}
|
||||
|
||||
func copyArgs() {
|
||||
tryLauncher()
|
||||
tryVersion()
|
||||
tryLicense()
|
||||
|
||||
command = flag.Args()
|
||||
|
||||
if u, err := user.Lookup(userName); err != nil {
|
||||
if errors.As(err, new(user.UnknownUserError)) {
|
||||
fmt.Println("unknown user", userName)
|
||||
} else {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
} else {
|
||||
ego = u
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Running as user", ego.Username, "("+ego.Uid+"),", "command:", command)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package dbus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// See set 'see' policy for NAME (--see=NAME)
|
||||
See []string `json:"see"`
|
||||
// Talk set 'talk' policy for NAME (--talk=NAME)
|
||||
Talk []string `json:"talk"`
|
||||
// Own set 'own' policy for NAME (--own=NAME)
|
||||
Own []string `json:"own"`
|
||||
|
||||
// Call set RULE for calls on NAME (--call=NAME=RULE)
|
||||
Call map[string]string `json:"call"`
|
||||
// Broadcast set RULE for broadcasts from NAME (--broadcast=NAME=RULE)
|
||||
Broadcast map[string]string `json:"broadcast"`
|
||||
|
||||
Log bool `json:"log,omitempty"`
|
||||
Filter bool `json:"filter"`
|
||||
}
|
||||
|
||||
func (c *Config) Args(bus [2]string) (args []string) {
|
||||
argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast)
|
||||
if c.Log {
|
||||
argc++
|
||||
}
|
||||
if c.Filter {
|
||||
argc++
|
||||
}
|
||||
|
||||
args = make([]string, 0, argc)
|
||||
args = append(args, bus[0], bus[1])
|
||||
if c.Filter {
|
||||
args = append(args, "--filter")
|
||||
}
|
||||
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)
|
||||
}
|
||||
for name, rule := range c.Call {
|
||||
args = append(args, "--call="+name+"="+rule)
|
||||
}
|
||||
for name, rule := range c.Broadcast {
|
||||
args = append(args, "--broadcast="+name+"="+rule)
|
||||
}
|
||||
if c.Log {
|
||||
args = append(args, "--log")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) buildSeal(seal *strings.Builder, bus [2]string) error {
|
||||
for _, arg := range c.Args(bus) {
|
||||
// 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')
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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{
|
||||
Call: make(map[string]string),
|
||||
Broadcast: make(map[string]string),
|
||||
|
||||
Filter: true,
|
||||
}
|
||||
|
||||
if defaults {
|
||||
c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}
|
||||
|
||||
c.Call["org.freedesktop.portal.*"] = "*"
|
||||
c.Broadcast["org.freedesktop.portal.*"] = "@/org/freedesktop/portal/*"
|
||||
|
||||
if id != "" {
|
||||
c.Own = []string{id + ".*"}
|
||||
if mpris {
|
||||
c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id+".*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
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()
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
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
|
||||
|
||||
path string
|
||||
session [2]string
|
||||
system [2]string
|
||||
|
||||
wait *chan error
|
||||
read *chan error
|
||||
ready *chan bool
|
||||
|
||||
seal *string
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *Proxy) String() string {
|
||||
if p == nil {
|
||||
return "(invalid dbus proxy)"
|
||||
}
|
||||
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
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(session, system *Config) error {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if p.seal != nil {
|
||||
panic("dbus proxy sealed twice")
|
||||
}
|
||||
|
||||
if session == nil && system == nil {
|
||||
return errors.New("no configuration to seal")
|
||||
}
|
||||
|
||||
seal := strings.Builder{}
|
||||
|
||||
if session != nil {
|
||||
if err := session.buildSeal(&seal, p.session); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if system != nil {
|
||||
if err := system.buildSeal(&seal, p.system); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
v := seal.String()
|
||||
p.seal = &v
|
||||
return nil
|
||||
}
|
||||
|
||||
// New returns a reference to a new unsealed Proxy.
|
||||
func New(binPath string, session, system [2]string) *Proxy {
|
||||
return &Proxy{path: binPath, session: session, system: system}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
userName string
|
||||
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
dbusVerbose bool
|
||||
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(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
|
||||
flag.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
|
||||
flag.BoolVar(&dbusVerbose, "dbus-log", false, "Enable logging in the D-Bus proxy")
|
||||
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(&flagVerbose, "v", false, "Verbose output")
|
||||
flag.BoolVar(&printVersion, "V", false, "Print version")
|
||||
}
|
||||
|
||||
func init() {
|
||||
methodHelpString := "Method of launching the child process, can be one of \"sudo\", \"bubblewrap\""
|
||||
if internal.SdBootedV {
|
||||
methodHelpString += ", \"systemd\""
|
||||
}
|
||||
|
||||
flag.StringVar(&launchOptionText, "method", "sudo", methodHelpString)
|
||||
}
|
|
@ -2,16 +2,16 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1717179513,
|
||||
"narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=",
|
||||
"lastModified": 1725361206,
|
||||
"narHash": "sha256-/HTUg+kMaqBPGrcQBYboAMsQHIWIkuKRDldss/035Hc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
|
||||
"rev": "2830c7c930311397d94c0b86a359c865c081c875",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "24.05",
|
||||
"ref": "nixos-unstable-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
65
flake.nix
65
flake.nix
|
@ -1,56 +1,47 @@
|
|||
{
|
||||
description = "ego development environment";
|
||||
description = "fortify sandbox tool and nixos module";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/24.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" ];
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
|
||||
supportedSystems = [
|
||||
"aarch64-linux"
|
||||
"i686-linux"
|
||||
"x86_64-linux"
|
||||
];
|
||||
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (
|
||||
nixosModules.fortify = import ./nixos.nix;
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
pkgs = nixpkgsFor.${system};
|
||||
in
|
||||
{
|
||||
default =
|
||||
let
|
||||
inherit (pkgs)
|
||||
mkShell
|
||||
buildGoModule
|
||||
acl
|
||||
xorg
|
||||
;
|
||||
in
|
||||
mkShell {
|
||||
packages = [
|
||||
(buildGoModule rec {
|
||||
pname = "ego";
|
||||
version = "0.0.0-flake";
|
||||
default = self.packages.${system}.fortify;
|
||||
|
||||
src = ./.;
|
||||
vendorHash = null; # we have no dependencies :3
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X"
|
||||
"main.Version=v${version}"
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
acl
|
||||
xorg.libxcb
|
||||
];
|
||||
})
|
||||
];
|
||||
};
|
||||
fortify = pkgs.callPackage ./package.nix { };
|
||||
}
|
||||
);
|
||||
|
||||
devShells = forAllSystems (system: {
|
||||
default = nixpkgsFor.${system}.mkShell {
|
||||
buildInputs =
|
||||
with nixpkgsFor.${system};
|
||||
[ self.packages.${system}.fortify ] ++ self.packages.${system}.fortify.buildInputs;
|
||||
|
||||
shellHook = ''
|
||||
which fortify
|
||||
'';
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
|||
module git.ophivana.moe/cat/ego
|
||||
module git.ophivana.moe/cat/fortify
|
||||
|
||||
go 1.22
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package app
|
||||
|
||||
func (a *App) Command() []string {
|
||||
return a.command
|
||||
}
|
||||
|
||||
func (a *App) UID() int {
|
||||
return a.uid
|
||||
}
|
||||
|
||||
func (a *App) AppendEnv(k, v string) {
|
||||
a.env = append(a.env, k+"="+v)
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
||||
)
|
||||
|
||||
var (
|
||||
dbusAddress [2]string
|
||||
dbusSystem bool
|
||||
)
|
||||
|
||||
func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) {
|
||||
a.setEnablement(internal.EnableDBus)
|
||||
|
||||
dbusSystem = dsg != nil
|
||||
var binPath string
|
||||
var sessionBus, systemBus [2]string
|
||||
|
||||
target := path.Join(a.sharePath, strconv.Itoa(os.Getpid()))
|
||||
sessionBus[1] = target + ".bus"
|
||||
systemBus[1] = target + ".system-bus"
|
||||
dbusAddress = [2]string{
|
||||
"unix:path=" + sessionBus[1],
|
||||
"unix:path=" + systemBus[1],
|
||||
}
|
||||
|
||||
if b, ok := util.Which("xdg-dbus-proxy"); !ok {
|
||||
internal.Fatal("D-Bus: Did not find 'xdg-dbus-proxy' in PATH")
|
||||
} else {
|
||||
binPath = b
|
||||
}
|
||||
|
||||
if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
|
||||
verbose.Println("D-Bus: DBUS_SESSION_BUS_ADDRESS not set, assuming default format")
|
||||
sessionBus[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
|
||||
} else {
|
||||
sessionBus[0] = addr
|
||||
}
|
||||
|
||||
if addr, ok := os.LookupEnv(dbusSystemBusAddress); !ok {
|
||||
verbose.Println("D-Bus: DBUS_SYSTEM_BUS_ADDRESS not set, assuming default format")
|
||||
systemBus[0] = "unix:path=/run/dbus/system_bus_socket"
|
||||
} else {
|
||||
systemBus[0] = addr
|
||||
}
|
||||
|
||||
p := dbus.New(binPath, sessionBus, systemBus)
|
||||
|
||||
dse.Log = log
|
||||
verbose.Println("D-Bus: sealing session proxy", dse.Args(sessionBus))
|
||||
if dsg != nil {
|
||||
dsg.Log = log
|
||||
verbose.Println("D-Bus: sealing system proxy", dsg.Args(systemBus))
|
||||
}
|
||||
if err := p.Seal(dse, dsg); err != nil {
|
||||
internal.Fatal("D-Bus: invalid config when sealing proxy,", err)
|
||||
}
|
||||
|
||||
ready := make(chan bool, 1)
|
||||
done := make(chan struct{})
|
||||
|
||||
verbose.Printf("Starting session bus proxy '%s' for address '%s'\n", dbusAddress[0], sessionBus[0])
|
||||
if dsg != nil {
|
||||
verbose.Printf("Starting system bus proxy '%s' for address '%s'\n", dbusAddress[1], systemBus[0])
|
||||
}
|
||||
if err := p.Start(&ready); err != nil {
|
||||
internal.Fatal("D-Bus: error starting proxy,", err)
|
||||
}
|
||||
verbose.Println("D-Bus proxy launch:", p)
|
||||
|
||||
go func() {
|
||||
if err := p.Wait(); err != nil {
|
||||
fmt.Println("warn: D-Bus proxy returned error,", err)
|
||||
} else {
|
||||
verbose.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
|
||||
a.exit.SealDBus(p, &done)
|
||||
|
||||
if !<-ready {
|
||||
internal.Fatal("D-Bus: proxy did not start correctly")
|
||||
}
|
||||
|
||||
a.AppendEnv(dbusSessionBusAddress, dbusAddress[0])
|
||||
if err := acl.UpdatePerm(sessionBus[1], a.UID(), acl.Read, acl.Write); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error preparing D-Bus session proxy '%s':", dbusAddress[0]), err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(sessionBus[1])
|
||||
}
|
||||
if dsg != nil {
|
||||
a.AppendEnv(dbusSystemBusAddress, dbusAddress[1])
|
||||
if err := acl.UpdatePerm(systemBus[1], a.UID(), acl.Read, acl.Write); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error preparing D-Bus system proxy '%s':", dbusAddress[1]), err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(systemBus[1])
|
||||
}
|
||||
}
|
||||
verbose.Printf("Session bus proxy '%s' for address '%s' configured\n", dbusAddress[0], sessionBus[0])
|
||||
if dsg != nil {
|
||||
verbose.Printf("System bus proxy '%s' for address '%s' configured\n", dbusAddress[1], systemBus[0])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
func (a *App) EnsureRunDir() {
|
||||
if err := os.Mkdir(a.runDirPath, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
internal.Fatal("Error creating runtime directory:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) EnsureRuntime() {
|
||||
if s, err := os.Stat(a.runtimePath); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Runtime directory does not exist")
|
||||
}
|
||||
internal.Fatal("Error accessing runtime directory:", err)
|
||||
} else if !s.IsDir() {
|
||||
internal.Fatal(fmt.Sprintf("Path '%s' is not a directory", a.runtimePath))
|
||||
} else {
|
||||
if err = acl.UpdatePerm(a.runtimePath, a.UID(), acl.Execute); err != nil {
|
||||
internal.Fatal("Error preparing runtime directory:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(a.runtimePath)
|
||||
}
|
||||
verbose.Printf("Runtime data dir '%s' configured\n", a.runtimePath)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) EnsureShare() {
|
||||
// acl is unnecessary as this directory is world executable
|
||||
if err := os.Mkdir(a.sharePath, 0701); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
internal.Fatal("Error creating shared directory:", err)
|
||||
}
|
||||
|
||||
// workaround for launch method sudo
|
||||
if a.LaunchOption() == LaunchMethodSudo {
|
||||
// ensure child runtime directory (e.g. `/tmp/fortify.%d/%d.share`)
|
||||
cr := path.Join(a.sharePath, a.Uid+".share")
|
||||
if err := os.Mkdir(cr, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
internal.Fatal("Error creating child runtime directory:", err)
|
||||
} else {
|
||||
if err = acl.UpdatePerm(cr, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
|
||||
internal.Fatal("Error preparing child runtime directory:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(cr)
|
||||
}
|
||||
a.AppendEnv("XDG_RUNTIME_DIR", cr)
|
||||
a.AppendEnv("XDG_SESSION_CLASS", "user")
|
||||
a.AppendEnv("XDG_SESSION_TYPE", "tty")
|
||||
verbose.Printf("Child runtime data dir '%s' configured\n", cr)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -8,18 +8,29 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// hidden path for main to act as a launcher
|
||||
egoLauncher = "EGO_LAUNCHER"
|
||||
)
|
||||
const launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD"
|
||||
|
||||
// hidden launcher path
|
||||
func tryLauncher() {
|
||||
func (a *App) launcherPayloadEnv() string {
|
||||
r := &bytes.Buffer{}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, r)
|
||||
|
||||
if err := gob.NewEncoder(enc).Encode(a.command); err != nil {
|
||||
internal.Fatal("Error encoding launcher payload:", err)
|
||||
}
|
||||
|
||||
_ = enc.Close()
|
||||
return launcherPayload + "=" + r.String()
|
||||
}
|
||||
|
||||
// Early hidden launcher path
|
||||
func Early(printVersion bool) {
|
||||
if printVersion {
|
||||
if r, ok := os.LookupEnv(egoLauncher); ok {
|
||||
// egoLauncher variable contains launcher payload
|
||||
if r, ok := os.LookupEnv(launcherPayload); ok {
|
||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
|
||||
|
||||
var argv []string
|
||||
|
@ -28,7 +39,7 @@ func tryLauncher() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.Unsetenv(egoLauncher); err != nil {
|
||||
if err := os.Unsetenv(launcherPayload); err != nil {
|
||||
fmt.Println("Error unsetting launcher payload:", err)
|
||||
// not fatal, do not fail
|
||||
}
|
||||
|
@ -36,7 +47,7 @@ func tryLauncher() {
|
|||
var p string
|
||||
|
||||
if len(argv) > 0 {
|
||||
if p, ok = which(argv[0]); !ok {
|
||||
if p, ok = util.Which(argv[0]); !ok {
|
||||
fmt.Printf("Did not find '%s' in PATH\n", argv[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
@ -45,6 +56,8 @@ func tryLauncher() {
|
|||
fmt.Println("No command was specified and $SHELL was unset")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
argv = []string{p}
|
||||
}
|
||||
|
||||
if err := syscall.Exec(p, argv, os.Environ()); err != nil {
|
||||
|
@ -58,15 +71,3 @@ func tryLauncher() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func launcherPayloadEnv() string {
|
||||
r := &bytes.Buffer{}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, r)
|
||||
|
||||
if err := gob.NewEncoder(enc).Encode(command); err != nil {
|
||||
fatal("Error encoding launcher payload:", err)
|
||||
}
|
||||
|
||||
_ = enc.Close()
|
||||
return egoLauncher + "=" + r.String()
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
pulseServer = "PULSE_SERVER"
|
||||
pulseCookie = "PULSE_COOKIE"
|
||||
|
||||
home = "HOME"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
)
|
||||
|
||||
func (a *App) SharePulse() {
|
||||
a.setEnablement(internal.EnablePulse)
|
||||
|
||||
// ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
|
||||
pulse := path.Join(a.runtimePath, "pulse")
|
||||
pulseS := path.Join(pulse, "native")
|
||||
if s, err := os.Stat(pulse); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Error accessing PulseAudio directory:", err)
|
||||
}
|
||||
internal.Fatal(fmt.Sprintf("PulseAudio dir '%s' not found", pulse))
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
a.AppendEnv(pulseServer, "unix:"+pulseS)
|
||||
if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
|
||||
internal.Fatal("Error preparing PulseAudio:", err)
|
||||
} else {
|
||||
a.exit.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) {
|
||||
internal.Fatal("PulseAudio directory found but socket does not exist")
|
||||
}
|
||||
internal.Fatal("Error accessing PulseAudio socket:", err)
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
internal.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish current user's pulse-cookie for target user
|
||||
pulseCookieSource := discoverPulseCookie()
|
||||
pulseCookieFinal := path.Join(a.sharePath, "pulse-cookie")
|
||||
a.AppendEnv(pulseCookie, pulseCookieFinal)
|
||||
verbose.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
||||
if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
|
||||
internal.Fatal("Error copying PulseAudio cookie:", err)
|
||||
}
|
||||
if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
|
||||
internal.Fatal("Error publishing PulseAudio cookie:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(pulseCookieFinal)
|
||||
}
|
||||
|
||||
verbose.Printf("PulseAudio dir '%s' configured\n", pulse)
|
||||
}
|
||||
}
|
||||
|
||||
// discoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie
|
||||
func discoverPulseCookie() string {
|
||||
if p, ok := os.LookupEnv(pulseCookie); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(home); ok {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
internal.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
pulseCookie, xdgConfigHome, home))
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
term = "TERM"
|
||||
sudoAskPass = "SUDO_ASKPASS"
|
||||
)
|
||||
const (
|
||||
LaunchMethodSudo uint8 = iota
|
||||
LaunchMethodBwrap
|
||||
LaunchMethodMachineCtl
|
||||
)
|
||||
|
||||
func (a *App) Run() {
|
||||
// pass $TERM to launcher
|
||||
if t, ok := os.LookupEnv(term); ok {
|
||||
a.AppendEnv(term, t)
|
||||
}
|
||||
|
||||
var commandBuilder func() (args []string)
|
||||
|
||||
switch a.launchOption {
|
||||
case LaunchMethodSudo:
|
||||
commandBuilder = a.commandBuilderSudo
|
||||
case LaunchMethodBwrap:
|
||||
commandBuilder = a.commandBuilderBwrap
|
||||
case LaunchMethodMachineCtl:
|
||||
commandBuilder = a.commandBuilderMachineCtl
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
cmd := exec.Command(a.toolPath, commandBuilder()...)
|
||||
cmd.Env = []string{}
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Dir = a.runDirPath
|
||||
|
||||
verbose.Println("Executing:", cmd)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
internal.Fatal("Error starting process:", err)
|
||||
}
|
||||
|
||||
a.exit.SealEnablements(a.enablements)
|
||||
|
||||
if statePath, err := state.SaveProcess(a.Uid, cmd, a.runDirPath, a.command, a.enablements); err != nil {
|
||||
// process already started, shouldn't be fatal
|
||||
fmt.Println("Error registering process:", err)
|
||||
} else {
|
||||
a.exit.SealStatePath(statePath)
|
||||
}
|
||||
|
||||
var r int
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
internal.Fatal("Error running process:", err)
|
||||
}
|
||||
}
|
||||
|
||||
verbose.Println("Process exited with exit code", r)
|
||||
internal.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 {
|
||||
verbose.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) commandBuilderBwrap() (args []string) {
|
||||
// TODO: build bwrap command
|
||||
internal.Fatal("bwrap")
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
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 !verbose.Get() {
|
||||
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 {
|
||||
internal.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 {
|
||||
internal.Fatal("Error reading executable path:", err)
|
||||
} else {
|
||||
if a.enablements.Has(internal.EnableDBus) {
|
||||
innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + dbusAddress[0] + "' ")
|
||||
if dbusSystem {
|
||||
innerCommand.WriteString(dbusSystemBusAddress + "=" + "'" + dbusAddress[1] + "' ")
|
||||
}
|
||||
}
|
||||
innerCommand.WriteString("exec " + executable + " -V")
|
||||
}
|
||||
args = append(args, innerCommand.String())
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
uid int // assigned
|
||||
env []string // modified via AppendEnv
|
||||
command []string // set on initialisation
|
||||
|
||||
exit *internal.ExitState // assigned
|
||||
|
||||
launchOptionText string // set on initialisation
|
||||
launchOption uint8 // assigned
|
||||
|
||||
sharePath string // set on initialisation
|
||||
runtimePath string // assigned
|
||||
runDirPath string // assigned
|
||||
toolPath string // assigned
|
||||
|
||||
enablements internal.Enablements // set via setEnablement
|
||||
*user.User // assigned
|
||||
|
||||
// absolutely *no* method of this type is thread-safe
|
||||
// so don't treat it as if it is
|
||||
}
|
||||
|
||||
func (a *App) LaunchOption() uint8 {
|
||||
return a.launchOption
|
||||
}
|
||||
|
||||
func (a *App) RunDir() string {
|
||||
return a.runDirPath
|
||||
}
|
||||
|
||||
func (a *App) setEnablement(e internal.Enablement) {
|
||||
if a.enablements.Has(e) {
|
||||
panic("enablement " + e.String() + " set twice")
|
||||
}
|
||||
|
||||
a.enablements |= e.Mask()
|
||||
}
|
||||
|
||||
func (a *App) SealExit(exit *internal.ExitState) {
|
||||
if a.exit != nil {
|
||||
panic("application exit state sealed twice")
|
||||
}
|
||||
a.exit = exit
|
||||
}
|
||||
|
||||
func New(userName string, args []string, launchOptionText string) *App {
|
||||
a := &App{
|
||||
command: args,
|
||||
launchOptionText: launchOptionText,
|
||||
sharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())),
|
||||
}
|
||||
|
||||
// runtimePath, runDirPath
|
||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
||||
fmt.Println("Env variable", xdgRuntimeDir, "unset")
|
||||
|
||||
// too early for fatal
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.runtimePath = r
|
||||
a.runDirPath = path.Join(a.runtimePath, "fortify")
|
||||
verbose.Println("Runtime directory at", a.runDirPath)
|
||||
}
|
||||
|
||||
// *user.User
|
||||
if u, err := user.Lookup(userName); err != nil {
|
||||
if errors.As(err, new(user.UnknownUserError)) {
|
||||
fmt.Println("unknown user", userName)
|
||||
} else {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// too early for fatal
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.User = u
|
||||
}
|
||||
|
||||
// uid
|
||||
if u, err := strconv.Atoi(a.Uid); err != nil {
|
||||
// usually unreachable
|
||||
panic("uid parse")
|
||||
} else {
|
||||
a.uid = u
|
||||
}
|
||||
|
||||
verbose.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command)
|
||||
if internal.SdBootedV {
|
||||
verbose.Println("System booted with systemd as init system (PID 1).")
|
||||
}
|
||||
|
||||
// launchOption, toolPath
|
||||
switch a.launchOptionText {
|
||||
case "sudo":
|
||||
a.launchOption = LaunchMethodSudo
|
||||
if sudoPath, ok := util.Which("sudo"); !ok {
|
||||
fmt.Println("Did not find 'sudo' in PATH")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.toolPath = sudoPath
|
||||
}
|
||||
case "bubblewrap":
|
||||
a.launchOption = LaunchMethodBwrap
|
||||
if bwrapPath, ok := util.Which("bwrap"); !ok {
|
||||
fmt.Println("Did not find 'bwrap' in PATH")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.toolPath = bwrapPath
|
||||
}
|
||||
case "systemd":
|
||||
a.launchOption = LaunchMethodMachineCtl
|
||||
if !internal.SdBootedV {
|
||||
fmt.Println("System has not been booted with systemd as init system (PID 1).")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if machineCtlPath, ok := util.Which("machinectl"); !ok {
|
||||
fmt.Println("Did not find 'machinectl' in PATH")
|
||||
} else {
|
||||
a.toolPath = machineCtlPath
|
||||
}
|
||||
default:
|
||||
fmt.Println("invalid launch method")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
verbose.Println("Determined launch method to be", a.launchOptionText, "with tool at", a.toolPath)
|
||||
|
||||
return a
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||
waylandDisplay = "WAYLAND_DISPLAY"
|
||||
)
|
||||
|
||||
func (a *App) ShareWayland() {
|
||||
a.setEnablement(internal.EnableWayland)
|
||||
|
||||
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
||||
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
||||
internal.Fatal("Wayland: WAYLAND_DISPLAY not set")
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
wp := path.Join(a.runtimePath, w)
|
||||
a.AppendEnv(waylandDisplay, wp)
|
||||
if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(wp)
|
||||
}
|
||||
verbose.Printf("Wayland socket '%s' configured\n", w)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
"git.ophivana.moe/cat/fortify/xcb"
|
||||
)
|
||||
|
||||
const display = "DISPLAY"
|
||||
|
||||
func (a *App) ShareX() {
|
||||
a.setEnablement(internal.EnableX)
|
||||
|
||||
// discovery X11 and grant user permission via the `ChangeHosts` command
|
||||
if d, ok := os.LookupEnv(display); !ok {
|
||||
internal.Fatal("X11: DISPLAY not set")
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
a.AppendEnv(display, d)
|
||||
|
||||
verbose.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 {
|
||||
internal.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
||||
} else {
|
||||
a.exit.XcbActionComplete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
)
|
||||
|
||||
var SdBootedV = func() bool {
|
||||
if v, err := util.SdBooted(); err != nil {
|
||||
fmt.Println("warn: read systemd marker:", err)
|
||||
return false
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}()
|
|
@ -0,0 +1,34 @@
|
|||
package internal
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
"git.ophivana.moe/cat/fortify/xcb"
|
||||
)
|
||||
|
||||
// ExitState keeps track of various changes fortify made to the system
|
||||
// as well as other resources that need to be manually released.
|
||||
// NOT thread safe.
|
||||
type ExitState struct {
|
||||
// target fortified user inherited from app.App
|
||||
user *user.User
|
||||
// integer UID of targeted user
|
||||
uid int
|
||||
// returns amount of launcher states read
|
||||
launcherStateCount func() (int, error)
|
||||
|
||||
// paths to strip ACLs (of target user) from
|
||||
aclCleanupCandidate []string
|
||||
// target process capability enablements
|
||||
enablements *Enablements
|
||||
// whether the xcb.ChangeHosts action was complete
|
||||
xcbActionComplete bool
|
||||
|
||||
// reference to D-Bus proxy instance, nil if disabled
|
||||
dbusProxy *dbus.Proxy
|
||||
// D-Bus wait complete notification
|
||||
dbusDone *chan struct{}
|
||||
|
||||
// path to fortify process state information
|
||||
statePath string
|
||||
|
||||
// prevents cleanup from happening twice
|
||||
complete bool
|
||||
}
|
||||
|
||||
// RegisterRevertPath registers a path with ACLs added by fortify
|
||||
func (s *ExitState) RegisterRevertPath(p string) {
|
||||
s.aclCleanupCandidate = append(s.aclCleanupCandidate, p)
|
||||
}
|
||||
|
||||
// SealEnablements submits the child process enablements
|
||||
func (s *ExitState) SealEnablements(e Enablements) {
|
||||
if s.enablements != nil {
|
||||
panic("enablement exit state set twice")
|
||||
}
|
||||
s.enablements = &e
|
||||
}
|
||||
|
||||
// XcbActionComplete submits xcb.ChangeHosts action completion
|
||||
func (s *ExitState) XcbActionComplete() {
|
||||
if s.xcbActionComplete {
|
||||
Fatal("xcb inserted twice")
|
||||
}
|
||||
s.xcbActionComplete = true
|
||||
}
|
||||
|
||||
// SealDBus submits the child's D-Bus proxy instance
|
||||
func (s *ExitState) SealDBus(p *dbus.Proxy, done *chan struct{}) {
|
||||
if p == nil {
|
||||
Fatal("unexpected nil dbus proxy exit state submitted")
|
||||
}
|
||||
if s.dbusProxy != nil {
|
||||
Fatal("dbus proxy exit state set twice")
|
||||
}
|
||||
s.dbusProxy = p
|
||||
s.dbusDone = done
|
||||
}
|
||||
|
||||
// SealStatePath submits filesystem path to the fortify process's state file
|
||||
func (s *ExitState) SealStatePath(v string) {
|
||||
if s.statePath != "" {
|
||||
panic("statePath set twice")
|
||||
}
|
||||
|
||||
s.statePath = v
|
||||
}
|
||||
|
||||
// NewExit initialises a new ExitState containing basic, unchanging information
|
||||
// about the fortify process required during cleanup
|
||||
func NewExit(u *user.User, uid int, f func() (int, error)) *ExitState {
|
||||
return &ExitState{
|
||||
uid: uid,
|
||||
user: u,
|
||||
|
||||
launcherStateCount: f,
|
||||
}
|
||||
}
|
||||
|
||||
func Fatal(msg ...any) {
|
||||
fmt.Println(msg...)
|
||||
BeforeExit()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var exitState *ExitState
|
||||
|
||||
func SealExit(s *ExitState) {
|
||||
if exitState != nil {
|
||||
panic("exit state submitted twice")
|
||||
}
|
||||
|
||||
exitState = s
|
||||
}
|
||||
|
||||
func BeforeExit() {
|
||||
if exitState == nil {
|
||||
fmt.Println("warn: cleanup attempted before exit state submission")
|
||||
return
|
||||
}
|
||||
|
||||
exitState.beforeExit()
|
||||
}
|
||||
|
||||
func (s *ExitState) beforeExit() {
|
||||
if s.complete {
|
||||
panic("beforeExit called twice")
|
||||
}
|
||||
|
||||
if s.statePath == "" {
|
||||
verbose.Println("State path is unset")
|
||||
} else {
|
||||
if err := os.Remove(s.statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
fmt.Println("Error removing state file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if count, err := s.launcherStateCount(); err != nil {
|
||||
fmt.Println("Error reading active launchers:", err)
|
||||
os.Exit(1)
|
||||
} else if count > 0 {
|
||||
// other launchers are still active
|
||||
verbose.Printf("Found %d active launchers, exiting without cleaning up\n", count)
|
||||
return
|
||||
}
|
||||
|
||||
verbose.Println("No other launchers active, will clean up")
|
||||
|
||||
if s.xcbActionComplete {
|
||||
verbose.Printf("X11: Removing XHost entry SI:localuser:%s\n", s.user.Username)
|
||||
if err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+s.user.Username); err != nil {
|
||||
fmt.Println("Error removing XHost entry:", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range s.aclCleanupCandidate {
|
||||
if err := acl.UpdatePerm(candidate, s.uid); err != nil {
|
||||
fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
|
||||
}
|
||||
verbose.Printf("Stripped ACL entry for user '%s' from '%s'\n", s.user.Username, candidate)
|
||||
}
|
||||
|
||||
if s.dbusProxy != nil {
|
||||
verbose.Println("D-Bus proxy registered, cleaning up")
|
||||
|
||||
if err := s.dbusProxy.Close(); err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
verbose.Println("D-Bus proxy already closed")
|
||||
} else {
|
||||
fmt.Println("Error closing D-Bus proxy:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// wait for Proxy.Wait to return
|
||||
<-*s.dbusDone
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
)
|
||||
|
||||
// we unfortunately have to assume there are never races between processes
|
||||
// this and launcher should eventually be replaced by a server process
|
||||
|
||||
type launcherState struct {
|
||||
PID int
|
||||
Launcher string
|
||||
Argv []string
|
||||
Command []string
|
||||
Capability internal.Enablements
|
||||
}
|
||||
|
||||
// ReadLaunchers reads all launcher state file entries for the requested user
|
||||
// and if decode is true decodes these launchers as well.
|
||||
func ReadLaunchers(runDirPath, uid string, decode bool) ([]*launcherState, error) {
|
||||
var f *os.File
|
||||
var r []*launcherState
|
||||
launcherPrefix := path.Join(runDirPath, uid)
|
||||
|
||||
if pl, err := os.ReadDir(launcherPrefix); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, e := range pl {
|
||||
if err = func() error {
|
||||
if f, err = os.Open(path.Join(launcherPrefix, e.Name())); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer func() {
|
||||
if f.Close() != nil {
|
||||
// unreachable
|
||||
panic("foreign state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
var s launcherState
|
||||
r = append(r, &s)
|
||||
if decode {
|
||||
return gob.NewDecoder(f).Decode(&s)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
func MustPrintLauncherStateGlobal(w **tabwriter.Writer, runDirPath string) {
|
||||
if dirs, err := os.ReadDir(runDirPath); err != nil {
|
||||
fmt.Println("Error reading runtime directory:", err)
|
||||
} else {
|
||||
for _, e := range dirs {
|
||||
if !e.IsDir() {
|
||||
verbose.Println("Skipped non-directory entry", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err = strconv.Atoi(e.Name()); err != nil {
|
||||
verbose.Println("Skipped non-uid entry", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
MustPrintLauncherState(w, runDirPath, e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MustPrintLauncherState(w **tabwriter.Writer, runDirPath, uid string) {
|
||||
launchers, err := ReadLaunchers(runDirPath, uid, true)
|
||||
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 !verbose.Get() {
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tEnablements\tLauncher\tCommand")
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
|
||||
}
|
||||
}
|
||||
|
||||
for _, state := range launchers {
|
||||
enablementsDescription := strings.Builder{}
|
||||
for i := internal.Enablement(0); i < internal.EnableLength; i++ {
|
||||
if state.Capability.Has(i) {
|
||||
enablementsDescription.WriteString(", " + i.String())
|
||||
}
|
||||
}
|
||||
if enablementsDescription.Len() == 0 {
|
||||
enablementsDescription.WriteString("none")
|
||||
}
|
||||
|
||||
if !verbose.Get() {
|
||||
_, _ = 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
)
|
||||
|
||||
// SaveProcess called after process start, before wait
|
||||
func SaveProcess(uid string, cmd *exec.Cmd, runDirPath string, command []string, enablements internal.Enablements) (string, error) {
|
||||
statePath := path.Join(runDirPath, uid, strconv.Itoa(cmd.Process.Pid))
|
||||
state := launcherState{
|
||||
PID: cmd.Process.Pid,
|
||||
Launcher: cmd.Path,
|
||||
Argv: cmd.Args,
|
||||
Command: command,
|
||||
Capability: enablements,
|
||||
}
|
||||
|
||||
if err := os.Mkdir(path.Join(runDirPath, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return statePath, err
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
|
||||
return statePath, err
|
||||
} else {
|
||||
defer func() {
|
||||
if f.Close() != nil {
|
||||
// unreachable
|
||||
panic("state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
return statePath, gob.NewEncoder(f).Encode(state)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Which(file string) (string, bool) {
|
||||
p, err := exec.LookPath(file)
|
||||
return p, err == nil
|
||||
}
|
||||
|
||||
func CopyFile(dst, src string) error {
|
||||
srcD, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if srcD.Close() != nil {
|
||||
// unreachable
|
||||
panic("src file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if dstD.Close() != nil {
|
||||
// unreachable
|
||||
panic("dst file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(dstD, srcD)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
systemdCheckPath = "/run/systemd/system"
|
||||
)
|
||||
|
||||
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
|
||||
func SdBooted() (bool, error) {
|
||||
_, err := os.Stat(systemdCheckPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package verbose
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Println(a ...any) {
|
||||
if verbose.Load() {
|
||||
fmt.Println(a...)
|
||||
}
|
||||
}
|
||||
|
||||
func Printf(format string, a ...any) {
|
||||
if verbose.Load() {
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package verbose
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
var verbose = new(atomic.Bool)
|
||||
|
||||
func Get() bool {
|
||||
return verbose.Load()
|
||||
}
|
||||
|
||||
func Set(v bool) {
|
||||
verbose.Store(v)
|
||||
}
|
390
main.go
390
main.go
|
@ -1,20 +1,33 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/app"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
var Version = "impure"
|
||||
var (
|
||||
Version = "impure"
|
||||
|
||||
a *app.App
|
||||
s *internal.ExitState
|
||||
|
||||
dbusSession *dbus.Config
|
||||
dbusSystem *dbus.Config
|
||||
|
||||
launchOptionText string
|
||||
)
|
||||
|
||||
func tryVersion() {
|
||||
if printVersion {
|
||||
|
@ -23,341 +36,98 @@ 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"
|
||||
|
||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||
waylandDisplay = "WAYLAND_DISPLAY"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
copyArgs()
|
||||
verbose.Set(flagVerbose)
|
||||
|
||||
if u, err := strconv.Atoi(ego.Uid); err != nil {
|
||||
// usually unreachable
|
||||
panic("ego uid parse")
|
||||
// launcher payload early exit
|
||||
app.Early(printVersion)
|
||||
|
||||
// version/license command early exit
|
||||
tryVersion()
|
||||
tryLicense()
|
||||
|
||||
a = app.New(userName, flag.Args(), launchOptionText)
|
||||
s = internal.NewExit(a.User, a.UID(), func() (int, error) {
|
||||
d, err := state.ReadLaunchers(a.RunDir(), a.Uid, false)
|
||||
return len(d), err
|
||||
})
|
||||
a.SealExit(s)
|
||||
internal.SealExit(s)
|
||||
|
||||
// parse D-Bus config file if applicable
|
||||
if mustDBus {
|
||||
if dbusConfigSession == "builtin" {
|
||||
dbusSession = dbus.NewConfig(dbusID, true, mpris)
|
||||
} else {
|
||||
uid = u
|
||||
if f, err := os.Open(dbusConfigSession); err != nil {
|
||||
internal.Fatal("Error opening D-Bus proxy config file:", err)
|
||||
} else {
|
||||
if err = json.NewDecoder(f).Decode(&dbusSession); err != nil {
|
||||
internal.Fatal("Error parsing D-Bus proxy config file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
||||
fatal("Env variable", xdgRuntimeDir, "unset")
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if f, err := os.Open(dbusConfigSystem); err != nil {
|
||||
internal.Fatal("Error opening D-Bus proxy config file:", err)
|
||||
} else {
|
||||
runtime = r
|
||||
runDir = path.Join(runtime, "ego")
|
||||
if err = json.NewDecoder(f).Decode(&dbusSystem); err != nil {
|
||||
internal.Fatal("Error parsing D-Bus proxy config file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// state query command
|
||||
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
||||
a.EnsureRunDir()
|
||||
|
||||
// state query command early exit
|
||||
tryState()
|
||||
|
||||
// Report warning if user home directory does not exist or has wrong ownership
|
||||
if stat, err := os.Stat(ego.HomeDir); err != nil {
|
||||
if verbose {
|
||||
// ensure Share (e.g. `/tmp/fortify.%d`)
|
||||
a.EnsureShare()
|
||||
|
||||
// warn about target user home directory ownership
|
||||
if stat, err := os.Stat(a.HomeDir); err != nil {
|
||||
if verbose.Get() {
|
||||
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\n", 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 exis\n", 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\n", 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 {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
fatal("Runtime directory does not exist")
|
||||
}
|
||||
fatal("Error accessing runtime directory:", err)
|
||||
} else if !s.IsDir() {
|
||||
fatal(fmt.Sprintf("Path '%s' is not a directory", runtime))
|
||||
} else {
|
||||
if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil {
|
||||
fatal("Error preparing runtime dir:", err)
|
||||
} else {
|
||||
registerRevertPath(runtime)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Runtime data dir '%s' configured\n", runtime)
|
||||
}
|
||||
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||
a.EnsureRuntime()
|
||||
|
||||
if mustWayland {
|
||||
a.ShareWayland()
|
||||
}
|
||||
|
||||
// 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)
|
||||
if mustX {
|
||||
a.ShareX()
|
||||
}
|
||||
|
||||
// Add rwx permissions to Wayland socket (e.g. `/run/user/%d/wayland-0`)
|
||||
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
||||
if 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)
|
||||
} else {
|
||||
registerRevertPath(wp)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Wayland socket '%s' configured\n", w)
|
||||
}
|
||||
if mustDBus {
|
||||
a.ShareDBus(dbusSession, dbusSystem, dbusVerbose)
|
||||
}
|
||||
|
||||
// Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command
|
||||
if d, ok := os.LookupEnv(display); !ok {
|
||||
if verbose {
|
||||
fmt.Println("X11: DISPLAY not set, skipping")
|
||||
}
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
env = append(env, display+"="+d)
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d)
|
||||
}
|
||||
if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
|
||||
fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
||||
} else {
|
||||
xcbActionComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`)
|
||||
pulse := path.Join(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)
|
||||
}
|
||||
if mustPulse {
|
||||
fatal("PulseAudio is unavailable")
|
||||
}
|
||||
if 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)
|
||||
} else {
|
||||
registerRevertPath(pulse)
|
||||
a.SharePulse()
|
||||
}
|
||||
|
||||
// Ensure permissions of PulseAudio socket `/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")
|
||||
}
|
||||
fatal("Error accessing PulseAudio socket:", err)
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
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 {
|
||||
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 = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil {
|
||||
fatal("Error publishing PulseAudio cookie:", err)
|
||||
} else {
|
||||
registerRevertPath(pulseCookieFinal)
|
||||
}
|
||||
|
||||
if 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")
|
||||
}
|
||||
args = append(args, innerCommand.String())
|
||||
|
||||
return
|
||||
a.Run()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
types
|
||||
mkOption
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mapAttrs
|
||||
mapAttrsToList
|
||||
foldlAttrs
|
||||
optional
|
||||
;
|
||||
|
||||
cfg = config.environment.fortify;
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
environment.fortify = {
|
||||
enable = mkEnableOption "fortify";
|
||||
|
||||
target = mkOption {
|
||||
default = { };
|
||||
type =
|
||||
let
|
||||
inherit (types)
|
||||
str
|
||||
enum
|
||||
bool
|
||||
package
|
||||
anything
|
||||
submodule
|
||||
listOf
|
||||
attrsOf
|
||||
nullOr
|
||||
;
|
||||
in
|
||||
attrsOf (submodule {
|
||||
options = {
|
||||
packages = mkOption {
|
||||
type = listOf package;
|
||||
default = [ ];
|
||||
description = ''
|
||||
List of extra packages to install via home-manager.
|
||||
'';
|
||||
};
|
||||
|
||||
launchers = mkOption {
|
||||
type = attrsOf (submodule {
|
||||
options = {
|
||||
command = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
Command to run as the target user.
|
||||
Setting this to null will default command to wrapper name.
|
||||
'';
|
||||
};
|
||||
|
||||
dbus = {
|
||||
config = mkOption {
|
||||
type = nullOr anything;
|
||||
default = null;
|
||||
description = ''
|
||||
D-Bus custom configuration.
|
||||
Setting this to null will enable built-in defaults.
|
||||
'';
|
||||
};
|
||||
|
||||
configSystem = mkOption {
|
||||
type = nullOr anything;
|
||||
default = null;
|
||||
description = ''
|
||||
D-Bus system bus custom configuration.
|
||||
Setting this to null will disable the system bus proxy.
|
||||
'';
|
||||
};
|
||||
|
||||
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;
|
||||
description = ''
|
||||
Whether to share the PulseAudio socket and cookie.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
share = mkOption {
|
||||
type = nullOr package;
|
||||
default = null;
|
||||
description = ''
|
||||
Package containing share files.
|
||||
Setting this to null will default package name to wrapper name.
|
||||
'';
|
||||
};
|
||||
|
||||
method = mkOption {
|
||||
type = enum [
|
||||
"simple"
|
||||
"sudo"
|
||||
"bubblewrap"
|
||||
"systemd"
|
||||
];
|
||||
default = "systemd";
|
||||
description = ''
|
||||
Launch method for the sandboxed program.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
default = { };
|
||||
};
|
||||
|
||||
persistence = mkOption {
|
||||
type = submodule {
|
||||
options = {
|
||||
directories = mkOption {
|
||||
type = listOf anything;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
files = mkOption {
|
||||
type = listOf anything;
|
||||
default = [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
Per-user state passed to github:nix-community/impermanence.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = anything;
|
||||
default = { };
|
||||
description = "Extra home-manager configuration.";
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.callPackage ./package.nix { };
|
||||
description = "Package providing fortify.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
description = "Privileged user account.";
|
||||
};
|
||||
|
||||
shell = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Shell set up to source home-manager for the privileged user.
|
||||
Required for setting up the environment of sandboxed programs.
|
||||
'';
|
||||
};
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
The path to persistent storage where per-user state should be stored.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.persistence.${cfg.stateDir}.users = mapAttrs (_: target: target.persistence) cfg.target;
|
||||
|
||||
home-manager.users =
|
||||
mapAttrs (_: target: target.extraConfig // { home.packages = target.packages; }) cfg.target
|
||||
// {
|
||||
${cfg.user}.home.packages =
|
||||
let
|
||||
wrap =
|
||||
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;
|
||||
dbusSystem =
|
||||
if launcher.dbus.configSystem != null then
|
||||
pkgs.writeText "${name}-dbus-system.json" (builtins.toJSON launcher.dbus.configSystem)
|
||||
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 ${launcher.dbus.id}" else "")
|
||||
+ (if dbusConfig != null then " -dbus-config ${dbusConfig}" else "")
|
||||
+ (if dbusSystem != null then " -dbus-system ${dbusSystem}" else "");
|
||||
in
|
||||
pkgs.writeShellScriptBin name (
|
||||
if launcher.method == "simple" then
|
||||
''
|
||||
exec sudo -u ${user} -i ${command} $@
|
||||
''
|
||||
else
|
||||
''
|
||||
exec fortify${capArgs} -method ${launcher.method} -u ${user} ${cfg.shell} -c "exec ${command} $@"
|
||||
''
|
||||
)
|
||||
) launchers;
|
||||
in
|
||||
foldlAttrs (
|
||||
acc: user: target:
|
||||
acc
|
||||
++ (foldlAttrs (
|
||||
shares: name: launcher:
|
||||
let
|
||||
pkg = if launcher.share != null then launcher.share else pkgs.${name};
|
||||
link = source: "[ -d '${source}' ] && ln -sv '${source}' $out/share || true";
|
||||
in
|
||||
shares
|
||||
++
|
||||
optional (launcher.method != "simple" && (launcher.capability.wayland || launcher.capability.x11))
|
||||
(
|
||||
pkgs.runCommand "${name}-share" { } ''
|
||||
mkdir -p $out/share
|
||||
${link "${pkg}/share/applications"}
|
||||
${link "${pkg}/share/icons"}
|
||||
${link "${pkg}/share/man"}
|
||||
''
|
||||
)
|
||||
) (wrap user target.launchers) target.launchers)
|
||||
) [ cfg.package ] cfg.target;
|
||||
};
|
||||
|
||||
security.polkit.extraConfig =
|
||||
let
|
||||
allowList = builtins.toJSON (mapAttrsToList (name: _: name) cfg.target);
|
||||
in
|
||||
''
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.machine1.host-shell" &&
|
||||
${allowList}.indexOf(action.lookup("user")) > -1 &&
|
||||
subject.user == "${cfg.user}") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
'';
|
||||
};
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
makeBinaryWrapper,
|
||||
xdg-dbus-proxy,
|
||||
acl,
|
||||
xorg,
|
||||
}:
|
||||
|
||||
buildGoModule rec {
|
||||
pname = "fortify";
|
||||
version = "0.0.0-beta.3";
|
||||
|
||||
src = ./.;
|
||||
vendorHash = null;
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X"
|
||||
"main.Version=v${version}"
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
acl
|
||||
xorg.libxcb
|
||||
];
|
||||
|
||||
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/${pname} --prefix PATH : ${lib.makeBinPath [ xdg-dbus-proxy ]}
|
||||
'';
|
||||
}
|
158
state.go
158
state.go
|
@ -1,162 +1,42 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
// we unfortunately have to assume there are never races between processes
|
||||
// this and launcher should eventually be replaced by a server process
|
||||
|
||||
var (
|
||||
stateActionEarly bool
|
||||
statePath string
|
||||
cleanupCandidate []string
|
||||
xcbActionComplete bool
|
||||
stateActionEarly [2]bool
|
||||
)
|
||||
|
||||
type launcherState struct {
|
||||
PID int
|
||||
Launcher string
|
||||
Argv []string
|
||||
Command []string
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers")
|
||||
flag.BoolVar(&stateActionEarly[0], "state", false, "print state information of active launchers")
|
||||
flag.BoolVar(&stateActionEarly[1], "state-current", false, "print state information of active launchers for the specified user")
|
||||
}
|
||||
|
||||
// tryState is called after app initialisation
|
||||
func tryState() {
|
||||
if !stateActionEarly {
|
||||
var w *tabwriter.Writer
|
||||
|
||||
switch {
|
||||
case stateActionEarly[0]:
|
||||
state.MustPrintLauncherStateGlobal(&w, a.RunDir())
|
||||
case stateActionEarly[1]:
|
||||
state.MustPrintLauncherState(&w, a.RunDir(), a.Uid)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
launchers, err := readLaunchers()
|
||||
if err != nil {
|
||||
fmt.Println("Error reading launchers:", err)
|
||||
os.Exit(1)
|
||||
if w != nil {
|
||||
if err := w.Flush(); err != nil {
|
||||
fmt.Println("warn: error formatting output:", err)
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
fmt.Println("No information available")
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func registerRevertPath(p string) {
|
||||
cleanupCandidate = append(cleanupCandidate, p)
|
||||
}
|
||||
|
||||
// called after process start, before wait
|
||||
func registerProcess(uid string, cmd *exec.Cmd) error {
|
||||
statePath = path.Join(runDir, uid, strconv.Itoa(cmd.Process.Pid))
|
||||
state := launcherState{
|
||||
PID: cmd.Process.Pid,
|
||||
Launcher: cmd.Path,
|
||||
Argv: cmd.Args,
|
||||
Command: command,
|
||||
}
|
||||
|
||||
if err := os.Mkdir(path.Join(runDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer func() {
|
||||
if f.Close() != nil {
|
||||
// unreachable
|
||||
panic("state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
return gob.NewEncoder(f).Encode(state)
|
||||
}
|
||||
}
|
||||
|
||||
func readLaunchers() ([]*launcherState, error) {
|
||||
var f *os.File
|
||||
var r []*launcherState
|
||||
launcherPrefix := path.Join(runDir, ego.Uid)
|
||||
|
||||
if pl, err := os.ReadDir(launcherPrefix); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, e := range pl {
|
||||
if err = func() error {
|
||||
if f, err = os.Open(path.Join(launcherPrefix, e.Name())); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer func() {
|
||||
if f.Close() != nil {
|
||||
// unreachable
|
||||
panic("foreign state file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
var s launcherState
|
||||
r = append(r, &s)
|
||||
return gob.NewDecoder(f).Decode(&s)
|
||||
}
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func beforeExit() {
|
||||
if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
fmt.Println("Error removing state file:", err)
|
||||
}
|
||||
|
||||
if a, err := readLaunchers(); err != nil {
|
||||
fmt.Println("Error reading active launchers:", err)
|
||||
os.Exit(1)
|
||||
} else if len(a) > 0 {
|
||||
// other launchers are still active
|
||||
if verbose {
|
||||
fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(a))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Println("No other launchers active, will clean up")
|
||||
}
|
||||
|
||||
if xcbActionComplete {
|
||||
if verbose {
|
||||
fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", ego.Username)
|
||||
}
|
||||
if err := changeHosts(xcbHostModeDelete, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
|
||||
fmt.Println("Error removing XHost entry:", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range cleanupCandidate {
|
||||
if err := aclUpdatePerm(candidate, uid); err != nil {
|
||||
fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", ego.Username, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(msg ...any) {
|
||||
fmt.Println(msg...)
|
||||
beforeExit()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
100
util.go
100
util.go
|
@ -1,100 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
systemdCheckPath = "/run/systemd/system"
|
||||
)
|
||||
|
||||
// https://www.freedesktop.org/software/systemd/man/sd_booted.html
|
||||
func sdBooted() bool {
|
||||
_, err := os.Stat(systemdCheckPath)
|
||||
if err != nil {
|
||||
if verbose {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
fmt.Println("System not booted through systemd")
|
||||
} else {
|
||||
fmt.Println("Error accessing", systemdCheckPath+":", err.Error())
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Try various ways to discover the current user's PulseAudio authentication cookie.
|
||||
func discoverPulseCookie() string {
|
||||
if p, ok := os.LookupEnv(pulseCookie); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(home); ok {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
pulseCookie, xdgConfigHome, home))
|
||||
return ""
|
||||
}
|
||||
|
||||
func which(file string) (string, bool) {
|
||||
p, err := exec.LookPath(file)
|
||||
return p, err == nil
|
||||
}
|
||||
|
||||
func copyFile(dst, src string) error {
|
||||
srcD, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if srcD.Close() != nil {
|
||||
// unreachable
|
||||
panic("src file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if dstD.Close() != nil {
|
||||
// unreachable
|
||||
panic("dst file closed prematurely")
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(dstD, srcD)
|
||||
return err
|
||||
}
|
72
x11.go
72
x11.go
|
@ -1,72 +0,0 @@
|
|||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <xcb/xcb.h>
|
||||
//#cgo linux LDFLAGS: -lxcb
|
||||
import "C"
|
||||
|
||||
const (
|
||||
xcbHostModeInsert = C.XCB_HOST_MODE_INSERT
|
||||
xcbHostModeDelete = C.XCB_HOST_MODE_DELETE
|
||||
|
||||
xcbFamilyInternet = C.XCB_FAMILY_INTERNET
|
||||
xcbFamilyDecnet = C.XCB_FAMILY_DECNET
|
||||
xcbFamilyChaos = C.XCB_FAMILY_CHAOS
|
||||
xcbFamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
|
||||
xcbFamilyInternet6 = C.XCB_FAMILY_INTERNET_6
|
||||
)
|
||||
|
||||
func changeHosts(mode, family C.uint8_t, address string) error {
|
||||
var c *C.xcb_connection_t
|
||||
c = C.xcb_connect(nil, nil)
|
||||
defer C.xcb_disconnect(c)
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr := C.CString(address)
|
||||
cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr)))
|
||||
C.free(unsafe.Pointer(addr))
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := C.xcb_request_check(c, cookie)
|
||||
if e != nil {
|
||||
defer C.free(unsafe.Pointer(e))
|
||||
return errors.New("xcb_change_hosts() failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func xcbHandleConnectionError(c *C.xcb_connection_t) error {
|
||||
if errno := C.xcb_connection_has_error(c); errno != 0 {
|
||||
switch errno {
|
||||
case C.XCB_CONN_ERROR:
|
||||
return errors.New("connection error")
|
||||
case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
|
||||
return errors.New("extension not supported")
|
||||
case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
|
||||
return errors.New("memory not available")
|
||||
case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
|
||||
return errors.New("request length exceeded")
|
||||
case C.XCB_CONN_CLOSED_PARSE_ERR:
|
||||
return errors.New("invalid display string")
|
||||
case C.XCB_CONN_CLOSED_INVALID_SCREEN:
|
||||
return errors.New("server has no screen matching display")
|
||||
default:
|
||||
return errors.New("generic X11 failure")
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package xcb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <xcb/xcb.h>
|
||||
//#cgo linux LDFLAGS: -lxcb
|
||||
import "C"
|
||||
|
||||
func xcbHandleConnectionError(c *C.xcb_connection_t) error {
|
||||
if errno := C.xcb_connection_has_error(c); errno != 0 {
|
||||
switch errno {
|
||||
case C.XCB_CONN_ERROR:
|
||||
return errors.New("connection error")
|
||||
case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
|
||||
return errors.New("extension not supported")
|
||||
case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
|
||||
return errors.New("memory not available")
|
||||
case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
|
||||
return errors.New("request length exceeded")
|
||||
case C.XCB_CONN_CLOSED_PARSE_ERR:
|
||||
return errors.New("invalid display string")
|
||||
case C.XCB_CONN_CLOSED_INVALID_SCREEN:
|
||||
return errors.New("server has no screen matching display")
|
||||
default:
|
||||
return errors.New("generic X11 failure")
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package xcb
|
||||
|
||||
//#include <stdlib.h>
|
||||
//#include <xcb/xcb.h>
|
||||
//#cgo linux LDFLAGS: -lxcb
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
HostModeInsert = C.XCB_HOST_MODE_INSERT
|
||||
HostModeDelete = C.XCB_HOST_MODE_DELETE
|
||||
|
||||
FamilyInternet = C.XCB_FAMILY_INTERNET
|
||||
FamilyDecnet = C.XCB_FAMILY_DECNET
|
||||
FamilyChaos = C.XCB_FAMILY_CHAOS
|
||||
FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
|
||||
FamilyInternet6 = C.XCB_FAMILY_INTERNET_6
|
||||
)
|
||||
|
||||
func ChangeHosts(mode, family C.uint8_t, address string) error {
|
||||
var c *C.xcb_connection_t
|
||||
c = C.xcb_connect(nil, nil)
|
||||
defer C.xcb_disconnect(c)
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr := C.CString(address)
|
||||
cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr)))
|
||||
C.free(unsafe.Pointer(addr))
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := C.xcb_request_check(c, cookie)
|
||||
if e != nil {
|
||||
defer C.free(unsafe.Pointer(e))
|
||||
return errors.New("xcb_change_hosts() failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue