fix chroot
This commit is contained in:
@@ -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
108
main.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user