Three issues prevented transparent proxying from working end-to-end: 1. bwrap dropped CAP_NET_ADMIN before exec, so ip tuntap/link commands failed inside the sandbox. Add --cap-add CAP_NET_ADMIN and CAP_NET_BIND_SERVICE when transparent proxy is active. 2. tun2socks only offered SOCKS5 no-auth (method 0x00), but many proxies (e.g. gost) require username/password auth (method 0x02). Pass through credentials from the proxy URL so tun2socks offers both auth methods. 3. DNS resolution failed because UDP DNS needs SOCKS5 UDP ASSOCIATE which most proxies don't support. Add --dns flag and DnsBridge that routes DNS queries from the sandbox through a Unix socket to a host-side DNS server. Falls back to TCP relay through the tunnel when no --dns is set. Also brings up loopback interface (ip link set lo up) inside the network namespace so socat can bind to 127.0.0.1.
159 lines
4.3 KiB
Go
159 lines
4.3 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/Use-Tusk/fence/internal/config"
|
|
"github.com/Use-Tusk/fence/internal/platform"
|
|
)
|
|
|
|
// Manager handles sandbox initialization and command wrapping.
|
|
type Manager struct {
|
|
config *config.Config
|
|
proxyBridge *ProxyBridge
|
|
dnsBridge *DnsBridge
|
|
reverseBridge *ReverseBridge
|
|
tun2socksPath string // path to extracted tun2socks binary on host
|
|
exposedPorts []int
|
|
debug bool
|
|
monitor bool
|
|
initialized bool
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)")
|
|
}
|
|
}
|
|
|
|
m.initialized = true
|
|
if m.config.Network.ProxyURL != "" {
|
|
m.logDebug("Sandbox manager initialized (proxy: %s)", m.config.Network.ProxyURL)
|
|
} 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.debug)
|
|
case platform.Linux:
|
|
return WrapCommandLinux(m.config, command, m.proxyBridge, m.dnsBridge, m.reverseBridge, m.tun2socksPath, m.debug)
|
|
default:
|
|
return "", fmt.Errorf("unsupported platform: %s", plat)
|
|
}
|
|
}
|
|
|
|
// Cleanup stops the proxies and cleans up resources.
|
|
func (m *Manager) Cleanup() {
|
|
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)
|
|
}
|
|
m.logDebug("Sandbox manager cleaned up")
|
|
}
|
|
|
|
func (m *Manager) logDebug(format string, args ...interface{}) {
|
|
if m.debug {
|
|
fmt.Fprintf(os.Stderr, "[fence] "+format+"\n", args...)
|
|
}
|
|
}
|