anonpenguin23 abcc23c4f3 refactor(monorepo): restructure repo with core, website, vault, os packages
- 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
2026-03-26 18:21:55 +02:00

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,
},
}
}