mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-27 20:14:13 +00:00
- add monorepo Makefile delegating to sub-projects - update CI workflows, GoReleaser, gitignore for new structure - revise README, CONTRIBUTING.md for monorepo overview - bump Go to 1.24
275 lines
7.3 KiB
Go
275 lines
7.3 KiB
Go
// Package sandbox manages service processes in isolated Linux namespaces.
|
|
//
|
|
// Each service runs with:
|
|
// - Separate mount namespace (CLONE_NEWNS) for filesystem isolation
|
|
// - Separate UTS namespace (CLONE_NEWUTS) for hostname isolation
|
|
// - Dedicated uid/gid (no root)
|
|
// - Read-only root filesystem except for the service's data directory
|
|
//
|
|
// NO PID namespace (CLONE_NEWPID) — services like RQLite and Olric become PID 1
|
|
// in a new PID namespace, which changes signal semantics (SIGTERM is ignored by default
|
|
// for PID 1). Mount + UTS namespaces provide sufficient isolation.
|
|
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"syscall"
|
|
)
|
|
|
|
// Config defines the sandbox parameters for a service.
|
|
type Config struct {
|
|
Name string // Human-readable name (e.g., "rqlite", "ipfs")
|
|
Binary string // Absolute path to the binary
|
|
Args []string // Command-line arguments
|
|
User uint32 // UID to run as
|
|
Group uint32 // GID to run as
|
|
DataDir string // Writable data directory
|
|
LogFile string // Path to log file
|
|
Seccomp SeccompMode // Seccomp enforcement mode
|
|
}
|
|
|
|
// Process represents a running sandboxed service.
|
|
type Process struct {
|
|
Config Config
|
|
cmd *exec.Cmd
|
|
}
|
|
|
|
// Start launches the service in an isolated namespace.
|
|
func Start(cfg Config) (*Process, error) {
|
|
// Write seccomp profile for this service
|
|
profilePath, err := WriteProfile(cfg.Name, cfg.Seccomp)
|
|
if err != nil {
|
|
log.Printf("WARNING: failed to write seccomp profile for %s: %v (running without seccomp)", cfg.Name, err)
|
|
} else {
|
|
modeStr := "enforce"
|
|
if cfg.Seccomp == SeccompAudit {
|
|
modeStr = "audit"
|
|
}
|
|
log.Printf("seccomp profile for %s written to %s (mode: %s)", cfg.Name, profilePath, modeStr)
|
|
}
|
|
|
|
cmd := exec.Command(cfg.Binary, cfg.Args...)
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Cloneflags: syscall.CLONE_NEWNS | // mount namespace
|
|
syscall.CLONE_NEWUTS, // hostname namespace
|
|
Credential: &syscall.Credential{
|
|
Uid: cfg.User,
|
|
Gid: cfg.Group,
|
|
},
|
|
}
|
|
|
|
// Redirect output to log file
|
|
if cfg.LogFile != "" {
|
|
logFile, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open log file %s: %w", cfg.LogFile, err)
|
|
}
|
|
cmd.Stdout = logFile
|
|
cmd.Stderr = logFile
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("failed to start %s: %w", cfg.Name, err)
|
|
}
|
|
|
|
log.Printf("started %s (PID %d, UID %d)", cfg.Name, cmd.Process.Pid, cfg.User)
|
|
|
|
return &Process{Config: cfg, cmd: cmd}, nil
|
|
}
|
|
|
|
// Stop sends SIGTERM to the process and waits for exit.
|
|
func (p *Process) Stop() error {
|
|
if p.cmd == nil || p.cmd.Process == nil {
|
|
return nil
|
|
}
|
|
|
|
log.Printf("stopping %s (PID %d)", p.Config.Name, p.cmd.Process.Pid)
|
|
|
|
if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
|
return fmt.Errorf("failed to signal %s: %w", p.Config.Name, err)
|
|
}
|
|
|
|
if err := p.cmd.Wait(); err != nil {
|
|
// Process exited with non-zero — not necessarily an error during shutdown
|
|
log.Printf("%s exited: %v", p.Config.Name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsRunning returns true if the process is still alive.
|
|
func (p *Process) IsRunning() bool {
|
|
if p.cmd == nil || p.cmd.Process == nil {
|
|
return false
|
|
}
|
|
// Signal 0 checks if the process exists
|
|
return p.cmd.Process.Signal(syscall.Signal(0)) == nil
|
|
}
|
|
|
|
// Supervisor manages the lifecycle of all sandboxed services.
|
|
type Supervisor struct {
|
|
mu sync.Mutex
|
|
processes map[string]*Process
|
|
}
|
|
|
|
// NewSupervisor creates a new service supervisor.
|
|
func NewSupervisor() *Supervisor {
|
|
return &Supervisor{
|
|
processes: make(map[string]*Process),
|
|
}
|
|
}
|
|
|
|
// StartAll launches all configured services in the correct dependency order.
|
|
// Order: RQLite → Olric → IPFS → IPFS Cluster → Gateway → CoreDNS
|
|
func (s *Supervisor) StartAll() error {
|
|
services := defaultServiceConfigs()
|
|
|
|
for _, cfg := range services {
|
|
proc, err := Start(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start %s: %w", cfg.Name, err)
|
|
}
|
|
s.mu.Lock()
|
|
s.processes[cfg.Name] = proc
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
log.Printf("all %d services started", len(services))
|
|
return nil
|
|
}
|
|
|
|
// StopAll stops all services in reverse order.
|
|
func (s *Supervisor) StopAll() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
// Stop in reverse dependency order
|
|
order := []string{"coredns", "gateway", "ipfs-cluster", "ipfs", "olric", "rqlite"}
|
|
for _, name := range order {
|
|
if proc, ok := s.processes[name]; ok {
|
|
if err := proc.Stop(); err != nil {
|
|
log.Printf("error stopping %s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RestartService restarts a single service by name.
|
|
func (s *Supervisor) RestartService(name string) error {
|
|
s.mu.Lock()
|
|
proc, exists := s.processes[name]
|
|
s.mu.Unlock()
|
|
|
|
if !exists {
|
|
return fmt.Errorf("service %s not found", name)
|
|
}
|
|
|
|
if err := proc.Stop(); err != nil {
|
|
log.Printf("error stopping %s for restart: %v", name, err)
|
|
}
|
|
|
|
newProc, err := Start(proc.Config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to restart %s: %w", name, err)
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.processes[name] = newProc
|
|
s.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStatus returns the running status of all services.
|
|
func (s *Supervisor) GetStatus() map[string]bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
status := make(map[string]bool)
|
|
for name, proc := range s.processes {
|
|
status[name] = proc.IsRunning()
|
|
}
|
|
return status
|
|
}
|
|
|
|
// defaultServiceConfigs returns the service configurations in startup order.
|
|
func defaultServiceConfigs() []Config {
|
|
const (
|
|
oramaDir = "/opt/orama/.orama"
|
|
binDir = "/opt/orama/bin"
|
|
logsDir = "/opt/orama/.orama/logs"
|
|
)
|
|
|
|
// Start in SeccompAudit mode to profile syscalls on sandbox.
|
|
// Switch to SeccompEnforce after capturing required syscalls in production.
|
|
mode := SeccompAudit
|
|
|
|
return []Config{
|
|
{
|
|
Name: "rqlite",
|
|
Binary: "/usr/local/bin/rqlited",
|
|
Args: []string{"-node-id", "1", "-http-addr", "0.0.0.0:4001", "-raft-addr", "0.0.0.0:4002", oramaDir + "/data/rqlite"},
|
|
User: 1001,
|
|
Group: 1001,
|
|
DataDir: oramaDir + "/data/rqlite",
|
|
LogFile: logsDir + "/rqlite.log",
|
|
Seccomp: mode,
|
|
},
|
|
{
|
|
Name: "olric",
|
|
Binary: "/usr/local/bin/olric-server",
|
|
Args: nil, // configured via OLRIC_SERVER_CONFIG env
|
|
User: 1002,
|
|
Group: 1002,
|
|
DataDir: oramaDir + "/data",
|
|
LogFile: logsDir + "/olric.log",
|
|
Seccomp: mode,
|
|
},
|
|
{
|
|
Name: "ipfs",
|
|
Binary: "/usr/local/bin/ipfs",
|
|
Args: []string{"daemon", "--enable-pubsub-experiment", "--repo-dir=" + oramaDir + "/data/ipfs/repo"},
|
|
User: 1003,
|
|
Group: 1003,
|
|
DataDir: oramaDir + "/data/ipfs",
|
|
LogFile: logsDir + "/ipfs.log",
|
|
Seccomp: mode,
|
|
},
|
|
{
|
|
Name: "ipfs-cluster",
|
|
Binary: "/usr/local/bin/ipfs-cluster-service",
|
|
Args: []string{"daemon", "--config", oramaDir + "/data/ipfs-cluster/service.json"},
|
|
User: 1004,
|
|
Group: 1004,
|
|
DataDir: oramaDir + "/data/ipfs-cluster",
|
|
LogFile: logsDir + "/ipfs-cluster.log",
|
|
Seccomp: mode,
|
|
},
|
|
{
|
|
Name: "gateway",
|
|
Binary: binDir + "/gateway",
|
|
Args: []string{"--config", oramaDir + "/configs/gateway.yaml"},
|
|
User: 1005,
|
|
Group: 1005,
|
|
DataDir: oramaDir,
|
|
LogFile: logsDir + "/gateway.log",
|
|
Seccomp: mode,
|
|
},
|
|
{
|
|
Name: "coredns",
|
|
Binary: "/usr/local/bin/coredns",
|
|
Args: []string{"-conf", "/etc/coredns/Corefile"},
|
|
User: 1006,
|
|
Group: 1006,
|
|
DataDir: oramaDir,
|
|
LogFile: logsDir + "/coredns.log",
|
|
Seccomp: mode,
|
|
},
|
|
}
|
|
}
|