filed

Job queue using FUSE

git clone git://mccd.space/filed

main.go (6169B)

      1 package main
      2 
      3 import (
      4 	"context"
      5 	"flag"
      6 	"fmt"
      7 	"git.sr.ht/~marcc/filed/store"
      8 	"log"
      9 	"log/slog"
     10 	"os"
     11 	"os/exec"
     12 	"path/filepath"
     13 	"syscall"
     14 
     15 	"bazil.org/fuse"
     16 	"bazil.org/fuse/fs"
     17 	_ "bazil.org/fuse/fs/fstestutil"
     18 	"github.com/landlock-lsm/go-landlock/landlock"
     19 )
     20 
     21 func usage() {
     22 	fmt.Fprintf(os.Stderr, "Usage: %s MOUNTPOINT\n", os.Args[0])
     23 	flag.PrintDefaults()
     24 }
     25 
     26 type Restrictions struct {
     27 	rwFiles []string
     28 	roFiles []string
     29 	rwDir   []string
     30 	roDir   []string
     31 }
     32 
     33 func main() {
     34 	flag.Usage = usage
     35 	restrictions := Restrictions{}
     36 	shouldLandlock := false
     37 	flag.Func("ro", "Read-only path", func(s string) error {
     38 		restrictions.roDir = append(restrictions.roDir, s)
     39 		shouldLandlock = true
     40 		return nil
     41 	})
     42 
     43 	flag.Func("rof", "Read-only file", func(s string) error {
     44 		restrictions.roFiles = append(restrictions.roFiles, s)
     45 		shouldLandlock = true
     46 		return nil
     47 	})
     48 
     49 	flag.Func("rwf", "Read-write file", func(s string) error {
     50 		restrictions.rwFiles = append(restrictions.rwFiles, s)
     51 
     52 		shouldLandlock = true
     53 		return nil
     54 	})
     55 
     56 	flag.Func("rw", "Read-write path", func(s string) error {
     57 		restrictions.rwDir = append(restrictions.rwDir, s)
     58 
     59 		shouldLandlock = true
     60 		return nil
     61 	})
     62 	flag.Parse()
     63 	if flag.NArg() != 1 {
     64 		usage()
     65 		os.Exit(2)
     66 	}
     67 
     68 	filedLaunchExecutable, err := exec.LookPath("filed-launch")
     69 	if err != nil {
     70 		log.Fatalf("filed-launch needs to be available in $PATH: %v", err)
     71 	}
     72 	fusermountExecutable, err := exec.LookPath("fusermount")
     73 	if err != nil {
     74 		log.Fatalf("fusermount needs to be available in $PATH: %v", err)
     75 	}
     76 
     77 	userUid := uint32(os.Getuid())
     78 	if userUid == 0 {
     79 		warning := fmt.Sprintf("Running %s as root is highly not recommended. Be careful", os.Args[0])
     80 		slog.Warn(warning)
     81 	}
     82 
     83 	dbPath := getDbPath()
     84 	mountpoint := flag.Arg(0)
     85 
     86 	if err := Unmount(mountpoint); err != nil {
     87 		slog.Debug("FUSE: Pre-start unmount failed (this is usually okay)", "error", err)
     88 	}
     89 
     90 	store, err := store.NewStore(dbPath)
     91 	if err != nil {
     92 		panic(err)
     93 	}
     94 	slog.Info("Mounting filesystem", "mountpoint", mountpoint)
     95 	c, err := fuse.Mount(
     96 		mountpoint,
     97 		fuse.FSName("filed"),
     98 		fuse.AllowOther(),
     99 		fuse.DefaultPermissions(),
    100 	)
    101 	if err != nil {
    102 		panic(err)
    103 	}
    104 	defer c.Close()
    105 
    106 	if shouldLandlock {
    107 		var rules []landlock.Rule
    108 
    109 		// For the filed daemon we need to append extra files and directories needed to operate.
    110 		// Later on, filed-launch will further landlock each process to make sure they can't
    111 		// access these files.
    112 
    113 		// fusermount and and filed-launcher are needed to launch applications
    114 		rwFilePathsForFiled := append(restrictions.rwFiles, dbPath)
    115 		rules = append(rules, landlock.RWFiles(rwFilePathsForFiled...))
    116 
    117 		// filed-launch are needed to launch applications
    118 		// fusermount for unmounting
    119 		roFilePathsForFiled := append(restrictions.roFiles, "/dev/fuse", "/dev/null", fusermountExecutable, filedLaunchExecutable)
    120 		rules = append(rules, landlock.ROFiles(roFilePathsForFiled...))
    121 
    122 		// /proc and /dev are needed to oversee the process and kill it
    123 		roDirPathsForFiled := append(restrictions.roDir, "/proc")
    124 		rules = append(rules, landlock.RODirs(roDirPathsForFiled...))
    125 
    126 		rwPathsForFiled := append(restrictions.rwFiles, mountpoint)
    127 		rules = append(rules, landlock.RWDirs(rwPathsForFiled...))
    128 
    129 		err := landlock.V5.BestEffort().RestrictPaths(
    130 			rules...,
    131 		)
    132 		if err != nil {
    133 			log.Fatalf("Failed to landlock: %v", err)
    134 		}
    135 	}
    136 
    137 	jobManager := NewJobManager(store, filedLaunchExecutable, &restrictions)
    138 	ctx, cancel := context.WithCancel(context.Background())
    139 	defer cancel()
    140 	jobManager.StartWorker(ctx)
    141 
    142 	err = fs.Serve(c, FS{jobManager})
    143 	if err != nil {
    144 		panic(err)
    145 	}
    146 
    147 }
    148 
    149 type FS struct {
    150 	manager *JobManager
    151 }
    152 
    153 func (fs FS) Root() (fs.Node, error) {
    154 	return RootDir{fs.manager}, nil
    155 }
    156 
    157 type RootDir struct {
    158 	manager *JobManager
    159 }
    160 
    161 func (RootDir) Attr(ctx context.Context, a *fuse.Attr) error {
    162 	a.Mode = os.ModeDir | 0o555
    163 	return nil
    164 }
    165 
    166 func (rd RootDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
    167 	slog.Debug("FUSE: Lookup", "name", name)
    168 	switch name {
    169 	case store.StatePending:
    170 		return &PendingDir{manager: rd.manager, inode: 2}, nil
    171 	case store.StateCompleted:
    172 		return &JobDir{state: name, manager: rd.manager, inode: 3}, nil
    173 	case store.StateFailed:
    174 		return &JobDir{state: name, manager: rd.manager, inode: 4}, nil
    175 	case store.StateRunning:
    176 		return &JobDir{state: name, manager: rd.manager, inode: 5}, nil
    177 	case NewIdName:
    178 		return &NewIdFile{manager: rd.manager, inode: 6}, nil
    179 	case ConfigName:
    180 		return &ConfigFile{manager: rd.manager, inode: 7}, nil
    181 	default:
    182 		return nil, syscall.ENOENT
    183 	}
    184 }
    185 
    186 var rootEntries = []fuse.Dirent{
    187 	{Name: store.StatePending, Type: fuse.DT_Dir},
    188 	{Name: store.StateCompleted, Type: fuse.DT_Dir},
    189 	{Name: store.StateFailed, Type: fuse.DT_Dir},
    190 	{Name: store.StateRunning, Type: fuse.DT_Dir},
    191 	{Name: NewIdName, Type: fuse.DT_File},
    192 	{Name: ConfigName, Type: fuse.DT_File},
    193 }
    194 
    195 func (RootDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
    196 	return rootEntries, nil
    197 }
    198 func getDbPath() string {
    199 	dbPath := os.Getenv("FILED_STATE_FILE")
    200 	if dbPath == "" {
    201 		xdg_home := os.Getenv("XDG_DATA_HOME")
    202 		if xdg_home == "" {
    203 			fmt.Fprintf(os.Stderr, "FILED_STATE_FILE environment variable needs to be set.\n")
    204 			fmt.Fprintf(os.Stderr, "For example: export FILED_STATE_FILE=$HOME/.local/share/filed.db")
    205 			usage()
    206 			os.Exit(1)
    207 		}
    208 		dbPath = filepath.Join(xdg_home, "filed.db")
    209 	}
    210 	return dbPath
    211 }
    212 
    213 // XXX Should be struct
    214 func getLandlockOptions() ([]string, []string, []string, []string, bool) {
    215 	var roPaths, roFilePaths, rwFilePaths, rwPaths []string
    216 	isSet := false
    217 	flag.Func("ro", "Read-only path", func(s string) error {
    218 		roPaths = append(roPaths, s)
    219 		isSet = true
    220 		return nil
    221 	})
    222 
    223 	flag.Func("rof", "Read-only file", func(s string) error {
    224 		roFilePaths = append(roFilePaths, s)
    225 		isSet = true
    226 		return nil
    227 	})
    228 
    229 	flag.Func("rwf", "Read-write file", func(s string) error {
    230 		rwFilePaths = append(rwFilePaths, s)
    231 
    232 		isSet = true
    233 		return nil
    234 	})
    235 
    236 	flag.Func("rw", "Read-write path", func(s string) error {
    237 		rwPaths = append(rwPaths, s)
    238 
    239 		isSet = true
    240 		return nil
    241 	})
    242 
    243 	return roPaths, roFilePaths, rwFilePaths, rwPaths, isSet
    244 }