Add NetworkConfig.AllowedDomains and DeniedDomains fields for controlling outbound connections by hostname. Deny rules are checked first (deny wins). When AllowedDomains is set, only matching domains are permitted. When only DeniedDomains is set, all domains except denied ones are allowed. Implement FilteringProxy that wraps gost HTTP proxy with domain enforcement via AllowConnect callback. Skip GreyHaven proxy/DNS defaults
262 lines
7.6 KiB
Go
262 lines
7.6 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"gitea.app.monadical.io/monadical/greywall/internal/config"
|
|
"gitea.app.monadical.io/monadical/greywall/internal/platform"
|
|
)
|
|
|
|
// Manager handles sandbox initialization and command wrapping.
|
|
type Manager struct {
|
|
config *config.Config
|
|
proxyBridge *ProxyBridge
|
|
dnsBridge *DnsBridge
|
|
reverseBridge *ReverseBridge
|
|
filterProxy *FilteringProxy
|
|
tun2socksPath string // path to extracted tun2socks binary on host
|
|
exposedPorts []int
|
|
debug bool
|
|
monitor bool
|
|
initialized bool
|
|
learning bool // learning mode: permissive sandbox with strace
|
|
straceLogPath string // host-side temp file for strace output
|
|
commandName string // name of the command being learned
|
|
}
|
|
|
|
// NewManager creates a new sandbox manager.
|
|
func NewManager(cfg *config.Config, debug, monitor bool) *Manager {
|
|
return &Manager{
|
|
config: cfg,
|
|
debug: debug,
|
|
monitor: monitor,
|
|
}
|
|
}
|
|
|
|
// SetExposedPorts sets the ports to expose for inbound connections.
|
|
func (m *Manager) SetExposedPorts(ports []int) {
|
|
m.exposedPorts = ports
|
|
}
|
|
|
|
// SetLearning enables or disables learning mode.
|
|
func (m *Manager) SetLearning(enabled bool) {
|
|
m.learning = enabled
|
|
}
|
|
|
|
// SetCommandName sets the command name for learning mode template generation.
|
|
func (m *Manager) SetCommandName(name string) {
|
|
m.commandName = name
|
|
}
|
|
|
|
// IsLearning returns whether learning mode is enabled.
|
|
func (m *Manager) IsLearning() bool {
|
|
return m.learning
|
|
}
|
|
|
|
// Initialize sets up the sandbox infrastructure.
|
|
func (m *Manager) Initialize() error {
|
|
if m.initialized {
|
|
return nil
|
|
}
|
|
|
|
if !platform.IsSupported() {
|
|
return fmt.Errorf("sandbox is not supported on platform: %s", platform.Detect())
|
|
}
|
|
|
|
// On Linux, set up proxy bridge and tun2socks if proxy is configured
|
|
if platform.Detect() == platform.Linux {
|
|
if m.config.Network.ProxyURL != "" {
|
|
// Extract embedded tun2socks binary
|
|
tun2socksPath, err := extractTun2Socks()
|
|
if err != nil {
|
|
m.logDebug("Failed to extract tun2socks: %v (will fall back to env-var proxying)", err)
|
|
} else {
|
|
m.tun2socksPath = tun2socksPath
|
|
}
|
|
|
|
// Create proxy bridge (socat: Unix socket -> external SOCKS5 proxy)
|
|
bridge, err := NewProxyBridge(m.config.Network.ProxyURL, m.debug)
|
|
if err != nil {
|
|
if m.tun2socksPath != "" {
|
|
_ = os.Remove(m.tun2socksPath)
|
|
}
|
|
return fmt.Errorf("failed to initialize proxy bridge: %w", err)
|
|
}
|
|
m.proxyBridge = bridge
|
|
|
|
// Create DNS bridge if a DNS server is configured
|
|
if m.config.Network.DnsAddr != "" {
|
|
dnsBridge, err := NewDnsBridge(m.config.Network.DnsAddr, m.debug)
|
|
if err != nil {
|
|
m.proxyBridge.Cleanup()
|
|
if m.tun2socksPath != "" {
|
|
_ = os.Remove(m.tun2socksPath)
|
|
}
|
|
return fmt.Errorf("failed to initialize DNS bridge: %w", err)
|
|
}
|
|
m.dnsBridge = dnsBridge
|
|
}
|
|
}
|
|
|
|
// Set up reverse bridge for exposed ports (inbound connections)
|
|
// Only needed when network namespace is available - otherwise they share the network
|
|
features := DetectLinuxFeatures()
|
|
if len(m.exposedPorts) > 0 && features.CanUnshareNet {
|
|
reverseBridge, err := NewReverseBridge(m.exposedPorts, m.debug)
|
|
if err != nil {
|
|
if m.proxyBridge != nil {
|
|
m.proxyBridge.Cleanup()
|
|
}
|
|
if m.tun2socksPath != "" {
|
|
_ = os.Remove(m.tun2socksPath)
|
|
}
|
|
return fmt.Errorf("failed to initialize reverse bridge: %w", err)
|
|
}
|
|
m.reverseBridge = reverseBridge
|
|
} else if len(m.exposedPorts) > 0 && m.debug {
|
|
m.logDebug("Skipping reverse bridge (no network namespace, ports accessible directly)")
|
|
}
|
|
}
|
|
|
|
// Start domain filtering proxy if allowedDomains/deniedDomains are configured
|
|
if m.config.Network.HasDomainFiltering() {
|
|
fp, err := NewFilteringProxy(&m.config.Network, m.debug)
|
|
if err != nil {
|
|
// Clean up any bridges that were already started
|
|
if m.reverseBridge != nil {
|
|
m.reverseBridge.Cleanup()
|
|
}
|
|
if m.dnsBridge != nil {
|
|
m.dnsBridge.Cleanup()
|
|
}
|
|
if m.proxyBridge != nil {
|
|
m.proxyBridge.Cleanup()
|
|
}
|
|
if m.tun2socksPath != "" {
|
|
_ = os.Remove(m.tun2socksPath)
|
|
}
|
|
return fmt.Errorf("failed to start filtering proxy: %w", err)
|
|
}
|
|
m.filterProxy = fp
|
|
m.logDebug("Domain filtering proxy started on %s", fp.Addr())
|
|
|
|
// Write Node.js proxy bootstrap script so fetch() honors HTTP_PROXY
|
|
if bootstrapPath, err := WriteNodeProxyBootstrap(); err != nil {
|
|
m.logDebug("Warning: failed to write Node.js proxy bootstrap: %v", err)
|
|
} else {
|
|
m.logDebug("Node.js proxy bootstrap written to %s", bootstrapPath)
|
|
}
|
|
}
|
|
|
|
m.initialized = true
|
|
if m.config.Network.ProxyURL != "" {
|
|
dnsInfo := "none"
|
|
if m.config.Network.DnsAddr != "" {
|
|
dnsInfo = m.config.Network.DnsAddr
|
|
}
|
|
m.logDebug("Sandbox manager initialized (proxy: %s, dns: %s)", m.config.Network.ProxyURL, dnsInfo)
|
|
} else {
|
|
m.logDebug("Sandbox manager initialized (no proxy, network blocked)")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WrapCommand wraps a command with sandbox restrictions.
|
|
// Returns an error if the command is blocked by policy.
|
|
func (m *Manager) WrapCommand(command string) (string, error) {
|
|
if !m.initialized {
|
|
if err := m.Initialize(); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Check if command is blocked by policy
|
|
if err := CheckCommand(command, m.config); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
plat := platform.Detect()
|
|
switch plat {
|
|
case platform.MacOS:
|
|
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.filterProxy, m.debug)
|
|
case platform.Linux:
|
|
if m.learning {
|
|
return m.wrapCommandLearning(command)
|
|
}
|
|
return WrapCommandLinux(m.config, command, m.proxyBridge, m.dnsBridge, m.reverseBridge, m.tun2socksPath, m.filterProxy, m.debug)
|
|
default:
|
|
return "", fmt.Errorf("unsupported platform: %s", plat)
|
|
}
|
|
}
|
|
|
|
// wrapCommandLearning creates a permissive sandbox with strace for learning mode.
|
|
func (m *Manager) wrapCommandLearning(command string) (string, error) {
|
|
// Create host-side temp file for strace output
|
|
tmpFile, err := os.CreateTemp("", "greywall-strace-*.log")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create strace log file: %w", err)
|
|
}
|
|
_ = tmpFile.Close()
|
|
m.straceLogPath = tmpFile.Name()
|
|
|
|
m.logDebug("Strace log file: %s", m.straceLogPath)
|
|
|
|
return WrapCommandLinuxWithOptions(m.config, command, m.proxyBridge, m.dnsBridge, m.reverseBridge, m.tun2socksPath, m.filterProxy, LinuxSandboxOptions{
|
|
UseLandlock: false, // Disabled: seccomp blocks ptrace which strace needs
|
|
UseSeccomp: false, // Disabled: conflicts with strace
|
|
UseEBPF: false,
|
|
Debug: m.debug,
|
|
Learning: true,
|
|
StraceLogPath: m.straceLogPath,
|
|
})
|
|
}
|
|
|
|
// GenerateLearnedTemplate generates a config template from the strace log collected during learning.
|
|
func (m *Manager) GenerateLearnedTemplate(cmdName string) (string, error) {
|
|
if m.straceLogPath == "" {
|
|
return "", fmt.Errorf("no strace log available (was learning mode enabled?)")
|
|
}
|
|
|
|
templatePath, err := GenerateLearnedTemplate(m.straceLogPath, cmdName, m.debug)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Clean up strace log since we've processed it
|
|
_ = os.Remove(m.straceLogPath)
|
|
m.straceLogPath = ""
|
|
|
|
return templatePath, nil
|
|
}
|
|
|
|
// Cleanup stops the proxies and cleans up resources.
|
|
func (m *Manager) Cleanup() {
|
|
if m.filterProxy != nil {
|
|
m.filterProxy.Shutdown()
|
|
}
|
|
if m.reverseBridge != nil {
|
|
m.reverseBridge.Cleanup()
|
|
}
|
|
if m.dnsBridge != nil {
|
|
m.dnsBridge.Cleanup()
|
|
}
|
|
if m.proxyBridge != nil {
|
|
m.proxyBridge.Cleanup()
|
|
}
|
|
if m.tun2socksPath != "" {
|
|
_ = os.Remove(m.tun2socksPath)
|
|
}
|
|
if m.straceLogPath != "" {
|
|
_ = os.Remove(m.straceLogPath)
|
|
m.straceLogPath = ""
|
|
}
|
|
m.logDebug("Sandbox manager cleaned up")
|
|
}
|
|
|
|
func (m *Manager) logDebug(format string, args ...interface{}) {
|
|
if m.debug {
|
|
fmt.Fprintf(os.Stderr, "[greywall] "+format+"\n", args...)
|
|
}
|
|
}
|