172 lines
4.3 KiB
Go
172 lines
4.3 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"ollie/util"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config represents the sandbox configuration
|
|
type Config struct {
|
|
General GeneralConfig `yaml:"general"`
|
|
Filesystem FilesystemConfig `yaml:"filesystem"`
|
|
Network NetworkConfig `yaml:"network"`
|
|
Env []string `yaml:"env"`
|
|
Advanced AdvancedConfig `yaml:"advanced"`
|
|
}
|
|
|
|
// GeneralConfig contains general sandbox settings
|
|
type GeneralConfig struct {
|
|
BestEffort bool `yaml:"best_effort"`
|
|
LogLevel string `yaml:"log_level"`
|
|
}
|
|
|
|
// FilesystemConfig contains filesystem permission settings
|
|
type FilesystemConfig struct {
|
|
RO []string `yaml:"ro"` // Read-only
|
|
ROX []string `yaml:"rox"` // Read-only with execute
|
|
RW []string `yaml:"rw"` // Read-write
|
|
RWX []string `yaml:"rwx"` // Read-write with execute
|
|
}
|
|
|
|
// NetworkConfig contains network permission settings
|
|
type NetworkConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Unrestricted bool `yaml:"unrestricted"`
|
|
BindTCP []string `yaml:"bind_tcp"`
|
|
ConnectTCP []string `yaml:"connect_tcp"`
|
|
}
|
|
|
|
// AdvancedConfig contains advanced landrun settings
|
|
type AdvancedConfig struct {
|
|
LDD bool `yaml:"ldd"`
|
|
AddExec bool `yaml:"add_exec"`
|
|
}
|
|
|
|
// EnvFunc looks up an environment variable by name.
|
|
type EnvFunc func(string) string
|
|
|
|
// expandPath replaces template variables with actual values using getenv for lookups.
|
|
func expandPath(pattern, cwd string, getenv EnvFunc) string {
|
|
s := pattern
|
|
s = strings.ReplaceAll(s, "{CWD}", cwd)
|
|
|
|
re := regexp.MustCompile(`\{([A-Z_][A-Z0-9_]*)\}`)
|
|
s = re.ReplaceAllStringFunc(s, func(match string) string {
|
|
varName := match[1 : len(match)-1]
|
|
|
|
home := getenv("HOME")
|
|
switch varName {
|
|
case "HOME":
|
|
if home != "" {
|
|
return home
|
|
}
|
|
case "TMPDIR":
|
|
if tmpdir := getenv("TMPDIR"); tmpdir != "" {
|
|
return tmpdir
|
|
}
|
|
return "/tmp"
|
|
case "XDG_CONFIG_HOME":
|
|
if xdg := getenv("XDG_CONFIG_HOME"); xdg != "" {
|
|
return xdg
|
|
}
|
|
return filepath.Join(home, ".config")
|
|
case "XDG_DATA_HOME":
|
|
if xdg := getenv("XDG_DATA_HOME"); xdg != "" {
|
|
return xdg
|
|
}
|
|
return filepath.Join(home, ".local/share")
|
|
case "XDG_CACHE_HOME":
|
|
if xdg := getenv("XDG_CACHE_HOME"); xdg != "" {
|
|
return xdg
|
|
}
|
|
return filepath.Join(home, ".cache")
|
|
case "XDG_STATE_HOME":
|
|
if xdg := getenv("XDG_STATE_HOME"); xdg != "" {
|
|
return xdg
|
|
}
|
|
return filepath.Join(home, ".local/state")
|
|
case "XDG_RUNTIME_DIR":
|
|
if xdg := getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
|
return xdg
|
|
}
|
|
return fmt.Sprintf("/run/user/%d", os.Getuid())
|
|
}
|
|
|
|
if val := getenv(varName); val != "" {
|
|
return val
|
|
}
|
|
return match
|
|
})
|
|
|
|
s = util.ExpandHome(s)
|
|
return s
|
|
}
|
|
|
|
// checkPath checks if the given absolute path is allowed by the sandbox config.
|
|
// If write is true, the path must fall under an RW or RWX entry.
|
|
// If write is false, any entry (RO, ROX, RW, RWX) grants access.
|
|
func checkPath(cfg *Config, path string, write bool, cwd string, getenv EnvFunc) error {
|
|
cleaned := filepath.Clean(path)
|
|
if resolved, err := filepath.EvalSymlinks(cleaned); err == nil {
|
|
cleaned = resolved
|
|
} else {
|
|
// File may not exist yet (new file creation). Resolve parent.
|
|
if rp, err2 := filepath.EvalSymlinks(filepath.Dir(cleaned)); err2 == nil {
|
|
cleaned = filepath.Join(rp, filepath.Base(cleaned))
|
|
}
|
|
}
|
|
|
|
var allowed []string
|
|
expandAndSplit := func(entries []string) {
|
|
for _, p := range entries {
|
|
for _, ep := range strings.Split(expandPath(p, cwd, getenv), ":") {
|
|
if ep != "" {
|
|
allowed = append(allowed, ep)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expandAndSplit(cfg.Filesystem.RW)
|
|
expandAndSplit(cfg.Filesystem.RWX)
|
|
if !write {
|
|
expandAndSplit(cfg.Filesystem.RO)
|
|
expandAndSplit(cfg.Filesystem.ROX)
|
|
}
|
|
|
|
for _, root := range allowed {
|
|
if pathUnder(cleaned, root) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if write {
|
|
return fmt.Errorf("path outside sandbox (no write access): %s", path)
|
|
}
|
|
return fmt.Errorf("path outside sandbox (no read access): %s", path)
|
|
}
|
|
|
|
func pathUnder(path, root string) bool {
|
|
return path == root || strings.HasPrefix(path, root+string(filepath.Separator))
|
|
}
|
|
|
|
// LoadSandbox parses a sandbox config from r.
|
|
func LoadSandbox(r io.Reader) (*Config, error) {
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &cfg, nil
|
|
}
|