1 Commits
main ... trunk

Author SHA1 Message Date
Zoe
fa319076d1 fix chroot 2025-05-27 14:13:37 +00:00
4 changed files with 207 additions and 56 deletions

View File

@@ -6,7 +6,6 @@ import (
"mime/multipart" "mime/multipart"
"net/rpc" "net/rpc"
"strings" "strings"
"time"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -133,4 +132,3 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) {
// Exported symbol // Exported symbol
var Plugin GLoomI var Plugin GLoomI
var Version = time.Now()

108
main.go
View File

@@ -67,6 +67,48 @@ func hasChangeUserAbility() bool {
return permitted return permitted
} }
func IsExecutableNewer(filePath string) bool {
// Get the location of the gloom binary
gloomPath, err := os.Executable()
if err != nil {
return false
}
gloomPath, err = filepath.EvalSymlinks(gloomPath)
if err != nil {
return false
}
fileInfo, err := os.Stat(gloomPath)
if err != nil {
return false
}
childInfo, err := os.Stat(filePath)
if err != nil {
return false
}
return fileInfo.ModTime().After(childInfo.ModTime())
}
func hasChangeResourceLimitAbility() 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_RESOURCE) || cap.Get(capability.PERMITTED, capability.CAP_SYS_RESOURCE)
}
// this is the default for user 1000 on my system
const NOFILE_LIMIT = 1048576
type PluginHost struct { type PluginHost struct {
UnixSocket string UnixSocket string
Process *os.Process Process *os.Process
@@ -151,12 +193,24 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
return nil, err return nil, err
} }
if _, err := os.Stat(filepath.Join(gloomDir, "pluginHost")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(gloomDir, "pluginHost")); err != nil {
if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil { if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil {
return nil, err return nil, err
} }
slog.Debug("Wrote pluginHost", "dir", filepath.Join(gloomDir, "pluginHost")) slog.Debug("Wrote pluginHost", "dir", filepath.Join(gloomDir, "pluginHost"))
} else {
if IsExecutableNewer(filepath.Join(gloomDir, "pluginHost")) {
slog.Debug("Replacing pluginHost", "dir", filepath.Join(gloomDir, "pluginHost"), "reason", "host is old")
if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil {
return nil, err
}
}
}
if err := os.Chmod(filepath.Join(gloomDir, "pluginHost"), 0777); err != nil {
return nil, err
} }
gloom := &GLoom{ gloom := &GLoom{
@@ -180,6 +234,10 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
if !hasChangeUserAbility() { if !hasChangeUserAbility() {
return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it") return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it")
} }
if !hasChangeResourceLimitAbility() {
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 { if err := os.MkdirAll(gloom.config.PluginDir, 0755); err != nil {
@@ -189,21 +247,33 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
// if gloomi is built into the binary // if gloomi is built into the binary
if _, err := embeddedAssets.Open("dist/gloomi.so"); err == nil { if _, err := embeddedAssets.Open("dist/gloomi.so"); err == nil {
// and if the plugin doesn't exist, copy it over // ensure the plugin directory exists
// TODO: instead, check if the plugin doesnt exist OR the binary has a newer timestamp than the current version
if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil { if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil {
return nil, err return nil, err
} }
if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so")); os.IsNotExist(err) {
gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so") gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so")
if err != nil { if err != nil {
return nil, err return nil, err
} }
gloomiPath := filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so")
if _, err := os.Stat(gloomiPath); err != nil {
// if the plugin doesn't exist, copy it over
slog.Debug("Replacing gloomi", "pluginPath", gloomiPath, "reason", "plugin doesnt exist")
if err := os.WriteFile(filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so"), gloomiData, 0755); err != nil { if err := os.WriteFile(filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so"), gloomiData, 0755); err != nil {
return nil, err return nil, err
} }
} else {
if IsExecutableNewer(gloomiPath) {
slog.Debug("Replacing gloomi", "pluginPath", gloomiPath, "reason", "plugin is old")
if err := os.WriteFile(gloomiPath, gloomiData, 0755); err != nil {
return nil, err
}
}
} }
} }
@@ -249,16 +319,16 @@ func (gloom *GLoom) LoadInitialPlugins() error {
slog.Info("Loading initial plugins") slog.Info("Loading initial plugins")
for _, plugin := range gloom.config.PreloadPlugins { for _, plugin := range gloom.config.PreloadPlugins {
// if plugin is in PluginDir, we should move it to its named folder // if a plugin is marked as preload, but its not organized in the way that GLoom would set it up, we should move
// the plugin to its named folder
if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, plugin.File)); err == nil { if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, plugin.File)); err == nil {
slog.Debug("Moving plugin to its named folder", "plugin", plugin.File) slog.Debug("Moving plugin to its named folder", "plugin", plugin.File)
if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, plugin.Name), 0755); err != nil { if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, plugin.Name), 0755); err != nil {
panic(fmt.Errorf("failed to create plugin folder: %w", err)) return fmt.Errorf("failed to create plugin folder: %w", err)
} }
if err := os.Rename(filepath.Join(gloom.config.PluginDir, plugin.File), filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File)); err != nil { if err := os.Rename(filepath.Join(gloom.config.PluginDir, plugin.File), filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File)); err != nil {
panic(fmt.Errorf("failed to move plugin to its named folder: %w", err)) return fmt.Errorf("failed to move plugin to its named folder: %w", err)
} }
} }
@@ -266,7 +336,7 @@ func (gloom *GLoom) LoadInitialPlugins() error {
path := filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File) path := filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File)
if err := gloom.RegisterPlugin(path, plugin.Name, plugin.Domains); err != nil { if err := gloom.RegisterPlugin(path, 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)) return fmt.Errorf("failed to load preload plugin %s: %w (make sure its in %s)", plugin.Name, err, path)
} }
} }
@@ -436,7 +506,7 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str
status = strings.TrimSpace(status) status = strings.TrimSpace(status)
if status == "ready" { if status == "ready" {
slog.Debug("PluginHost ported ready", "pluginPath", pluginPath) slog.Debug("PluginHost ready", "pluginPath", pluginPath, "hostPid", process.Pid)
break break
} else if strings.HasPrefix(status, "Error: ") { } else if strings.HasPrefix(status, "Error: ") {
errorMessage := strings.TrimPrefix(status, "Error: ") errorMessage := strings.TrimPrefix(status, "Error: ")
@@ -773,10 +843,8 @@ func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error {
} }
func main() { func main() {
debug, err := strconv.ParseBool(os.Getenv("DEBUG")) // no need to check error value, it will always be a valid bool
if err != nil { debug, _ := strconv.ParseBool(os.Getenv("DEBUG"))
debug = false
}
level := slog.LevelInfo level := slog.LevelInfo
if debug { if debug {
@@ -796,14 +864,20 @@ func main() {
defer gloom.Cleanup() defer gloom.Cleanup()
if err := gloom.StartRPCServer(); err != nil { if err := gloom.StartRPCServer(); err != nil {
panic("Failed to start RPC server: " + err.Error()) slog.Error("Failed to start RPC server", "error", err)
os.Exit(1)
} }
gloom.LoadInitialPlugins() err = gloom.LoadInitialPlugins()
if err != nil {
slog.Error("Failed to load initial plugins", "error", err)
os.Exit(1)
}
slog.Info("Server running at http://localhost:3000") slog.Info("Server running at http://localhost:3000")
if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil { if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil {
panic(err) slog.Error("Failed to start proxy server", "error", err)
os.Exit(1)
} }
} }

View File

@@ -1,6 +1,12 @@
package main package main
import "github.com/gofiber/fiber/v3" import (
"fmt"
"os"
"path/filepath"
"github.com/gofiber/fiber/v3"
)
type MyPlugin struct{} type MyPlugin struct{}
@@ -16,6 +22,60 @@ func (p *MyPlugin) RegisterRoutes(router fiber.Router) {
router.Get("/hello", func(c fiber.Ctx) error { router.Get("/hello", func(c fiber.Ctx) error {
return c.SendString("Hello from MyPlugin!") return c.SendString("Hello from MyPlugin!")
}) })
router.Get("/dir/:path?", func(c fiber.Ctx) error {
// Get the directory path from the URL parameter
// If the parameter is empty, default to the current working directory.
dirPath := c.Params("path")
if dirPath == "" {
var err error
dirPath, err = os.Getwd() // Get current working directory
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to get current working directory",
})
}
}
// Ensure the path is absolute for security and clarity
absPath, err := filepath.Abs(dirPath)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Failed to get absolute path for %s: %v", dirPath, err),
})
}
// Read the directory contents
files, err := os.ReadDir(absPath)
if err != nil {
// Handle different error types for better feedback
if os.IsNotExist(err) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": fmt.Sprintf("Directory not found: %s", absPath),
})
}
if os.IsPermission(err) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": fmt.Sprintf("Permission denied to access directory: %s", absPath),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Failed to read directory %s: %v", absPath, err),
})
}
// Extract file names and prepare the response
var fileNames []string
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
// Return the list of file names as JSON
return c.JSON(fiber.Map{
"directory": absPath,
"files": fileNames,
})
})
} }
// Exported symbol // Exported symbol

View File

@@ -8,8 +8,10 @@ import (
"net" "net"
"os" "os"
"os/signal" "os/signal"
"os/user"
"path/filepath" "path/filepath"
"plugin" "plugin"
"strconv"
"strings" "strings"
"syscall" "syscall"
@@ -147,51 +149,63 @@ func main() {
} }
// we are chrooting and changing to nobody to "sandbox" the plugin // we are chrooting and changing to nobody to "sandbox" the plugin
// nobodyUser, err := user.Lookup("nobody") 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 { if err != nil {
Print("Error: failed to read plugin: %w", err) Print("Error: failed to get nobody user: %w", err)
} }
pluginFileName := filepath.Base(realPluginPath) nobodyUid, err := strconv.Atoi(nobodyUser.Uid)
if err != nil {
// copy the plugin to the chroot directory Print("Error: failed to parse nobody uid: %w", err)
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 { nobodyGid, err := strconv.Atoi(nobodyUser.Gid)
// Print("Error: failed to chown chroot directory: %w", err) if err != nil {
// } Print("Error: failed to parse nobody gid: %w", err)
}
realPluginPath = "/" + pluginFileName if err := os.Chown(chrootDir, nobodyUid, nobodyGid); err != nil {
Print("Error: failed to chown chroot directory: %w", err)
}
if err := os.Chown(realPluginPath, nobodyUid, nobodyGid); err != nil {
Print("Error: failed to chown plugin directory: %w", err)
}
chrootTmp := filepath.Join(chrootDir, "tmp")
if err := os.RemoveAll(chrootTmp); err != nil {
Print("Error: failed to remove chroot tmp directory: %w", err)
}
if err := os.MkdirAll(chrootTmp, 0755); err != nil {
Print("Error: failed to create chroot tmp directory: %w", err)
}
// we could bind os.TempDir() to chrootTmp, but that would allow for plugins to read eachothers temp files, so
// instead we'll create a new temp directory specifically for the plugin that gets clearned on exit and startup.
defer func() {
os.RemoveAll(chrootTmp)
}()
realPluginPath = "/" + filepath.Base(realPluginPath)
socketPath = "/" + filepath.Base(socketPath) socketPath = "/" + filepath.Base(socketPath)
if err := syscall.Chroot(chrootDir); err != nil { if err := syscall.Chroot(chrootDir); err != nil {
Print("Error: failed to chroot: %w", err) Print("Error: failed to chroot: %w", err)
} }
// if err := syscall.Setgid(nobodyGid); err != nil { if err := syscall.Chdir("/"); err != nil {
// Print("Error: failed to setgid: %w", err) Print("Error: failed to chdir: %w", err)
// } }
// if err := syscall.Setuid(nobodyUid); err != nil { if err := syscall.Setgid(nobodyGid); err != nil {
// Print("Error: failed to setuid: %w", err) 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) p, err := plugin.Open(realPluginPath)
@@ -233,6 +247,11 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if err := os.Chmod(socketPath, 0666); err != nil {
Print("Error: failed to chmod socket: %v", err)
os.Exit(1)
}
if err := router.Listener(listener, fiber.ListenConfig{ if err := router.Listener(listener, fiber.ListenConfig{
DisableStartupMessage: true, DisableStartupMessage: true,
BeforeServeFunc: func(app *fiber.App) error { BeforeServeFunc: func(app *fiber.App) error {