filed
Job queue using FUSE
git clone git://mccd.space/filed
| Log | Files | Refs | README | LICENSE |
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 }