From e8583084216573ce6e1955ddf338e4fc76c36413 Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Thu, 22 May 2025 19:53:56 +0000 Subject: [PATCH] small fix --- README.md | 13 ++-- embed_gloomi.go | 1 + gloomi/main.go | 12 ++-- go.mod | 2 + go.sum | 2 + main.go | 174 +++++++++++++++++++++++++++++++++++++-------- pluginHost/main.go | 72 +++++++++++++++++-- zqdgr.config.json | 6 +- 8 files changed, 234 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index c63a7d9..7430890 100644 --- a/README.md +++ b/README.md @@ -58,14 +58,19 @@ GLoom will also use a config file in the `GLOOM_DIR` to configure plugins and ot ```toml [[plugins]] file = "gloomi.so" +name = "GLoomI" domains = ["localhost"] ``` -The `[[plugins]]` array is a list of plugins to preload. Each plugin is an object with the following fields: - -- `file` - The path to the plugin file. This is a string value. -- `domains` - An array of domains to forward requests to this plugin. This is an array of strings. +The config is in TOML and has the following keys: +- `plugins` - An array of plugins to load. Each plugin is an object with the following keys: + - `file` - The name of the plugin file. + - `name` - The name of the plugin. Can only contain alphanumeric characters and `-` and `_`. + - `domains` - An array of domains to load the plugin on. +- `pluginDir` - The directory to store plugins in relative to `GLOOM_DIR`. Defaults to `plugs`. +- `enableChroot` - Whether to enable chroot, this forces plugins to a specific directory that they cannot escape. + Defaults to `false`. ## Usage diff --git a/embed_gloomi.go b/embed_gloomi.go index 811b60c..cd98004 100644 --- a/embed_gloomi.go +++ b/embed_gloomi.go @@ -11,5 +11,6 @@ var embeddedAssets embed.FS var defaultConfig = []byte(` [[plugins]] file = "gloomi.so" +name = "GLoomI" domains = ["localhost"] `) diff --git a/gloomi/main.go b/gloomi/main.go index aefe177..3ac3f4c 100644 --- a/gloomi/main.go +++ b/gloomi/main.go @@ -16,13 +16,13 @@ type GLoomI struct { func (p *GLoomI) Init() (*fiber.Config, error) { // Connect to the RPC server - client, err := rpc.Dial("tcp", "localhost:7143") + client, err := rpc.Dial("tcp", "127.0.0.1:7143") if err != nil { return nil, fmt.Errorf("failed to connect to Gloom RPC server: %w", err) } p.client = client return &fiber.Config{ - BodyLimit: 1024 * 1024 * 1024 * 5, // 5GB + BodyLimit: 1024 * 1024 * 1024 * 5, // 5GiB }, nil } @@ -94,10 +94,12 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) { } var pluginUploadStruct struct { - Domains []string `json:"domains"` - Name string `json:"name"` - Data []byte `json:"data"` + FileName string `json:"fileName"` + Name string `json:"name"` + Domains []string `json:"domains"` + Data []byte `json:"data"` } + pluginUploadStruct.FileName = pluginFile.Filename pluginUploadStruct.Name = pluginUpload.Name pluginUploadStruct.Domains = domains pluginUploadStruct.Data, err = io.ReadAll(pluginData) diff --git a/go.mod b/go.mod index 4131b16..9fc5c68 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/mattn/go-sqlite3 v1.14.24 ) +require github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + require ( github.com/BurntSushi/toml v1.5.0 golang.org/x/sync v0.14.0 // indirect diff --git a/go.sum b/go.sum index 8ffe20b..7a02f7c 100644 --- a/go.sum +++ b/go.sum @@ -4,5 +4,7 @@ github.com/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd h1:JNazPdlAs307G github.com/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd/go.mod h1:CnRvcleiS2kvK1N2PeQmeoRP5EXpBDpHPkg72vAUaSg= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= diff --git a/main.go b/main.go index 22fcea1..2d9655d 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "fmt" + "log" "log/slog" "math/rand/v2" "net" @@ -23,8 +24,50 @@ import ( "github.com/juls0730/gloom/libs" "github.com/juls0730/sentinel" _ "github.com/mattn/go-sqlite3" + "github.com/syndtr/gocapability/capability" ) +func hasChrootAbility() bool { + cap, err := capability.NewPid2(0) + if err != nil { + return false + } + + err = cap.Load() + if err != nil { + return false + } + + return cap.Get(capability.EFFECTIVE, capability.CAP_SYS_CHROOT) || cap.Get(capability.PERMITTED, capability.CAP_SYS_CHROOT) +} + +func hasChangeUserAbility() bool { + cap, err := capability.NewPid2(0) + if err != nil { + return false + } + + err = cap.Load() + if err != nil { + return false + } + + permitted := true + for _, permission := range []capability.Cap{ + capability.CAP_SETUID, + capability.CAP_SETGID, + capability.CAP_SETFCAP, + } { + if cap.Get(capability.EFFECTIVE, permission) || cap.Get(capability.PERMITTED, permission) { + continue + } + permitted = false + break + } + + return permitted +} + type PluginHost struct { UnixSocket string Process *os.Process @@ -37,13 +80,16 @@ type PluginConfig struct { type PreloadPlugin struct { File string + Name string Domains []string } // might change this later const DEFAULT_PLUGIN_DIR = "plugs" +const DEFAULT_CHROOT_DIR = "plugData" type GLoomConfig struct { + EnableChroot bool `toml:"enableChroot"` PluginDir string `toml:"pluginDir"` PreloadPlugins []PreloadPlugin `toml:"plugins"` } @@ -51,8 +97,6 @@ type GLoomConfig struct { type GLoom struct { config GLoomConfig - // path to the /tmp directory where the pluginHost sockets are created - tmpDir string gloomDir string // maps plugin names to plugins @@ -65,6 +109,8 @@ type GLoom struct { } func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { + log.SetOutput(os.Stderr) + gloomDir := os.Getenv("GLOOM_DIR") if gloomDir == "" { if os.Getenv("XDG_DATA_HOME") != "" { @@ -106,11 +152,6 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { return nil, err } - tmpDir, err := os.MkdirTemp(os.TempDir(), "gloom") - if err != nil { - return nil, err - } - if _, err := os.Stat(filepath.Join(gloomDir, "pluginHost")); os.IsNotExist(err) { if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil { return nil, err @@ -120,7 +161,6 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { } gloom := &GLoom{ - tmpDir: tmpDir, gloomDir: gloomDir, plugins: libs.SyncMap[string, *PluginHost]{}, DB: db, @@ -132,6 +172,17 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { return nil, err } + if gloom.config.EnableChroot { + // make sure we have super user privileges + if !hasChrootAbility() { + return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it") + } + + if !hasChangeUserAbility() { + return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it") + } + } + if err := os.MkdirAll(gloom.config.PluginDir, 0755); err != nil { slog.Error("Failed to create pluginDir", "dir", gloom.config.PluginDir, "error", err) return nil, err @@ -141,13 +192,17 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { if _, err := embeddedAssets.Open("dist/gloomi.so"); err == nil { // and if the plugin doesn't exist, copy it over // TODO: instead, check if the plugin doesnt exist OR the binary has a newer timestamp than the current version - if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, "gloomi.so")); os.IsNotExist(err) { + if err := os.MkdirAll(path.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil { + return nil, err + } + + if _, err := os.Stat(path.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so")); os.IsNotExist(err) { gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so") if err != nil { return nil, err } - if err := os.WriteFile(filepath.Join(gloom.config.PluginDir, "gloomi.so"), gloomiData, 0755); err != nil { + if err := os.WriteFile(path.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so"), gloomiData, 0755); err != nil { return nil, err } } @@ -172,23 +227,31 @@ func (gloom *GLoom) loadConfig() error { return err } - slog.Debug("Loaded config", "config", gloom.config) - if gloom.config.PluginDir == "" { gloom.config.PluginDir = DEFAULT_PLUGIN_DIR } gloom.config.PluginDir = filepath.Join(gloom.gloomDir, gloom.config.PluginDir) + slog.Debug("Loaded config", "config", gloom.config) + return nil } +func (gloom *GLoom) Cleanup() error { + for _, plugin := range gloom.plugins.Keys() { + gloom.StopPlugin(plugin) + } + + // return nil +} + func (gloom *GLoom) LoadInitialPlugins() error { slog.Info("Loading initial plugins") for _, plugin := range gloom.config.PreloadPlugins { - if err := gloom.RegisterPlugin(filepath.Join(gloom.config.PluginDir, plugin.File), plugin.File, plugin.Domains); err != nil { - panic(fmt.Errorf("failed to load preload plugin %s: %w (make sure its in %s)", plugin.File, err, gloom.config.PluginDir)) + if err := gloom.RegisterPlugin(filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File), plugin.Name, plugin.Domains); err != nil { + panic(fmt.Errorf("failed to load preload plugin %s: %w (make sure its in %s)", plugin.Name, err, path.Join(gloom.config.PluginDir, plugin.Name))) } } @@ -269,30 +332,65 @@ func (dt *MutexLock[T]) Unlock(id T) { var deploymentLock = NewMutexLock[string]() func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []string) (err error) { + for _, char := range name { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' || char == '_') { + return fmt.Errorf("invalid plugin name") + } + } + slog.Info("Registering plugin", "pluginPath", pluginPath, "domains", domains) pathStr := strconv.FormatUint(uint64(rand.Uint64()), 16) - socketPath := path.Join(gloom.tmpDir, pathStr+".sock") + socketPath := path.Join(gloom.config.PluginDir, name, pathStr+".sock") slog.Debug("Starting pluginHost", "pluginPath", pluginPath) processPath := path.Join(gloom.gloomDir, "pluginHost") args := []string{"--plugin-path", pluginPath, "--socket-path", socketPath} + if gloom.config.EnableChroot { + if err := os.MkdirAll(path.Join(gloom.config.PluginDir, name), 0755); err != nil { + return fmt.Errorf("failed to create chroot directory: %w", err) + } + + args = append(args, "--chroot-dir", path.Join(gloom.config.PluginDir, name)) + } + + files, err := os.ReadDir(path.Join(gloom.config.PluginDir, name)) + if err != nil { + return fmt.Errorf("failed to read pluginDir: %w", err) + } + + slog.Debug("Removing dead sockets", "pluginDir", path.Join(gloom.config.PluginDir, name)) + + for _, file := range files { + if file.IsDir() { + continue + } + + // remove all dead sockets + if strings.HasSuffix(file.Name(), ".sock") { + if err := os.Remove(path.Join(gloom.config.PluginDir, name, file.Name())); err != nil { + return fmt.Errorf("failed to remove socket: %w", err) + } + } + } + cmd := exec.Command(processPath, args...) + stderrPipe, err := cmd.StderrPipe() if err != nil { return fmt.Errorf("failed to get stderr pipe: %w", err) } + reader := bufio.NewReader(stderrPipe) + readTimeout := time.After(30 * time.Second) + if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start pluginHost: %w", err) } process := cmd.Process - reader := bufio.NewReader(stderrPipe) - readTimeout := time.After(30 * time.Second) - select { case <-readTimeout: _ = process.Signal(os.Interrupt) @@ -318,6 +416,8 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str } } + slog.Debug("Creating proxy", "socketPath", socketPath) + proxy, err := sentinel.NewDeploymentProxy(socketPath, NewUnixSocketTransport) if err != nil { return err @@ -368,10 +468,7 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str return nil } -// removes plugin from proxy and kills the process -func (gloom *GLoom) DeletePlugin(pluginName string) error { - slog.Debug("Deleting plugin", "pluginName", pluginName) - +func (gloom *GLoom) StopPlugin(pluginName string) error { pluginHost, ok := gloom.plugins.Load(pluginName) if !ok { return fmt.Errorf("plugin not found") @@ -389,6 +486,17 @@ func (gloom *GLoom) DeletePlugin(pluginName string) error { gloom.plugins.Delete(pluginName) + return nil +} + +// removes plugin from proxy and kills the process +func (gloom *GLoom) DeletePlugin(pluginName string) error { + slog.Debug("Deleting plugin", "pluginName", pluginName) + + if err := gloom.StopPlugin(pluginName); err != nil { + return err + } + var pluginPath string if err := gloom.DB.QueryRow("SELECT path FROM plugins WHERE name = ?", pluginName).Scan(&pluginPath); err != nil { return fmt.Errorf("failed to get plugin path: %w", err) @@ -457,14 +565,15 @@ func (rpc *GloomRPC) ListPlugins(_ struct{}, reply *[]PluginData) error { } type PluginUpload struct { - Name string `json:"name"` - Domains []string `json:"domains"` - Data []byte `json:"data"` + FileName string `json:"fileName"` + Name string `json:"name"` + Domains []string `json:"domains"` + Data []byte `json:"data"` } func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { for _, preloadPlugin := range rpc.gloom.config.PreloadPlugins { - if plugin.Name == preloadPlugin.File { + if plugin.Name == preloadPlugin.Name { *reply = "Plugin is preloaded" return nil } @@ -497,7 +606,7 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { defer deploymentLock.Unlock(plugin.Name) slog.Info("Uploading plugin", "plugin", plugin.Name, "domains", plugin.Domains) - pluginPath, err := filepath.Abs(filepath.Join(rpc.gloom.config.PluginDir, (plugin.Name + ".so"))) + pluginPath, err := filepath.Abs(filepath.Join(rpc.gloom.config.PluginDir, plugin.Name, plugin.FileName)) if err != nil { *reply = "Plugin upload failed" return err @@ -567,6 +676,11 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { } } + if err := os.MkdirAll(filepath.Join(rpc.gloom.config.PluginDir, plugin.Name), 0755); err != nil { + *reply = "Plugin upload failed" + return err + } + // regardless of if plugin exists or not, we'll upload the file since this could be an update to an existing plugin if err := os.WriteFile(pluginPath, plugin.Data, 0644); err != nil { *reply = "Plugin upload failed" @@ -605,7 +719,7 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error { for _, preloadPlugin := range rpc.gloom.config.PreloadPlugins { - if pluginName == preloadPlugin.File { + if pluginName == preloadPlugin.Name { *reply = "Plugin is preloaded" return nil } @@ -644,8 +758,10 @@ func main() { gloom, err := NewGloom(proxyManager) if err != nil { - panic(err) + fmt.Printf("Failed to create GLoom: %v\n", err) + os.Exit(1) } + defer gloom.Cleanup() if err := gloom.StartRPCServer(); err != nil { panic("Failed to start RPC server: " + err.Error()) diff --git a/pluginHost/main.go b/pluginHost/main.go index f904d92..98e10ca 100644 --- a/pluginHost/main.go +++ b/pluginHost/main.go @@ -10,12 +10,15 @@ import ( "os/signal" "path/filepath" "plugin" + "strings" + "syscall" "github.com/gofiber/fiber/v3" ) var pluginPath string var socketPath string +var chrootDir string // Idk why I originally wrote this solution when stderr is literally just the best solution for me, but this // makes the pluginHost more generally useful outside of GLoom, so I'm keeping it @@ -47,13 +50,13 @@ func main() { if router != nil { if err := router.Shutdown(); err != nil { - log.Printf("Error shutting down router: %v", err) + log.Printf("Error: error shutting down router: %v", err) } } if listener != nil { if err := listener.Close(); err != nil { - log.Printf("Error closing listener: %v", err) + log.Printf("Error: error closing listener: %v", err) } os.Remove(socketPath) @@ -70,10 +73,11 @@ func main() { fs.StringVar(&pluginPath, "plugin-path", "", "Path to the plugin") fs.StringVar(&socketPath, "socket-path", "", "Path to the socket") fs.StringVar(&controlPath, "control-path", "", "Path to the control socket") + fs.StringVar(&chrootDir, "chroot-dir", "", "Path to the chroot directory") err := fs.Parse(os.Args[1:]) if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing arguments: %v", err) + fmt.Fprintf(os.Stderr, "Error: error parsing arguments: %v", err) os.Exit(1) } os.Args = fs.Args() @@ -98,13 +102,13 @@ func main() { controlListener, err := net.Listen("unix", controlPath) if err != nil { - log.Fatalf("Error listening on control socket: %v", err) + log.Fatalf("Error: error listening on control socket: %v", err) } defer controlListener.Close() conn, err := controlListener.Accept() if err != nil { - log.Printf("Error accepting control connection: %v", err) + log.Printf("Error: error accepting control connection: %v", err) return } defer conn.Close() @@ -120,13 +124,13 @@ func main() { os.Remove(controlPath) } if !ok { - log.Printf("Control connection is not a writer") + log.Printf("Error: control connection is not a writer") return } } if _, err := os.Stat(socketPath); err == nil { - Print("Error: Socket %s already exists", socketPath) + Print("Error: socket %s already exists", socketPath) os.Exit(1) } @@ -136,6 +140,60 @@ func main() { os.Exit(1) } + if chrootDir != "" { + if !strings.HasPrefix(socketPath, chrootDir) { + Print("Error: socket path is not in the chroot directory, but chroot is enabled, and therefore the socket cannot be used by the plugin. This is a GLoom bug, please report it.") + os.Exit(1) + } + + // we are chrooting and changing to nobody to "sandbox" the plugin + // nobodyUser, err := user.Lookup("nobody") + // if err != nil { + // Print("Error: failed to get nobody user: %w", err) + // } + + // nobodyUid, err := strconv.Atoi(nobodyUser.Uid) + // if err != nil { + // Print("Error: failed to parse nobody uid: %w", err) + // } + + // nobodyGid, err := strconv.Atoi(nobodyUser.Gid) + // if err != nil { + // Print("Error: failed to parse nobody gid: %w", err) + // } + + pluginData, err := os.ReadFile(realPluginPath) + if err != nil { + Print("Error: failed to read plugin: %w", err) + } + + pluginFileName := filepath.Base(realPluginPath) + + // copy the plugin to the chroot directory + if err := os.WriteFile(filepath.Join(chrootDir, pluginFileName), pluginData, 0644); err != nil { + Print("Error: failed to copy plugin to chroot: %w", err) + } + + // if err := os.Chown(chrootDir, nobodyUid, nobodyGid); err != nil { + // Print("Error: failed to chown chroot directory: %w", err) + // } + + realPluginPath = "/" + pluginFileName + socketPath = "/" + filepath.Base(socketPath) + + if err := syscall.Chroot(chrootDir); err != nil { + Print("Error: failed to chroot: %w", err) + } + + // if err := syscall.Setgid(nobodyGid); err != nil { + // Print("Error: failed to setgid: %w", err) + // } + + // if err := syscall.Setuid(nobodyUid); err != nil { + // Print("Error: failed to setuid: %w", err) + // } + } + p, err := plugin.Open(realPluginPath) if err != nil { Print("Error: could not open plugin %s: %v", realPluginPath, err) diff --git a/zqdgr.config.json b/zqdgr.config.json index 95d651f..a6e6b32 100644 --- a/zqdgr.config.json +++ b/zqdgr.config.json @@ -6,8 +6,8 @@ "license": "MIT", "scripts": { "build": "zqdgr build:gloomi && zqdgr build:gloom", - "build:pluginHost": "sh -c \"cd pluginHost; go build -ldflags '-w -s' -o ../dist/host main.go\"", - "build:gloomi": "sh -c \"cd gloomi; zqdgr build\"", + "build:pluginHost": "cd pluginHost; go build -ldflags '-w -s' -o ../dist/host main.go", + "build:gloomi": "cd gloomi; zqdgr build", "build:gloom": "zqdgr build:pluginHost && go build -tags=gloomi -o dist/gloom", "build:nogloomi": "zqdgr build:pluginHost && go build -tags=!gloomi -o dist/gloom", "clean": "rm -rf dist && rm -rf plugin/plugin.so", @@ -15,4 +15,4 @@ }, "pattern": "**/*.go", "excluded_dirs": [] -} \ No newline at end of file +}