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"
"net/rpc"
"strings"
"time"
"github.com/gofiber/fiber/v3"
)
@@ -133,4 +132,3 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) {
// Exported symbol
var Plugin GLoomI
var Version = time.Now()

108
main.go
View File

@@ -67,6 +67,48 @@ func hasChangeUserAbility() bool {
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 {
UnixSocket string
Process *os.Process
@@ -151,12 +193,24 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
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 {
return nil, err
}
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{
@@ -180,6 +234,10 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
if !hasChangeUserAbility() {
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 {
@@ -189,21 +247,33 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
// if gloomi is built into the binary
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
// ensure the plugin directory exists
if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil {
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")
if err != nil {
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 {
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")
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 {
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 {
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 {
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)
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)
if status == "ready" {
slog.Debug("PluginHost ported ready", "pluginPath", pluginPath)
slog.Debug("PluginHost ready", "pluginPath", pluginPath, "hostPid", process.Pid)
break
} else if strings.HasPrefix(status, "Error: ") {
errorMessage := strings.TrimPrefix(status, "Error: ")
@@ -773,10 +843,8 @@ func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error {
}
func main() {
debug, err := strconv.ParseBool(os.Getenv("DEBUG"))
if err != nil {
debug = false
}
// no need to check error value, it will always be a valid bool
debug, _ := strconv.ParseBool(os.Getenv("DEBUG"))
level := slog.LevelInfo
if debug {
@@ -796,14 +864,20 @@ func main() {
defer gloom.Cleanup()
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")
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
import "github.com/gofiber/fiber/v3"
import (
"fmt"
"os"
"path/filepath"
"github.com/gofiber/fiber/v3"
)
type MyPlugin struct{}
@@ -16,6 +22,60 @@ func (p *MyPlugin) RegisterRoutes(router fiber.Router) {
router.Get("/hello", func(c fiber.Ctx) error {
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

View File

@@ -8,8 +8,10 @@ import (
"net"
"os"
"os/signal"
"os/user"
"path/filepath"
"plugin"
"strconv"
"strings"
"syscall"
@@ -147,51 +149,63 @@ func main() {
}
// 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)
nobodyUser, err := user.Lookup("nobody")
if err != nil {
Print("Error: failed to read plugin: %w", err)
Print("Error: failed to get nobody user: %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)
nobodyUid, err := strconv.Atoi(nobodyUser.Uid)
if err != nil {
Print("Error: failed to parse nobody uid: %w", err)
}
// if err := os.Chown(chrootDir, nobodyUid, nobodyGid); err != nil {
// Print("Error: failed to chown chroot directory: %w", err)
// }
nobodyGid, err := strconv.Atoi(nobodyUser.Gid)
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)
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.Chdir("/"); err != nil {
Print("Error: failed to chdir: %w", err)
}
// if err := syscall.Setuid(nobodyUid); err != nil {
// Print("Error: failed to setuid: %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)
@@ -233,6 +247,11 @@ func main() {
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{
DisableStartupMessage: true,
BeforeServeFunc: func(app *fiber.App) error {