diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ac2e317..740a445 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# Fence Architecture +# Architecture Fence restricts network and filesystem access for arbitrary commands. It works by: @@ -36,6 +36,7 @@ fence/ │ ├── manager.go # Orchestrates sandbox lifecycle │ ├── macos.go # macOS sandbox-exec profiles │ ├── linux.go # Linux bubblewrap + socat bridges +│ ├── monitor.go # macOS log stream violation monitoring │ ├── dangerous.go # Protected file/directory lists │ └── utils.go # Path normalization, shell quoting └── pkg/fence/ # Public Go API @@ -241,8 +242,63 @@ flowchart TD | Proxy routing | Environment variables | socat bridges + env vars | | Filesystem control | Profile rules | Bind mounts | | Inbound connections | Profile rules (`network-bind`) | Reverse socat bridges | +| Violation monitoring | log stream + proxy | proxy only | | Requirements | Built-in | bwrap, socat | +## Violation Monitoring + +The `-m` (monitor) flag enables real-time visibility into blocked operations. + +### Output Prefixes + +| Prefix | Source | Description | +|--------|--------|-------------| +| `[fence:http]` | Both | HTTP/HTTPS proxy (blocked requests only in monitor mode) | +| `[fence:socks]` | Both | SOCKS5 proxy (blocked requests only in monitor mode) | +| `[fence:logstream]` | macOS only | Kernel-level sandbox violations from `log stream` | +| `[fence:filter]` | Both | Domain filter rule matches (debug mode only) | + +### macOS Log Stream + +On macOS, fence spawns `log stream` with a predicate to capture sandbox violations: + +```bash +log stream --predicate 'eventMessage ENDSWITH "_SBX"' --style compact +``` + +Violations include: + +- `network-outbound` - blocked network connections +- `file-read*` - blocked file reads +- `file-write*` - blocked file writes + +Filtered out (too noisy): + +- `mach-lookup` - IPC service lookups +- `file-ioctl` - device control operations +- `/dev/tty*` writes - terminal output +- `mDNSResponder` - system DNS resolution +- `/private/var/run/syslog` - system logging + +### Linux Limitations + +Linux uses network namespace isolation (`--unshare-net`), which prevents connections at the namespace level rather than logging them. There's no kernel-level violation stream equivalent to macOS. + +With `-m` on Linux, you only see proxy-level denials: + +```text +[fence:http] 14:30:01 ✗ CONNECT 403 evil.com (blocked by proxy) +[fence:socks] 14:30:02 ✗ CONNECT evil.com:22 BLOCKED +``` + +### Debug vs Monitor Mode + +| Flag | Proxy logs | Filter rules | Log stream | Sandbox command | +|------|------------|--------------|------------|-----------------| +| `-m` | Blocked only | No | Yes (macOS) | No | +| `-d` | All | Yes | No | Yes | +| `-m -d` | All | Yes | Yes (macOS) | Yes | + ## Security Model ### How Each Layer Works @@ -268,7 +324,7 @@ Access control follows a deny-by-default model for writes: #### Dangerous File Protection -Certain paths are always protected regardless of config to prevent common attack vectors: +Certain paths are always protected from writes regardless of config to prevent common attack vectors: - Shell configs: `.bashrc`, `.zshrc`, `.profile`, `.bash_profile` - Git hooks: `.git/hooks/*` (can execute arbitrary code on git operations) @@ -329,10 +385,3 @@ Apple deprecated `sandbox-exec` but it still works on current macOS (including S #### Not for hostile code containment Fence is defense-in-depth for running semi-trusted code (npm install, build scripts, CI jobs), not a security boundary against actively malicious software designed to escape sandboxes. - -## Dependencies - -- `github.com/spf13/cobra` - CLI framework -- `github.com/things-go/go-socks5` - SOCKS5 proxy implementation -- `bubblewrap` (Linux) - Unprivileged sandboxing -- `socat` (Linux) - Socket relay for namespace bridging diff --git a/README.md b/README.md index 634925c..dc65fb0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Go implementation of process sandboxing with network and filesystem restrictions. -`fence` wraps arbitrary commands in a security sandbox, blocking network access by default and restricting filesystem operations based on configurable rules. +**`fence`** wraps arbitrary commands in a security sandbox, blocking network access by default and restricting filesystem operations based on configurable rules. > [!NOTE] > This is still a work in progress and may see significant changes. @@ -12,9 +12,11 @@ A Go implementation of process sandboxing with network and filesystem restrictio - **Network Isolation**: All network access blocked by default - **Domain Allowlisting**: Configure which domains are allowed - **Filesystem Restrictions**: Control read/write access to paths +- **Violation Monitoring**: Real-time logging of blocked requests and sandbox denials - **Cross-Platform**: macOS (sandbox-exec) and Linux (bubblewrap) - **HTTP/SOCKS5 Proxies**: Built-in filtering proxies for domain control -- **Library + CLI**: Use as a Go package or command-line tool + +You can use **`fence`** as a Go package or CLI tool. ## Installation @@ -87,7 +89,9 @@ fence [flags] [command...] Flags: -c string Run command string directly (like sh -c) - -d, --debug Enable debug logging + -d, --debug Enable debug logging (shows sandbox command, proxy activity, filter rules) + -m, --monitor Monitor mode (shows blocked requests and violations only) + -p, --port Expose port for inbound connections (can be repeated) -s, --settings Path to settings file (default: ~/.fence.json) -h, --help Help for fence ``` @@ -107,6 +111,12 @@ fence -c "git clone https://github.com/user/repo && cd repo && npm install" # Debug mode shows proxy activity fence -d wget https://example.com + +# Monitor mode shows violations/blocked requests only +fence -m npm install + +# Expose a port for inbound connections +fence -p 3000 -c "npm run dev" ``` ## Library Usage @@ -130,8 +140,8 @@ func main() { }, } - // Create manager - manager := fence.NewManager(cfg, false) + // Create manager (debug=false, monitor=false) + manager := fence.NewManager(cfg, false, false) defer manager.Cleanup() // Initialize (starts proxies) @@ -180,7 +190,7 @@ For detailed security model, limitations, and architecture, see [ARCHITECTURE.md ### Linux -- `bubblewrap` (bwrap) +- `bubblewrap` (for sandboxing) - `socat` (for network bridging) Install on Ubuntu/Debian: diff --git a/cmd/fence/main.go b/cmd/fence/main.go index 629cc40..a4e8b95 100644 --- a/cmd/fence/main.go +++ b/cmd/fence/main.go @@ -17,6 +17,7 @@ import ( var ( debug bool + monitor bool settingsPath string cmdString string exposePorts []string @@ -60,6 +61,7 @@ Configuration file format (~/.fence.json): } rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging") + rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations (macOS: log stream, all: proxy denials)") rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: ~/.fence.json)") rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)") rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)") @@ -117,7 +119,7 @@ func runCommand(cmd *cobra.Command, args []string) error { cfg = config.Default() } - manager := sandbox.NewManager(cfg, debug) + manager := sandbox.NewManager(cfg, debug, monitor) manager.SetExposedPorts(ports) defer manager.Cleanup() @@ -125,6 +127,18 @@ func runCommand(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to initialize sandbox: %w", err) } + var logMonitor *sandbox.LogMonitor + if monitor { + logMonitor = sandbox.NewLogMonitor(sandbox.GetSessionSuffix()) + if logMonitor != nil { + if err := logMonitor.Start(); err != nil { + fmt.Fprintf(os.Stderr, "[fence] Warning: failed to start log monitor: %v\n", err) + } else { + defer logMonitor.Stop() + } + } + } + sandboxedCommand, err := manager.WrapCommand(command) if err != nil { return fmt.Errorf("failed to wrap command: %w", err) diff --git a/internal/proxy/http.go b/internal/proxy/http.go index 8b463a0..495b792 100644 --- a/internal/proxy/http.go +++ b/internal/proxy/http.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "sync" "time" @@ -24,15 +25,19 @@ type HTTPProxy struct { listener net.Listener filter FilterFunc debug bool + monitor bool mu sync.RWMutex running bool } // NewHTTPProxy creates a new HTTP proxy with the given filter. -func NewHTTPProxy(filter FilterFunc, debug bool) *HTTPProxy { +// If monitor is true, only blocked requests are logged. +// If debug is true, all requests and filter rules are logged. +func NewHTTPProxy(filter FilterFunc, debug, monitor bool) *HTTPProxy { return &HTTPProxy{ - filter: filter, - debug: debug, + filter: filter, + debug: debug, + monitor: monitor, } } @@ -95,6 +100,7 @@ func (p *HTTPProxy) handleRequest(w http.ResponseWriter, r *http.Request) { // handleConnect handles HTTPS CONNECT requests (tunnel). func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) { + start := time.Now() host, portStr, err := net.SplitHostPort(r.Host) if err != nil { host = r.Host @@ -108,11 +114,13 @@ func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) { // Check if allowed if !p.filter(host, port) { - p.logDebug("CONNECT blocked: %s:%d", host, port) + p.logRequest("CONNECT", fmt.Sprintf("https://%s:%d", host, port), host, 403, "BLOCKED", time.Since(start)) http.Error(w, "Connection blocked by network allowlist", http.StatusForbidden) return } + p.logRequest("CONNECT", fmt.Sprintf("https://%s:%d", host, port), host, 200, "ALLOWED", time.Since(start)) + // Connect to target targetConn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 10*time.Second) if err != nil { @@ -157,6 +165,7 @@ func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) { // handleHTTP handles regular HTTP proxy requests. func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() targetURL, err := url.Parse(r.RequestURI) if err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) @@ -172,7 +181,7 @@ func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) { } if !p.filter(host, port) { - p.logDebug("HTTP blocked: %s:%d", host, port) + p.logRequest(r.Method, r.RequestURI, host, 403, "BLOCKED", time.Since(start)) http.Error(w, "Connection blocked by network allowlist", http.StatusForbidden) return } @@ -203,7 +212,7 @@ func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) { resp, err := client.Do(proxyReq) if err != nil { - p.logDebug("HTTP request failed: %v", err) + p.logRequest(r.Method, r.RequestURI, host, 502, "ERROR", time.Since(start)) http.Error(w, "Bad Gateway", http.StatusBadGateway) return } @@ -218,21 +227,57 @@ func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) + + p.logRequest(r.Method, r.RequestURI, host, resp.StatusCode, "ALLOWED", time.Since(start)) } func (p *HTTPProxy) logDebug(format string, args ...interface{}) { if p.debug { - fmt.Printf("[fence:http] "+format+"\n", args...) + fmt.Fprintf(os.Stderr, "[fence:http] "+format+"\n", args...) } } +// logRequest logs a detailed request entry. +// In monitor mode (-m), only blocked/error requests are logged. +// In debug mode (-d), all requests are logged. +func (p *HTTPProxy) logRequest(method, url, host string, status int, action string, duration time.Duration) { + isBlocked := action == "BLOCKED" || action == "ERROR" + + if p.monitor && !p.debug && !isBlocked { + return + } + + if !p.debug && !p.monitor { + return + } + + timestamp := time.Now().Format("15:04:05") + statusIcon := "✓" + switch action { + case "BLOCKED": + statusIcon = "✗" + case "ERROR": + statusIcon = "!" + } + fmt.Fprintf(os.Stderr, "[fence:http] %s %s %-7s %d %s %s (%v)\n", timestamp, statusIcon, method, status, host, truncateURL(url, 60), duration.Round(time.Millisecond)) +} + +// truncateURL shortens a URL for display. +func truncateURL(url string, maxLen int) string { + if len(url) <= maxLen { + return url + } + return url[:maxLen-3] + "..." +} + // CreateDomainFilter creates a filter function from a config. +// When debug is true, logs filter rule matches to stderr. func CreateDomainFilter(cfg *config.Config, debug bool) FilterFunc { return func(host string, port int) bool { if cfg == nil { // No config = deny all if debug { - fmt.Printf("[fence:filter] No config, denying: %s:%d\n", host, port) + fmt.Fprintf(os.Stderr, "[fence:filter] No config, denying: %s:%d\n", host, port) } return false } @@ -241,7 +286,7 @@ func CreateDomainFilter(cfg *config.Config, debug bool) FilterFunc { for _, denied := range cfg.Network.DeniedDomains { if config.MatchesDomain(host, denied) { if debug { - fmt.Printf("[fence:filter] Denied by rule: %s:%d (matched %s)\n", host, port, denied) + fmt.Fprintf(os.Stderr, "[fence:filter] Denied by rule: %s:%d (matched %s)\n", host, port, denied) } return false } @@ -251,14 +296,14 @@ func CreateDomainFilter(cfg *config.Config, debug bool) FilterFunc { for _, allowed := range cfg.Network.AllowedDomains { if config.MatchesDomain(host, allowed) { if debug { - fmt.Printf("[fence:filter] Allowed by rule: %s:%d (matched %s)\n", host, port, allowed) + fmt.Fprintf(os.Stderr, "[fence:filter] Allowed by rule: %s:%d (matched %s)\n", host, port, allowed) } return true } } if debug { - fmt.Printf("[fence:filter] No matching rule, denying: %s:%d\n", host, port) + fmt.Fprintf(os.Stderr, "[fence:filter] No matching rule, denying: %s:%d\n", host, port) } return false } diff --git a/internal/proxy/socks.go b/internal/proxy/socks.go index dc725d8..5f514d8 100644 --- a/internal/proxy/socks.go +++ b/internal/proxy/socks.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "os" + "time" "github.com/things-go/go-socks5" ) @@ -14,21 +16,26 @@ type SOCKSProxy struct { listener net.Listener filter FilterFunc debug bool + monitor bool port int } // NewSOCKSProxy creates a new SOCKS5 proxy with the given filter. -func NewSOCKSProxy(filter FilterFunc, debug bool) *SOCKSProxy { +// If monitor is true, only blocked connections are logged. +// If debug is true, all connections are logged. +func NewSOCKSProxy(filter FilterFunc, debug, monitor bool) *SOCKSProxy { return &SOCKSProxy{ - filter: filter, - debug: debug, + filter: filter, + debug: debug, + monitor: monitor, } } // fenceRuleSet implements socks5.RuleSet for domain filtering. type fenceRuleSet struct { - filter FilterFunc - debug bool + filter FilterFunc + debug bool + monitor bool } func (r *fenceRuleSet) Allow(ctx context.Context, req *socks5.Request) (context.Context, bool) { @@ -39,11 +46,14 @@ func (r *fenceRuleSet) Allow(ctx context.Context, req *socks5.Request) (context. port := req.DestAddr.Port allowed := r.filter(host, port) - if r.debug { + + shouldLog := r.debug || (r.monitor && !allowed) + if shouldLog { + timestamp := time.Now().Format("15:04:05") if allowed { - fmt.Printf("[fence:socks] Allowed: %s:%d\n", host, port) + fmt.Fprintf(os.Stderr, "[fence:socks] %s ✓ CONNECT %s:%d ALLOWED\n", timestamp, host, port) } else { - fmt.Printf("[fence:socks] Blocked: %s:%d\n", host, port) + fmt.Fprintf(os.Stderr, "[fence:socks] %s ✗ CONNECT %s:%d BLOCKED\n", timestamp, host, port) } } return ctx, allowed @@ -61,8 +71,9 @@ func (p *SOCKSProxy) Start() (int, error) { server := socks5.NewServer( socks5.WithRule(&fenceRuleSet{ - filter: p.filter, - debug: p.debug, + filter: p.filter, + debug: p.debug, + monitor: p.monitor, }), ) p.server = server @@ -70,13 +81,13 @@ func (p *SOCKSProxy) Start() (int, error) { go func() { if err := p.server.Serve(p.listener); err != nil { if p.debug { - fmt.Printf("[fence:socks] Server error: %v\n", err) + fmt.Fprintf(os.Stderr, "[fence:socks] Server error: %v\n", err) } } }() if p.debug { - fmt.Printf("[fence:socks] SOCKS5 proxy listening on localhost:%d\n", p.port) + fmt.Fprintf(os.Stderr, "[fence:socks] SOCKS5 proxy listening on localhost:%d\n", p.port) } return p.port, nil } diff --git a/internal/sandbox/manager.go b/internal/sandbox/manager.go index 52cc7f0..8598deb 100644 --- a/internal/sandbox/manager.go +++ b/internal/sandbox/manager.go @@ -20,14 +20,16 @@ type Manager struct { socksPort int exposedPorts []int debug bool + monitor bool initialized bool } // NewManager creates a new sandbox manager. -func NewManager(cfg *config.Config, debug bool) *Manager { +func NewManager(cfg *config.Config, debug, monitor bool) *Manager { return &Manager{ - config: cfg, - debug: debug, + config: cfg, + debug: debug, + monitor: monitor, } } @@ -48,14 +50,14 @@ func (m *Manager) Initialize() error { filter := proxy.CreateDomainFilter(m.config, m.debug) - m.httpProxy = proxy.NewHTTPProxy(filter, m.debug) + m.httpProxy = proxy.NewHTTPProxy(filter, m.debug, m.monitor) httpPort, err := m.httpProxy.Start() if err != nil { return fmt.Errorf("failed to start HTTP proxy: %w", err) } m.httpPort = httpPort - m.socksProxy = proxy.NewSOCKSProxy(filter, m.debug) + m.socksProxy = proxy.NewSOCKSProxy(filter, m.debug, m.monitor) socksPort, err := m.socksProxy.Start() if err != nil { m.httpProxy.Stop() diff --git a/internal/sandbox/monitor.go b/internal/sandbox/monitor.go new file mode 100644 index 0000000..a175560 --- /dev/null +++ b/internal/sandbox/monitor.go @@ -0,0 +1,198 @@ +// Package sandbox provides sandboxing functionality for macOS and Linux. +package sandbox + +import ( + "bufio" + "context" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + "time" + + "github.com/Use-Tusk/fence/internal/platform" +) + +// LogMonitor monitors sandbox violations via macOS log stream. +type LogMonitor struct { + sessionSuffix string + cmd *exec.Cmd + cancel context.CancelFunc + running bool +} + +// NewLogMonitor creates a new log monitor for the given session suffix. +// Returns nil on non-macOS platforms. +func NewLogMonitor(sessionSuffix string) *LogMonitor { + if platform.Detect() != platform.MacOS { + return nil + } + return &LogMonitor{ + sessionSuffix: sessionSuffix, + } +} + +// Start begins monitoring the macOS unified log for sandbox violations. +func (m *LogMonitor) Start() error { + if m == nil { + return nil + } + + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + // Build predicate to filter for our session's violations + // Note: We use the broader "_SBX" suffix to ensure we capture events + // even if there's a slight delay in log delivery + predicate := `eventMessage ENDSWITH "_SBX"` + + m.cmd = exec.CommandContext(ctx, "log", "stream", + "--predicate", predicate, + "--style", "compact", + ) + + stdout, err := m.cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %w", err) + } + + if err := m.cmd.Start(); err != nil { + return fmt.Errorf("failed to start log stream: %w", err) + } + + m.running = true + + // Parse log output in background + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + if violation := parseViolation(line); violation != "" { + fmt.Fprintf(os.Stderr, "%s\n", violation) + } + } + }() + + // Give log stream a moment to initialize + time.Sleep(100 * time.Millisecond) + + return nil +} + +// Stop stops the log monitor. +func (m *LogMonitor) Stop() { + if m == nil || !m.running { + return + } + + // Give a moment for any pending events to be processed + time.Sleep(500 * time.Millisecond) + + if m.cancel != nil { + m.cancel() + } + + if m.cmd != nil && m.cmd.Process != nil { + m.cmd.Process.Kill() + m.cmd.Wait() + } + + m.running = false +} + +// violationPattern matches sandbox denial log entries +var violationPattern = regexp.MustCompile(`Sandbox: (\w+)\((\d+)\) deny\(\d+\) (\S+)(.*)`) + +// parseViolation extracts and formats a sandbox violation from a log line. +// Returns empty string if the line should be filtered out. +func parseViolation(line string) string { + // Skip header lines + if strings.HasPrefix(line, "Filtering") || strings.HasPrefix(line, "Timestamp") { + return "" + } + + // Skip duplicate report summaries + if strings.Contains(line, "duplicate report") { + return "" + } + + // Skip CMD64 marker lines (they follow the actual violation) + if strings.HasPrefix(line, "CMD64_") { + return "" + } + + // Match violation pattern + matches := violationPattern.FindStringSubmatch(line) + if matches == nil { + return "" + } + + process := matches[1] + pid := matches[2] + operation := matches[3] + details := strings.TrimSpace(matches[4]) + + // Filter: only show network and file operations + if !shouldShowViolation(operation) { + return "" + } + + // Filter out noisy violations + if isNoisyViolation(operation, details) { + return "" + } + + // Format the output + timestamp := time.Now().Format("15:04:05") + + if details != "" { + return fmt.Sprintf("[fence:logstream] %s ✗ %s %s (%s:%s)", timestamp, operation, details, process, pid) + } + return fmt.Sprintf("[fence:logstream] %s ✗ %s (%s:%s)", timestamp, operation, process, pid) +} + +// shouldShowViolation returns true if this violation type should be displayed. +func shouldShowViolation(operation string) bool { + // Show network violations + if strings.HasPrefix(operation, "network-") { + return true + } + + // Show file read/write violations + if strings.HasPrefix(operation, "file-read") || + strings.HasPrefix(operation, "file-write") { + return true + } + + // Filter out everything else (mach-lookup, file-ioctl, etc.) + return false +} + +// isNoisyViolation returns true if this violation is system noise that should be filtered. +func isNoisyViolation(operation, details string) bool { + // Filter out TTY/terminal writes (very noisy from any process that prints output) + if strings.HasPrefix(details, "/dev/tty") || + strings.HasPrefix(details, "/dev/pts") { + return true + } + + // Filter out mDNSResponder (system DNS resolution socket) + if strings.Contains(details, "mDNSResponder") { + return true + } + + // Filter out other system sockets that are typically noise + if strings.HasPrefix(details, "/private/var/run/syslog") { + return true + } + + return false +} + +// GetSessionSuffix returns the session suffix used for filtering. +// This is the same suffix used in macOS sandbox-exec profiles. +func GetSessionSuffix() string { + return sessionSuffix // defined in macos.go +} + diff --git a/pkg/fence/fence.go b/pkg/fence/fence.go index ceed945..2be24e3 100644 --- a/pkg/fence/fence.go +++ b/pkg/fence/fence.go @@ -19,8 +19,10 @@ type FilesystemConfig = config.FilesystemConfig type Manager = sandbox.Manager // NewManager creates a new sandbox manager. -func NewManager(cfg *Config, debug bool) *Manager { - return sandbox.NewManager(cfg, debug) +// If debug is true, verbose logging is enabled. +// If monitor is true, only violations (blocked requests) are logged. +func NewManager(cfg *Config, debug, monitor bool) *Manager { + return sandbox.NewManager(cfg, debug, monitor) } // DefaultConfig returns the default configuration with all network blocked.