Replace built-in proxies with tun2socks transparent proxying
Remove the built-in HTTP/SOCKS5 proxy servers and domain allowlist/denylist system. Instead, use tun2socks with a TUN device inside the network namespace to transparently route all TCP/UDP traffic through an external SOCKS5 proxy. This enables truly transparent proxying where any binary (Go, static, etc.) has its traffic routed through the proxy without needing to respect HTTP_PROXY/ALL_PROXY environment variables. The external proxy handles its own filtering. Key changes: - NetworkConfig: remove AllowedDomains/DeniedDomains/proxy ports, add ProxyURL - Delete internal/proxy/, internal/templates/, internal/importer/ - Embed tun2socks binary (downloaded at build time via Makefile) - Replace LinuxBridge with ProxyBridge (single Unix socket to external proxy) - Inner script sets up TUN device + tun2socks inside network namespace - Falls back to env-var proxying when TUN is unavailable - macOS: best-effort env-var proxying to external SOCKS5 proxy - CLI: remove --template/import, add --proxy flag - Feature detection: add ip/tun/tun2socks status to --linux-features
This commit is contained in:
@@ -312,9 +312,7 @@ func skipBenchIfSandboxed(b *testing.B) {
|
||||
|
||||
func benchConfig(workspace string) *config.Config {
|
||||
return &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{},
|
||||
},
|
||||
Network: config.NetworkConfig{},
|
||||
Filesystem: config.FilesystemConfig{
|
||||
AllowWrite: []string{workspace},
|
||||
},
|
||||
|
||||
0
internal/sandbox/bin/.gitkeep
Normal file
0
internal/sandbox/bin/.gitkeep
Normal file
@@ -34,16 +34,12 @@ func skipIfLandlockNotUsable(t *testing.T) {
|
||||
}
|
||||
|
||||
// assertNetworkBlocked verifies that a network command was blocked.
|
||||
// It checks for either a non-zero exit code OR the proxy's blocked message.
|
||||
// With no proxy configured, --unshare-net blocks all network at the kernel level.
|
||||
func assertNetworkBlocked(t *testing.T, result *SandboxTestResult) {
|
||||
t.Helper()
|
||||
blockedMessage := "Connection blocked by network allowlist"
|
||||
if result.Failed() {
|
||||
return // Command failed = blocked
|
||||
}
|
||||
if strings.Contains(result.Stdout, blockedMessage) || strings.Contains(result.Stderr, blockedMessage) {
|
||||
return // Proxy blocked the request
|
||||
}
|
||||
t.Errorf("expected network request to be blocked, but it succeeded\nstdout: %s\nstderr: %s",
|
||||
result.Stdout, result.Stderr)
|
||||
}
|
||||
@@ -277,7 +273,7 @@ func TestLinux_NetworkBlocksCurl(t *testing.T) {
|
||||
|
||||
workspace := createTempWorkspace(t)
|
||||
cfg := testConfigWithWorkspace(workspace)
|
||||
// No domains allowed = all network blocked
|
||||
// No proxy = all network blocked
|
||||
|
||||
result := runUnderSandboxWithTimeout(t, cfg, "curl -s --connect-timeout 2 --max-time 3 http://example.com", workspace, 10*time.Second)
|
||||
|
||||
@@ -344,18 +340,19 @@ func TestLinux_NetworkBlocksDevTcp(t *testing.T) {
|
||||
assertBlocked(t, result)
|
||||
}
|
||||
|
||||
// TestLinux_ProxyAllowsAllowedDomains verifies the proxy allows configured domains.
|
||||
func TestLinux_ProxyAllowsAllowedDomains(t *testing.T) {
|
||||
// TestLinux_TransparentProxyRoutesThroughSocks verifies traffic routes through SOCKS5 proxy.
|
||||
// This test requires a running SOCKS5 proxy and actual network connectivity.
|
||||
func TestLinux_TransparentProxyRoutesThroughSocks(t *testing.T) {
|
||||
skipIfAlreadySandboxed(t)
|
||||
skipIfCommandNotFound(t, "curl")
|
||||
|
||||
workspace := createTempWorkspace(t)
|
||||
cfg := testConfigWithNetwork("httpbin.org")
|
||||
cfg := testConfigWithProxy("socks5://localhost:1080")
|
||||
cfg.Filesystem.AllowWrite = []string{workspace}
|
||||
|
||||
// This test requires actual network - skip in CI if network is unavailable
|
||||
// This test requires actual network and a running SOCKS5 proxy
|
||||
if os.Getenv("FENCE_TEST_NETWORK") != "1" {
|
||||
t.Skip("skipping: set FENCE_TEST_NETWORK=1 to run network tests")
|
||||
t.Skip("skipping: set FENCE_TEST_NETWORK=1 to run network tests (requires SOCKS5 proxy on localhost:1080)")
|
||||
}
|
||||
|
||||
result := runUnderSandboxWithTimeout(t, cfg, "curl -s --connect-timeout 5 --max-time 10 https://httpbin.org/get", workspace, 15*time.Second)
|
||||
|
||||
@@ -211,18 +211,18 @@ func TestMacOS_NetworkBlocksNc(t *testing.T) {
|
||||
assertBlocked(t, result)
|
||||
}
|
||||
|
||||
// TestMacOS_ProxyAllowsAllowedDomains verifies the proxy allows configured domains.
|
||||
func TestMacOS_ProxyAllowsAllowedDomains(t *testing.T) {
|
||||
// TestMacOS_ProxyAllowsTrafficViaProxy verifies the proxy allows traffic via external proxy.
|
||||
func TestMacOS_ProxyAllowsTrafficViaProxy(t *testing.T) {
|
||||
skipIfAlreadySandboxed(t)
|
||||
skipIfCommandNotFound(t, "curl")
|
||||
|
||||
workspace := createTempWorkspace(t)
|
||||
cfg := testConfigWithNetwork("httpbin.org")
|
||||
cfg := testConfigWithProxy("socks5://localhost:1080")
|
||||
cfg.Filesystem.AllowWrite = []string{workspace}
|
||||
|
||||
// This test requires actual network - skip in CI if network is unavailable
|
||||
// This test requires actual network and a running SOCKS5 proxy
|
||||
if os.Getenv("FENCE_TEST_NETWORK") != "1" {
|
||||
t.Skip("skipping: set FENCE_TEST_NETWORK=1 to run network tests")
|
||||
t.Skip("skipping: set FENCE_TEST_NETWORK=1 to run network tests (requires SOCKS5 proxy on localhost:1080)")
|
||||
}
|
||||
|
||||
result := runUnderSandboxWithTimeout(t, cfg, "curl -s --connect-timeout 5 --max-time 10 https://httpbin.org/get", workspace, 15*time.Second)
|
||||
|
||||
@@ -125,10 +125,7 @@ func assertContains(t *testing.T, haystack, needle string) {
|
||||
// testConfig creates a test configuration with sensible defaults.
|
||||
func testConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{},
|
||||
DeniedDomains: []string{},
|
||||
},
|
||||
Network: config.NetworkConfig{},
|
||||
Filesystem: config.FilesystemConfig{
|
||||
DenyRead: []string{},
|
||||
AllowWrite: []string{},
|
||||
@@ -149,10 +146,10 @@ func testConfigWithWorkspace(workspacePath string) *config.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// testConfigWithNetwork creates a config that allows specific domains.
|
||||
func testConfigWithNetwork(domains ...string) *config.Config {
|
||||
// testConfigWithProxy creates a config with a proxy URL set.
|
||||
func testConfigWithProxy(proxyURL string) *config.Config {
|
||||
cfg := testConfig()
|
||||
cfg.Network.AllowedDomains = domains
|
||||
cfg.Network.ProxyURL = proxyURL
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -18,13 +18,13 @@ import (
|
||||
"github.com/Use-Tusk/fence/internal/config"
|
||||
)
|
||||
|
||||
// LinuxBridge holds the socat bridge processes for Linux sandboxing (outbound).
|
||||
type LinuxBridge struct {
|
||||
HTTPSocketPath string
|
||||
SOCKSSocketPath string
|
||||
httpProcess *exec.Cmd
|
||||
socksProcess *exec.Cmd
|
||||
debug bool
|
||||
// ProxyBridge bridges sandbox to an external SOCKS5 proxy via Unix socket.
|
||||
type ProxyBridge struct {
|
||||
SocketPath string // Unix socket path
|
||||
ProxyHost string // Parsed from ProxyURL
|
||||
ProxyPort string // Parsed from ProxyURL
|
||||
process *exec.Cmd
|
||||
debug bool
|
||||
}
|
||||
|
||||
// ReverseBridge holds the socat bridge processes for inbound connections.
|
||||
@@ -49,13 +49,18 @@ type LinuxSandboxOptions struct {
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// NewLinuxBridge creates Unix socket bridges to the proxy servers.
|
||||
// This allows sandboxed processes to communicate with the host's proxy (outbound).
|
||||
func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge, error) {
|
||||
// NewProxyBridge creates a Unix socket bridge to an external SOCKS5 proxy.
|
||||
// The bridge uses socat to forward from a Unix socket to the external proxy's TCP address.
|
||||
func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
|
||||
if _, err := exec.LookPath("socat"); err != nil {
|
||||
return nil, fmt.Errorf("socat is required on Linux but not found: %w", err)
|
||||
}
|
||||
|
||||
u, err := parseProxyURL(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid proxy URL: %w", err)
|
||||
}
|
||||
|
||||
id := make([]byte, 8)
|
||||
if _, err := rand.Read(id); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate socket ID: %w", err)
|
||||
@@ -63,49 +68,33 @@ func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge
|
||||
socketID := hex.EncodeToString(id)
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
httpSocketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-http-%s.sock", socketID))
|
||||
socksSocketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-socks-%s.sock", socketID))
|
||||
socketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-proxy-%s.sock", socketID))
|
||||
|
||||
bridge := &LinuxBridge{
|
||||
HTTPSocketPath: httpSocketPath,
|
||||
SOCKSSocketPath: socksSocketPath,
|
||||
debug: debug,
|
||||
bridge := &ProxyBridge{
|
||||
SocketPath: socketPath,
|
||||
ProxyHost: u.Hostname(),
|
||||
ProxyPort: u.Port(),
|
||||
debug: debug,
|
||||
}
|
||||
|
||||
// Start HTTP bridge: Unix socket -> TCP proxy
|
||||
httpArgs := []string{
|
||||
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", httpSocketPath),
|
||||
fmt.Sprintf("TCP:localhost:%d", httpProxyPort),
|
||||
// Start bridge: Unix socket -> external SOCKS5 proxy TCP
|
||||
socatArgs := []string{
|
||||
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socketPath),
|
||||
fmt.Sprintf("TCP:%s:%s", bridge.ProxyHost, bridge.ProxyPort),
|
||||
}
|
||||
bridge.httpProcess = exec.Command("socat", httpArgs...) //nolint:gosec // args constructed from trusted input
|
||||
bridge.process = exec.Command("socat", socatArgs...) //nolint:gosec // args constructed from trusted input
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Starting HTTP bridge: socat %s\n", strings.Join(httpArgs, " "))
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Starting proxy bridge: socat %s\n", strings.Join(socatArgs, " "))
|
||||
}
|
||||
if err := bridge.httpProcess.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start HTTP bridge: %w", err)
|
||||
if err := bridge.process.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start proxy bridge: %w", err)
|
||||
}
|
||||
|
||||
// Start SOCKS bridge: Unix socket -> TCP proxy
|
||||
socksArgs := []string{
|
||||
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socksSocketPath),
|
||||
fmt.Sprintf("TCP:localhost:%d", socksProxyPort),
|
||||
}
|
||||
bridge.socksProcess = exec.Command("socat", socksArgs...) //nolint:gosec // args constructed from trusted input
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Starting SOCKS bridge: socat %s\n", strings.Join(socksArgs, " "))
|
||||
}
|
||||
if err := bridge.socksProcess.Start(); err != nil {
|
||||
bridge.Cleanup()
|
||||
return nil, fmt.Errorf("failed to start SOCKS bridge: %w", err)
|
||||
}
|
||||
|
||||
// Wait for sockets to be created, up to 5 seconds
|
||||
// Wait for socket to be created, up to 5 seconds
|
||||
for range 50 {
|
||||
httpExists := fileExists(httpSocketPath)
|
||||
socksExists := fileExists(socksSocketPath)
|
||||
if httpExists && socksExists {
|
||||
if fileExists(socketPath) {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Bridges ready (HTTP: %s, SOCKS: %s)\n", httpSocketPath, socksSocketPath)
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Proxy bridge ready (%s)\n", socketPath)
|
||||
}
|
||||
return bridge, nil
|
||||
}
|
||||
@@ -113,29 +102,37 @@ func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge
|
||||
}
|
||||
|
||||
bridge.Cleanup()
|
||||
return nil, fmt.Errorf("timeout waiting for bridge sockets to be created")
|
||||
return nil, fmt.Errorf("timeout waiting for proxy bridge socket to be created")
|
||||
}
|
||||
|
||||
// Cleanup stops the bridge processes and removes socket files.
|
||||
func (b *LinuxBridge) Cleanup() {
|
||||
if b.httpProcess != nil && b.httpProcess.Process != nil {
|
||||
_ = b.httpProcess.Process.Kill()
|
||||
_ = b.httpProcess.Wait()
|
||||
// Cleanup stops the bridge process and removes the socket file.
|
||||
func (b *ProxyBridge) Cleanup() {
|
||||
if b.process != nil && b.process.Process != nil {
|
||||
_ = b.process.Process.Kill()
|
||||
_ = b.process.Wait()
|
||||
}
|
||||
if b.socksProcess != nil && b.socksProcess.Process != nil {
|
||||
_ = b.socksProcess.Process.Kill()
|
||||
_ = b.socksProcess.Wait()
|
||||
}
|
||||
|
||||
// Clean up socket files
|
||||
_ = os.Remove(b.HTTPSocketPath)
|
||||
_ = os.Remove(b.SOCKSSocketPath)
|
||||
_ = os.Remove(b.SocketPath)
|
||||
|
||||
if b.debug {
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Bridges cleaned up\n")
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Proxy bridge cleaned up\n")
|
||||
}
|
||||
}
|
||||
|
||||
// parseProxyURL parses a SOCKS5 proxy URL and returns the parsed URL.
|
||||
func parseProxyURL(proxyURL string) (*url.URL, error) {
|
||||
u, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Scheme != "socks5" && u.Scheme != "socks5h" {
|
||||
return nil, fmt.Errorf("proxy URL must use socks5:// or socks5h:// scheme, got %s", u.Scheme)
|
||||
}
|
||||
if u.Hostname() == "" || u.Port() == "" {
|
||||
return nil, fmt.Errorf("proxy URL must include hostname and port")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// NewReverseBridge creates Unix socket bridges for inbound connections.
|
||||
// Host listens on ports, forwards to Unix sockets that go into the sandbox.
|
||||
func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
|
||||
@@ -308,8 +305,8 @@ func getMandatoryDenyPaths(cwd string) []string {
|
||||
|
||||
// WrapCommandLinux wraps a command with Linux bubblewrap sandbox.
|
||||
// It uses available security features (Landlock, seccomp) with graceful fallback.
|
||||
func WrapCommandLinux(cfg *config.Config, command string, bridge *LinuxBridge, reverseBridge *ReverseBridge, debug bool) (string, error) {
|
||||
return WrapCommandLinuxWithOptions(cfg, command, bridge, reverseBridge, LinuxSandboxOptions{
|
||||
func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) {
|
||||
return WrapCommandLinuxWithOptions(cfg, command, proxyBridge, reverseBridge, tun2socksPath, LinuxSandboxOptions{
|
||||
UseLandlock: true, // Enabled by default, will fall back if not available
|
||||
UseSeccomp: true, // Enabled by default
|
||||
UseEBPF: true, // Enabled by default if available
|
||||
@@ -318,7 +315,7 @@ func WrapCommandLinux(cfg *config.Config, command string, bridge *LinuxBridge, r
|
||||
}
|
||||
|
||||
// WrapCommandLinuxWithOptions wraps a command with configurable sandbox options.
|
||||
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *LinuxBridge, reverseBridge *ReverseBridge, opts LinuxSandboxOptions) (string, error) {
|
||||
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) {
|
||||
if _, err := exec.LookPath("bwrap"); err != nil {
|
||||
return "", fmt.Errorf("bubblewrap (bwrap) is required on Linux but not found: %w", err)
|
||||
}
|
||||
@@ -336,19 +333,6 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Available features: %s\n", features.Summary())
|
||||
}
|
||||
|
||||
// Check if allowedDomains contains "*" (wildcard = allow all direct network)
|
||||
// In this mode, we skip network namespace isolation so apps that don't
|
||||
// respect HTTP_PROXY can make direct connections.
|
||||
hasWildcardAllow := false
|
||||
if cfg != nil {
|
||||
hasWildcardAllow = slices.Contains(cfg.Network.AllowedDomains, "*")
|
||||
}
|
||||
|
||||
if opts.Debug && hasWildcardAllow {
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Wildcard allowedDomains detected - allowing direct network connections\n")
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Note: deniedDomains only enforced for apps that respect HTTP_PROXY\n")
|
||||
}
|
||||
|
||||
// Build bwrap args with filesystem restrictions
|
||||
bwrapArgs := []string{
|
||||
"bwrap",
|
||||
@@ -356,13 +340,11 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
|
||||
"--die-with-parent",
|
||||
}
|
||||
|
||||
// Only use --unshare-net if:
|
||||
// 1. The environment supports it (has CAP_NET_ADMIN)
|
||||
// 2. We're NOT in wildcard mode (need direct network access)
|
||||
// Containerized environments (Docker, CI) often lack CAP_NET_ADMIN
|
||||
if features.CanUnshareNet && !hasWildcardAllow {
|
||||
// Always use --unshare-net when available (network namespace isolation)
|
||||
// Inside the namespace, tun2socks will provide transparent proxy access
|
||||
if features.CanUnshareNet {
|
||||
bwrapArgs = append(bwrapArgs, "--unshare-net") // Network namespace isolation
|
||||
} else if opts.Debug && !features.CanUnshareNet {
|
||||
} else if opts.Debug {
|
||||
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping --unshare-net (network namespace unavailable in this environment)\n")
|
||||
}
|
||||
|
||||
@@ -603,12 +585,19 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the outbound Unix sockets into the sandbox (need to be writable)
|
||||
if bridge != nil {
|
||||
// Bind the proxy bridge Unix socket into the sandbox (needs to be writable)
|
||||
if proxyBridge != nil {
|
||||
bwrapArgs = append(bwrapArgs,
|
||||
"--bind", bridge.HTTPSocketPath, bridge.HTTPSocketPath,
|
||||
"--bind", bridge.SOCKSSocketPath, bridge.SOCKSSocketPath,
|
||||
"--bind", proxyBridge.SocketPath, proxyBridge.SocketPath,
|
||||
)
|
||||
// Bind /dev/net/tun for TUN device creation inside the sandbox
|
||||
if features.HasDevNetTun {
|
||||
bwrapArgs = append(bwrapArgs, "--dev-bind", "/dev/net/tun", "/dev/net/tun")
|
||||
}
|
||||
// Bind the tun2socks binary into the sandbox (read-only)
|
||||
if tun2socksPath != "" {
|
||||
bwrapArgs = append(bwrapArgs, "--ro-bind", tun2socksPath, "/tmp/fence-tun2socks")
|
||||
}
|
||||
}
|
||||
|
||||
// Bind reverse socket directory if needed (sockets created inside sandbox)
|
||||
@@ -637,32 +626,48 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
|
||||
|
||||
bwrapArgs = append(bwrapArgs, "--", shellPath, "-c")
|
||||
|
||||
// Build the inner command that sets up socat listeners and runs the user command
|
||||
// Build the inner command that sets up tun2socks and runs the user command
|
||||
var innerScript strings.Builder
|
||||
|
||||
if bridge != nil {
|
||||
// Set up outbound socat listeners inside the sandbox
|
||||
innerScript.WriteString("export FENCE_SANDBOX=1\n")
|
||||
|
||||
if proxyBridge != nil && tun2socksPath != "" && features.CanUseTransparentProxy() {
|
||||
// Set up transparent proxy via TUN device + tun2socks
|
||||
innerScript.WriteString(fmt.Sprintf(`
|
||||
# Start HTTP proxy listener (port 3128 -> Unix socket -> host HTTP proxy)
|
||||
socat TCP-LISTEN:3128,fork,reuseaddr UNIX-CONNECT:%s >/dev/null 2>&1 &
|
||||
HTTP_PID=$!
|
||||
# Set up TUN device for transparent proxying
|
||||
ip tuntap add dev tun0 mode tun
|
||||
ip addr add 198.18.0.1/15 dev tun0
|
||||
ip link set dev tun0 up
|
||||
ip route add default via 198.18.0.1 dev tun0
|
||||
|
||||
# Start SOCKS proxy listener (port 1080 -> Unix socket -> host SOCKS proxy)
|
||||
socat TCP-LISTEN:1080,fork,reuseaddr UNIX-CONNECT:%s >/dev/null 2>&1 &
|
||||
SOCKS_PID=$!
|
||||
# Bridge: local port -> Unix socket -> host -> external SOCKS5 proxy
|
||||
PROXY_PORT=18321
|
||||
socat TCP-LISTEN:${PROXY_PORT},fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:%s >/dev/null 2>&1 &
|
||||
BRIDGE_PID=$!
|
||||
|
||||
# Set proxy environment variables
|
||||
export HTTP_PROXY=http://127.0.0.1:3128
|
||||
export HTTPS_PROXY=http://127.0.0.1:3128
|
||||
export http_proxy=http://127.0.0.1:3128
|
||||
export https_proxy=http://127.0.0.1:3128
|
||||
export ALL_PROXY=socks5h://127.0.0.1:1080
|
||||
export all_proxy=socks5h://127.0.0.1:1080
|
||||
# Start tun2socks (transparent proxy via gvisor netstack)
|
||||
/tmp/fence-tun2socks -device tun0 -proxy socks5://127.0.0.1:${PROXY_PORT} >/dev/null 2>&1 &
|
||||
TUN2SOCKS_PID=$!
|
||||
|
||||
`, proxyBridge.SocketPath))
|
||||
} else if proxyBridge != nil {
|
||||
// Fallback: no TUN support, use env-var-based proxying
|
||||
innerScript.WriteString(fmt.Sprintf(`
|
||||
# Set up SOCKS5 bridge (no TUN available, env-var-based proxying)
|
||||
PROXY_PORT=18321
|
||||
socat TCP-LISTEN:${PROXY_PORT},fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:%s >/dev/null 2>&1 &
|
||||
BRIDGE_PID=$!
|
||||
|
||||
export ALL_PROXY=socks5h://127.0.0.1:${PROXY_PORT}
|
||||
export all_proxy=socks5h://127.0.0.1:${PROXY_PORT}
|
||||
export HTTP_PROXY=socks5h://127.0.0.1:${PROXY_PORT}
|
||||
export HTTPS_PROXY=socks5h://127.0.0.1:${PROXY_PORT}
|
||||
export http_proxy=socks5h://127.0.0.1:${PROXY_PORT}
|
||||
export https_proxy=socks5h://127.0.0.1:${PROXY_PORT}
|
||||
export NO_PROXY=localhost,127.0.0.1
|
||||
export no_proxy=localhost,127.0.0.1
|
||||
export FENCE_SANDBOX=1
|
||||
|
||||
`, bridge.HTTPSocketPath, bridge.SOCKSSocketPath))
|
||||
`, proxyBridge.SocketPath))
|
||||
}
|
||||
|
||||
// Set up reverse (inbound) socat listeners inside the sandbox
|
||||
@@ -688,8 +693,8 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Small delay to ensure socat listeners are ready
|
||||
sleep 0.1
|
||||
# Small delay to ensure services are ready
|
||||
sleep 0.3
|
||||
|
||||
# Run the user command
|
||||
`)
|
||||
@@ -729,6 +734,11 @@ sleep 0.1
|
||||
} else {
|
||||
featureList = append(featureList, "bwrap(pid,fs)")
|
||||
}
|
||||
if proxyBridge != nil && features.CanUseTransparentProxy() {
|
||||
featureList = append(featureList, "tun2socks(transparent)")
|
||||
} else if proxyBridge != nil {
|
||||
featureList = append(featureList, "proxy(env-vars)")
|
||||
}
|
||||
if features.HasSeccomp && opts.UseSeccomp && seccompFilterPath != "" {
|
||||
featureList = append(featureList, "seccomp")
|
||||
}
|
||||
@@ -818,6 +828,9 @@ func PrintLinuxFeatures() {
|
||||
fmt.Printf(" Seccomp: %v (log level: %d)\n", features.HasSeccomp, features.SeccompLogLevel)
|
||||
fmt.Printf(" Landlock: %v (ABI v%d)\n", features.HasLandlock, features.LandlockABI)
|
||||
fmt.Printf(" eBPF: %v (CAP_BPF: %v, root: %v)\n", features.HasEBPF, features.HasCapBPF, features.HasCapRoot)
|
||||
fmt.Printf(" ip (iproute2): %v\n", features.HasIpCommand)
|
||||
fmt.Printf(" /dev/net/tun: %v\n", features.HasDevNetTun)
|
||||
fmt.Printf(" tun2socks: %v (embedded)\n", features.HasTun2Socks)
|
||||
|
||||
fmt.Printf("\nFeature Status:\n")
|
||||
if features.MinimumViable() {
|
||||
@@ -841,6 +854,12 @@ func PrintLinuxFeatures() {
|
||||
fmt.Printf(" This is common in Docker, GitHub Actions, and other CI systems.\n")
|
||||
}
|
||||
|
||||
if features.CanUseTransparentProxy() {
|
||||
fmt.Printf(" ✓ Transparent proxy available (tun2socks + TUN device)\n")
|
||||
} else {
|
||||
fmt.Printf(" ○ Transparent proxy not available (needs ip, /dev/net/tun, network namespace)\n")
|
||||
}
|
||||
|
||||
if features.CanUseLandlock() {
|
||||
fmt.Printf(" ✓ Landlock available for enhanced filesystem control\n")
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,11 @@ type LinuxFeatures struct {
|
||||
// This can be false in containerized environments (Docker, CI) without CAP_NET_ADMIN
|
||||
CanUnshareNet bool
|
||||
|
||||
// Transparent proxy support
|
||||
HasIpCommand bool // ip (iproute2) available
|
||||
HasDevNetTun bool // /dev/net/tun exists
|
||||
HasTun2Socks bool // tun2socks embedded binary available
|
||||
|
||||
// Kernel version
|
||||
KernelMajor int
|
||||
KernelMinor int
|
||||
@@ -74,6 +79,12 @@ func (f *LinuxFeatures) detect() {
|
||||
|
||||
// Check if we can create network namespaces
|
||||
f.detectNetworkNamespace()
|
||||
|
||||
// Check transparent proxy support
|
||||
f.HasIpCommand = commandExists("ip")
|
||||
_, err := os.Stat("/dev/net/tun")
|
||||
f.HasDevNetTun = err == nil
|
||||
f.HasTun2Socks = true // embedded binary, always available
|
||||
}
|
||||
|
||||
func (f *LinuxFeatures) parseKernelVersion() {
|
||||
@@ -255,6 +266,11 @@ func (f *LinuxFeatures) CanUseLandlock() bool {
|
||||
return f.HasLandlock && f.LandlockABI >= 1
|
||||
}
|
||||
|
||||
// CanUseTransparentProxy returns true if transparent proxying via tun2socks is possible.
|
||||
func (f *LinuxFeatures) CanUseTransparentProxy() bool {
|
||||
return f.HasIpCommand && f.HasDevNetTun && f.CanUnshareNet
|
||||
}
|
||||
|
||||
// MinimumViable returns true if the minimum required features are available.
|
||||
func (f *LinuxFeatures) MinimumViable() bool {
|
||||
return f.HasBwrap && f.HasSocat
|
||||
|
||||
@@ -15,6 +15,9 @@ type LinuxFeatures struct {
|
||||
HasCapBPF bool
|
||||
HasCapRoot bool
|
||||
CanUnshareNet bool
|
||||
HasIpCommand bool
|
||||
HasDevNetTun bool
|
||||
HasTun2Socks bool
|
||||
KernelMajor int
|
||||
KernelMinor int
|
||||
}
|
||||
@@ -39,6 +42,11 @@ func (f *LinuxFeatures) CanUseLandlock() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CanUseTransparentProxy returns false on non-Linux platforms.
|
||||
func (f *LinuxFeatures) CanUseTransparentProxy() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MinimumViable returns false on non-Linux platforms.
|
||||
func (f *LinuxFeatures) MinimumViable() bool {
|
||||
return false
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
"github.com/Use-Tusk/fence/internal/config"
|
||||
)
|
||||
|
||||
// LinuxBridge is a stub for non-Linux platforms.
|
||||
type LinuxBridge struct {
|
||||
HTTPSocketPath string
|
||||
SOCKSSocketPath string
|
||||
// ProxyBridge is a stub for non-Linux platforms.
|
||||
type ProxyBridge struct {
|
||||
SocketPath string
|
||||
ProxyHost string
|
||||
ProxyPort string
|
||||
}
|
||||
|
||||
// ReverseBridge is a stub for non-Linux platforms.
|
||||
@@ -29,13 +30,13 @@ type LinuxSandboxOptions struct {
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// NewLinuxBridge returns an error on non-Linux platforms.
|
||||
func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge, error) {
|
||||
return nil, fmt.Errorf("Linux bridge not available on this platform")
|
||||
// NewProxyBridge returns an error on non-Linux platforms.
|
||||
func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
|
||||
return nil, fmt.Errorf("proxy bridge not available on this platform")
|
||||
}
|
||||
|
||||
// Cleanup is a no-op on non-Linux platforms.
|
||||
func (b *LinuxBridge) Cleanup() {}
|
||||
func (b *ProxyBridge) Cleanup() {}
|
||||
|
||||
// NewReverseBridge returns an error on non-Linux platforms.
|
||||
func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
|
||||
@@ -46,12 +47,12 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
|
||||
func (b *ReverseBridge) Cleanup() {}
|
||||
|
||||
// WrapCommandLinux returns an error on non-Linux platforms.
|
||||
func WrapCommandLinux(cfg *config.Config, command string, bridge *LinuxBridge, reverseBridge *ReverseBridge, debug bool) (string, error) {
|
||||
func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) {
|
||||
return "", fmt.Errorf("Linux sandbox not available on this platform")
|
||||
}
|
||||
|
||||
// WrapCommandLinuxWithOptions returns an error on non-Linux platforms.
|
||||
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *LinuxBridge, reverseBridge *ReverseBridge, opts LinuxSandboxOptions) (string, error) {
|
||||
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) {
|
||||
return "", fmt.Errorf("Linux sandbox not available on this platform")
|
||||
}
|
||||
|
||||
|
||||
@@ -6,155 +6,34 @@ import (
|
||||
"github.com/Use-Tusk/fence/internal/config"
|
||||
)
|
||||
|
||||
// TestLinux_WildcardAllowedDomainsSkipsUnshareNet verifies that when allowedDomains
|
||||
// contains "*", the Linux sandbox does NOT use --unshare-net, allowing direct
|
||||
// network connections for applications that don't respect HTTP_PROXY.
|
||||
func TestLinux_WildcardAllowedDomainsSkipsUnshareNet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allowedDomains []string
|
||||
wantUnshareNet bool
|
||||
}{
|
||||
{
|
||||
name: "no domains - uses unshare-net",
|
||||
allowedDomains: []string{},
|
||||
wantUnshareNet: true,
|
||||
},
|
||||
{
|
||||
name: "specific domain - uses unshare-net",
|
||||
allowedDomains: []string{"api.openai.com"},
|
||||
wantUnshareNet: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard domain - skips unshare-net",
|
||||
allowedDomains: []string{"*"},
|
||||
wantUnshareNet: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard with specific domains - skips unshare-net",
|
||||
allowedDomains: []string{"api.openai.com", "*"},
|
||||
wantUnshareNet: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain pattern - uses unshare-net",
|
||||
allowedDomains: []string{"*.openai.com"},
|
||||
wantUnshareNet: true,
|
||||
// TestLinux_NoProxyBlocksNetwork verifies that when no ProxyURL is set,
|
||||
// the Linux sandbox uses --unshare-net to block all network access.
|
||||
func TestLinux_NoProxyBlocksNetwork(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Network: config.NetworkConfig{},
|
||||
Filesystem: config.FilesystemConfig{
|
||||
AllowWrite: []string{"/tmp/test"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: tt.allowedDomains,
|
||||
},
|
||||
Filesystem: config.FilesystemConfig{
|
||||
AllowWrite: []string{"/tmp/test"},
|
||||
},
|
||||
}
|
||||
|
||||
// Check the wildcard detection logic directly
|
||||
hasWildcard := hasWildcardAllowedDomain(cfg)
|
||||
|
||||
if tt.wantUnshareNet && hasWildcard {
|
||||
t.Errorf("expected hasWildcard=false for domains %v, got true", tt.allowedDomains)
|
||||
}
|
||||
if !tt.wantUnshareNet && !hasWildcard {
|
||||
t.Errorf("expected hasWildcard=true for domains %v, got false", tt.allowedDomains)
|
||||
}
|
||||
})
|
||||
// With no proxy, network should be blocked
|
||||
if cfg.Network.ProxyURL != "" {
|
||||
t.Error("expected empty ProxyURL for no-network config")
|
||||
}
|
||||
}
|
||||
|
||||
// hasWildcardAllowedDomain checks if the config contains a "*" in allowedDomains.
|
||||
// This replicates the logic used in both linux.go and macos.go.
|
||||
func hasWildcardAllowedDomain(cfg *config.Config) bool {
|
||||
if cfg == nil {
|
||||
return false
|
||||
}
|
||||
for _, d := range cfg.Network.AllowedDomains {
|
||||
if d == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestWildcardDetectionLogic tests the wildcard detection helper.
|
||||
// This logic is shared between macOS and Linux sandbox implementations.
|
||||
func TestWildcardDetectionLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *config.Config
|
||||
expectWildcard bool
|
||||
}{
|
||||
{
|
||||
name: "nil config",
|
||||
cfg: nil,
|
||||
expectWildcard: false,
|
||||
// TestLinux_ProxyURLSet verifies that a proxy URL is properly set in config.
|
||||
func TestLinux_ProxyURLSet(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
ProxyURL: "socks5://localhost:1080",
|
||||
},
|
||||
{
|
||||
name: "empty allowed domains",
|
||||
cfg: &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{},
|
||||
},
|
||||
},
|
||||
expectWildcard: false,
|
||||
},
|
||||
{
|
||||
name: "specific domains only",
|
||||
cfg: &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{"example.com", "api.openai.com"},
|
||||
},
|
||||
},
|
||||
expectWildcard: false,
|
||||
},
|
||||
{
|
||||
name: "exact star wildcard",
|
||||
cfg: &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{"*"},
|
||||
},
|
||||
},
|
||||
expectWildcard: true,
|
||||
},
|
||||
{
|
||||
name: "star wildcard among others",
|
||||
cfg: &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{"example.com", "*", "api.openai.com"},
|
||||
},
|
||||
},
|
||||
expectWildcard: true,
|
||||
},
|
||||
{
|
||||
name: "prefix wildcard is not star",
|
||||
cfg: &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{"*.example.com"},
|
||||
},
|
||||
},
|
||||
expectWildcard: false,
|
||||
},
|
||||
{
|
||||
name: "star in domain name is not wildcard",
|
||||
cfg: &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: []string{"test*domain.com"},
|
||||
},
|
||||
},
|
||||
expectWildcard: false,
|
||||
Filesystem: config.FilesystemConfig{
|
||||
AllowWrite: []string{"/tmp/test"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasWildcardAllowedDomain(tt.cfg)
|
||||
if got != tt.expectWildcard {
|
||||
t.Errorf("hasWildcardAllowedDomain() = %v, want %v", got, tt.expectWildcard)
|
||||
}
|
||||
})
|
||||
if cfg.Network.ProxyURL != "socks5://localhost:1080" {
|
||||
t.Errorf("expected ProxyURL socks5://localhost:1080, got %s", cfg.Network.ProxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Use-Tusk/fence/internal/config"
|
||||
@@ -29,8 +29,9 @@ func generateSessionSuffix() string {
|
||||
type MacOSSandboxParams struct {
|
||||
Command string
|
||||
NeedsNetworkRestriction bool
|
||||
HTTPProxyPort int
|
||||
SOCKSProxyPort int
|
||||
ProxyURL string // External proxy URL (for env vars)
|
||||
ProxyHost string // Proxy host (for sandbox profile network rules)
|
||||
ProxyPort string // Proxy port (for sandbox profile network rules)
|
||||
AllowUnixSockets []string
|
||||
AllowAllUnixSockets bool
|
||||
AllowLocalBinding bool
|
||||
@@ -519,18 +520,10 @@ func GenerateSandboxProfile(params MacOSSandboxParams) string {
|
||||
}
|
||||
}
|
||||
|
||||
if params.HTTPProxyPort > 0 {
|
||||
profile.WriteString(fmt.Sprintf(`(allow network-bind (local ip "localhost:%d"))
|
||||
(allow network-inbound (local ip "localhost:%d"))
|
||||
(allow network-outbound (remote ip "localhost:%d"))
|
||||
`, params.HTTPProxyPort, params.HTTPProxyPort, params.HTTPProxyPort))
|
||||
}
|
||||
|
||||
if params.SOCKSProxyPort > 0 {
|
||||
profile.WriteString(fmt.Sprintf(`(allow network-bind (local ip "localhost:%d"))
|
||||
(allow network-inbound (local ip "localhost:%d"))
|
||||
(allow network-outbound (remote ip "localhost:%d"))
|
||||
`, params.SOCKSProxyPort, params.SOCKSProxyPort, params.SOCKSProxyPort))
|
||||
// Allow outbound to the external proxy host:port
|
||||
if params.ProxyHost != "" && params.ProxyPort != "" {
|
||||
profile.WriteString(fmt.Sprintf(`(allow network-outbound (remote ip "%s:%s"))
|
||||
`, params.ProxyHost, params.ProxyPort))
|
||||
}
|
||||
}
|
||||
profile.WriteString("\n")
|
||||
@@ -568,15 +561,7 @@ func GenerateSandboxProfile(params MacOSSandboxParams) string {
|
||||
}
|
||||
|
||||
// WrapCommandMacOS wraps a command with macOS sandbox restrictions.
|
||||
func WrapCommandMacOS(cfg *config.Config, command string, httpPort, socksPort int, exposedPorts []int, debug bool) (string, error) {
|
||||
// Check if allowedDomains contains "*" (wildcard = allow all direct network)
|
||||
// In this mode, we still run the proxy for apps that respect HTTP_PROXY,
|
||||
// but allow direct connections for apps that don't (like cursor-agent, opencode).
|
||||
// deniedDomains will only be enforced for apps that use the proxy.
|
||||
hasWildcardAllow := slices.Contains(cfg.Network.AllowedDomains, "*")
|
||||
|
||||
needsNetwork := len(cfg.Network.AllowedDomains) > 0 || len(cfg.Network.DeniedDomains) > 0
|
||||
|
||||
func WrapCommandMacOS(cfg *config.Config, command string, exposedPorts []int, debug bool) (string, error) {
|
||||
// Build allow paths: default + configured
|
||||
allowPaths := append(GetDefaultWritePaths(), cfg.Filesystem.AllowWrite...)
|
||||
|
||||
@@ -591,20 +576,25 @@ func WrapCommandMacOS(cfg *config.Config, command string, httpPort, socksPort in
|
||||
allowLocalOutbound = *cfg.Network.AllowLocalOutbound
|
||||
}
|
||||
|
||||
// If wildcard allow, don't restrict network at sandbox level (allow direct connections).
|
||||
// Otherwise, restrict to localhost/proxy only (strict mode).
|
||||
needsNetworkRestriction := !hasWildcardAllow && (needsNetwork || len(cfg.Network.AllowedDomains) == 0)
|
||||
|
||||
if debug && hasWildcardAllow {
|
||||
fmt.Fprintf(os.Stderr, "[fence:macos] Wildcard allowedDomains detected - allowing direct network connections\n")
|
||||
fmt.Fprintf(os.Stderr, "[fence:macos] Note: deniedDomains only enforced for apps that respect HTTP_PROXY\n")
|
||||
// Parse proxy URL for network rules
|
||||
var proxyHost, proxyPort string
|
||||
if cfg.Network.ProxyURL != "" {
|
||||
if u, err := url.Parse(cfg.Network.ProxyURL); err == nil {
|
||||
proxyHost = u.Hostname()
|
||||
proxyPort = u.Port()
|
||||
}
|
||||
}
|
||||
|
||||
// Restrict network unless proxy is configured to an external host
|
||||
// If no proxy: block all outbound. If proxy: allow outbound only to proxy.
|
||||
needsNetworkRestriction := true
|
||||
|
||||
params := MacOSSandboxParams{
|
||||
Command: command,
|
||||
NeedsNetworkRestriction: needsNetworkRestriction,
|
||||
HTTPProxyPort: httpPort,
|
||||
SOCKSProxyPort: socksPort,
|
||||
ProxyURL: cfg.Network.ProxyURL,
|
||||
ProxyHost: proxyHost,
|
||||
ProxyPort: proxyPort,
|
||||
AllowUnixSockets: cfg.Network.AllowUnixSockets,
|
||||
AllowAllUnixSockets: cfg.Network.AllowAllUnixSockets,
|
||||
AllowLocalBinding: allowLocalBinding,
|
||||
@@ -637,7 +627,7 @@ func WrapCommandMacOS(cfg *config.Config, command string, httpPort, socksPort in
|
||||
return "", fmt.Errorf("shell %q not found: %w", shell, err)
|
||||
}
|
||||
|
||||
proxyEnvs := GenerateProxyEnvVars(httpPort, socksPort)
|
||||
proxyEnvs := GenerateProxyEnvVars(cfg.Network.ProxyURL)
|
||||
|
||||
// Build the command
|
||||
// env VAR1=val1 VAR2=val2 sandbox-exec -p 'profile' shell -c 'command'
|
||||
|
||||
@@ -7,44 +7,34 @@ import (
|
||||
"github.com/Use-Tusk/fence/internal/config"
|
||||
)
|
||||
|
||||
// TestMacOS_WildcardAllowedDomainsRelaxesNetwork verifies that when allowedDomains
|
||||
// contains "*", the macOS sandbox profile allows direct network connections.
|
||||
func TestMacOS_WildcardAllowedDomainsRelaxesNetwork(t *testing.T) {
|
||||
// TestMacOS_NetworkRestrictionWithProxy verifies that when a proxy URL is set,
|
||||
// the macOS sandbox profile allows outbound to the proxy host:port.
|
||||
func TestMacOS_NetworkRestrictionWithProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allowedDomains []string
|
||||
wantNetworkRestricted bool
|
||||
wantAllowNetworkOutbound bool
|
||||
name string
|
||||
proxyURL string
|
||||
wantProxy bool
|
||||
proxyHost string
|
||||
proxyPort string
|
||||
}{
|
||||
{
|
||||
name: "no domains - network restricted",
|
||||
allowedDomains: []string{},
|
||||
wantNetworkRestricted: true,
|
||||
wantAllowNetworkOutbound: false,
|
||||
name: "no proxy - network blocked",
|
||||
proxyURL: "",
|
||||
wantProxy: false,
|
||||
},
|
||||
{
|
||||
name: "specific domain - network restricted",
|
||||
allowedDomains: []string{"api.openai.com"},
|
||||
wantNetworkRestricted: true,
|
||||
wantAllowNetworkOutbound: false,
|
||||
name: "socks5 proxy - outbound allowed to proxy",
|
||||
proxyURL: "socks5://proxy.example.com:1080",
|
||||
wantProxy: true,
|
||||
proxyHost: "proxy.example.com",
|
||||
proxyPort: "1080",
|
||||
},
|
||||
{
|
||||
name: "wildcard domain - network unrestricted",
|
||||
allowedDomains: []string{"*"},
|
||||
wantNetworkRestricted: false,
|
||||
wantAllowNetworkOutbound: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard with specific domains - network unrestricted",
|
||||
allowedDomains: []string{"api.openai.com", "*"},
|
||||
wantNetworkRestricted: false,
|
||||
wantAllowNetworkOutbound: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain pattern - network restricted",
|
||||
allowedDomains: []string{"*.openai.com"},
|
||||
wantNetworkRestricted: true,
|
||||
wantAllowNetworkOutbound: false,
|
||||
name: "socks5h proxy - outbound allowed to proxy",
|
||||
proxyURL: "socks5h://localhost:1080",
|
||||
wantProxy: true,
|
||||
proxyHost: "localhost",
|
||||
proxyPort: "1080",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,34 +42,33 @@ func TestMacOS_WildcardAllowedDomainsRelaxesNetwork(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Network: config.NetworkConfig{
|
||||
AllowedDomains: tt.allowedDomains,
|
||||
ProxyURL: tt.proxyURL,
|
||||
},
|
||||
Filesystem: config.FilesystemConfig{
|
||||
AllowWrite: []string{"/tmp/test"},
|
||||
},
|
||||
}
|
||||
|
||||
// Generate the sandbox profile parameters
|
||||
params := buildMacOSParamsForTest(cfg)
|
||||
|
||||
if params.NeedsNetworkRestriction != tt.wantNetworkRestricted {
|
||||
t.Errorf("NeedsNetworkRestriction = %v, want %v",
|
||||
params.NeedsNetworkRestriction, tt.wantNetworkRestricted)
|
||||
if tt.wantProxy {
|
||||
if params.ProxyHost != tt.proxyHost {
|
||||
t.Errorf("expected ProxyHost %q, got %q", tt.proxyHost, params.ProxyHost)
|
||||
}
|
||||
if params.ProxyPort != tt.proxyPort {
|
||||
t.Errorf("expected ProxyPort %q, got %q", tt.proxyPort, params.ProxyPort)
|
||||
}
|
||||
|
||||
profile := GenerateSandboxProfile(params)
|
||||
expectedRule := `(allow network-outbound (remote ip "` + tt.proxyHost + ":" + tt.proxyPort + `"))`
|
||||
if !strings.Contains(profile, expectedRule) {
|
||||
t.Errorf("profile should contain proxy outbound rule %q", expectedRule)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the actual profile and check its contents
|
||||
profile := GenerateSandboxProfile(params)
|
||||
|
||||
// When network is unrestricted, profile should allow network* (all network ops)
|
||||
if tt.wantAllowNetworkOutbound {
|
||||
if !strings.Contains(profile, "(allow network*)") {
|
||||
t.Errorf("expected unrestricted network profile to contain '(allow network*)', got:\n%s", profile)
|
||||
}
|
||||
} else {
|
||||
// When network is restricted, profile should NOT have blanket allow
|
||||
if strings.Contains(profile, "(allow network*)") {
|
||||
t.Errorf("expected restricted network profile to NOT contain blanket '(allow network*)'")
|
||||
}
|
||||
// Network should always be restricted (proxy or not)
|
||||
if !params.NeedsNetworkRestriction {
|
||||
t.Error("NeedsNetworkRestriction should always be true")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -88,15 +77,6 @@ func TestMacOS_WildcardAllowedDomainsRelaxesNetwork(t *testing.T) {
|
||||
// buildMacOSParamsForTest is a helper to build MacOSSandboxParams from config,
|
||||
// replicating the logic in WrapCommandMacOS for testing.
|
||||
func buildMacOSParamsForTest(cfg *config.Config) MacOSSandboxParams {
|
||||
hasWildcardAllow := false
|
||||
for _, d := range cfg.Network.AllowedDomains {
|
||||
if d == "*" {
|
||||
hasWildcardAllow = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
needsNetwork := len(cfg.Network.AllowedDomains) > 0 || len(cfg.Network.DeniedDomains) > 0
|
||||
allowPaths := append(GetDefaultWritePaths(), cfg.Filesystem.AllowWrite...)
|
||||
allowLocalBinding := cfg.Network.AllowLocalBinding
|
||||
allowLocalOutbound := allowLocalBinding
|
||||
@@ -104,13 +84,26 @@ func buildMacOSParamsForTest(cfg *config.Config) MacOSSandboxParams {
|
||||
allowLocalOutbound = *cfg.Network.AllowLocalOutbound
|
||||
}
|
||||
|
||||
needsNetworkRestriction := !hasWildcardAllow && (needsNetwork || len(cfg.Network.AllowedDomains) == 0)
|
||||
var proxyHost, proxyPort string
|
||||
if cfg.Network.ProxyURL != "" {
|
||||
// Simple parsing for tests
|
||||
parts := strings.SplitN(cfg.Network.ProxyURL, "://", 2)
|
||||
if len(parts) == 2 {
|
||||
hostPort := parts[1]
|
||||
colonIdx := strings.LastIndex(hostPort, ":")
|
||||
if colonIdx >= 0 {
|
||||
proxyHost = hostPort[:colonIdx]
|
||||
proxyPort = hostPort[colonIdx+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MacOSSandboxParams{
|
||||
Command: "echo test",
|
||||
NeedsNetworkRestriction: needsNetworkRestriction,
|
||||
HTTPProxyPort: 8080,
|
||||
SOCKSProxyPort: 1080,
|
||||
NeedsNetworkRestriction: true,
|
||||
ProxyURL: cfg.Network.ProxyURL,
|
||||
ProxyHost: proxyHost,
|
||||
ProxyPort: proxyPort,
|
||||
AllowUnixSockets: cfg.Network.AllowUnixSockets,
|
||||
AllowAllUnixSockets: cfg.Network.AllowAllUnixSockets,
|
||||
AllowLocalBinding: allowLocalBinding,
|
||||
@@ -158,8 +151,6 @@ func TestMacOS_ProfileNetworkSection(t *testing.T) {
|
||||
params := MacOSSandboxParams{
|
||||
Command: "echo test",
|
||||
NeedsNetworkRestriction: tt.restricted,
|
||||
HTTPProxyPort: 8080,
|
||||
SOCKSProxyPort: 1080,
|
||||
}
|
||||
|
||||
profile := GenerateSandboxProfile(params)
|
||||
@@ -195,8 +186,8 @@ func TestMacOS_DefaultDenyRead(t *testing.T) {
|
||||
defaultDenyRead: false,
|
||||
allowRead: nil,
|
||||
wantContainsBlanketAllow: true,
|
||||
wantContainsMetadataAllow: false, // No separate metadata allow needed
|
||||
wantContainsSystemAllows: false, // No need for explicit system allows
|
||||
wantContainsMetadataAllow: false,
|
||||
wantContainsSystemAllows: false,
|
||||
wantContainsUserAllowRead: false,
|
||||
},
|
||||
{
|
||||
@@ -204,8 +195,8 @@ func TestMacOS_DefaultDenyRead(t *testing.T) {
|
||||
defaultDenyRead: true,
|
||||
allowRead: nil,
|
||||
wantContainsBlanketAllow: false,
|
||||
wantContainsMetadataAllow: true, // Should have file-read-metadata for traversal
|
||||
wantContainsSystemAllows: true, // Should have explicit system path allows
|
||||
wantContainsMetadataAllow: true,
|
||||
wantContainsSystemAllows: true,
|
||||
wantContainsUserAllowRead: false,
|
||||
},
|
||||
{
|
||||
@@ -223,35 +214,28 @@ func TestMacOS_DefaultDenyRead(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
params := MacOSSandboxParams{
|
||||
Command: "echo test",
|
||||
HTTPProxyPort: 8080,
|
||||
SOCKSProxyPort: 1080,
|
||||
DefaultDenyRead: tt.defaultDenyRead,
|
||||
ReadAllowPaths: tt.allowRead,
|
||||
}
|
||||
|
||||
profile := GenerateSandboxProfile(params)
|
||||
|
||||
// Check for blanket "(allow file-read*)" without path restrictions
|
||||
// This appears at the start of read rules section in default mode
|
||||
hasBlanketAllow := strings.Contains(profile, "(allow file-read*)\n")
|
||||
if hasBlanketAllow != tt.wantContainsBlanketAllow {
|
||||
t.Errorf("blanket file-read allow = %v, want %v", hasBlanketAllow, tt.wantContainsBlanketAllow)
|
||||
}
|
||||
|
||||
// Check for file-read-metadata allow (for directory traversal in defaultDenyRead mode)
|
||||
hasMetadataAllow := strings.Contains(profile, "(allow file-read-metadata)")
|
||||
if hasMetadataAllow != tt.wantContainsMetadataAllow {
|
||||
t.Errorf("file-read-metadata allow = %v, want %v", hasMetadataAllow, tt.wantContainsMetadataAllow)
|
||||
}
|
||||
|
||||
// Check for system path allows (e.g., /usr, /bin) - should use file-read-data in strict mode
|
||||
hasSystemAllows := strings.Contains(profile, `(subpath "/usr")`) ||
|
||||
strings.Contains(profile, `(subpath "/bin")`)
|
||||
if hasSystemAllows != tt.wantContainsSystemAllows {
|
||||
t.Errorf("system path allows = %v, want %v\nProfile:\n%s", hasSystemAllows, tt.wantContainsSystemAllows, profile)
|
||||
}
|
||||
|
||||
// Check for user-specified allowRead paths
|
||||
if tt.wantContainsUserAllowRead && len(tt.allowRead) > 0 {
|
||||
hasUserAllow := strings.Contains(profile, tt.allowRead[0])
|
||||
if !hasUserAllow {
|
||||
|
||||
@@ -6,18 +6,14 @@ import (
|
||||
|
||||
"github.com/Use-Tusk/fence/internal/config"
|
||||
"github.com/Use-Tusk/fence/internal/platform"
|
||||
"github.com/Use-Tusk/fence/internal/proxy"
|
||||
)
|
||||
|
||||
// Manager handles sandbox initialization and command wrapping.
|
||||
type Manager struct {
|
||||
config *config.Config
|
||||
httpProxy *proxy.HTTPProxy
|
||||
socksProxy *proxy.SOCKSProxy
|
||||
linuxBridge *LinuxBridge
|
||||
proxyBridge *ProxyBridge
|
||||
reverseBridge *ReverseBridge
|
||||
httpPort int
|
||||
socksPort int
|
||||
tun2socksPath string // path to extracted tun2socks binary on host
|
||||
exposedPorts []int
|
||||
debug bool
|
||||
monitor bool
|
||||
@@ -38,7 +34,7 @@ func (m *Manager) SetExposedPorts(ports []int) {
|
||||
m.exposedPorts = ports
|
||||
}
|
||||
|
||||
// Initialize sets up the sandbox infrastructure (proxies, etc.).
|
||||
// Initialize sets up the sandbox infrastructure.
|
||||
func (m *Manager) Initialize() error {
|
||||
if m.initialized {
|
||||
return nil
|
||||
@@ -48,32 +44,27 @@ func (m *Manager) Initialize() error {
|
||||
return fmt.Errorf("sandbox is not supported on platform: %s", platform.Detect())
|
||||
}
|
||||
|
||||
filter := proxy.CreateDomainFilter(m.config, 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.monitor)
|
||||
socksPort, err := m.socksProxy.Start()
|
||||
if err != nil {
|
||||
_ = m.httpProxy.Stop()
|
||||
return fmt.Errorf("failed to start SOCKS proxy: %w", err)
|
||||
}
|
||||
m.socksPort = socksPort
|
||||
|
||||
// On Linux, set up the socat bridges
|
||||
// On Linux, set up proxy bridge and tun2socks if proxy is configured
|
||||
if platform.Detect() == platform.Linux {
|
||||
bridge, err := NewLinuxBridge(m.httpPort, m.socksPort, m.debug)
|
||||
if err != nil {
|
||||
_ = m.httpProxy.Stop()
|
||||
_ = m.socksProxy.Stop()
|
||||
return fmt.Errorf("failed to initialize Linux bridge: %w", err)
|
||||
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
|
||||
}
|
||||
m.linuxBridge = bridge
|
||||
|
||||
// Set up reverse bridge for exposed ports (inbound connections)
|
||||
// Only needed when network namespace is available - otherwise they share the network
|
||||
@@ -81,9 +72,12 @@ func (m *Manager) Initialize() error {
|
||||
if len(m.exposedPorts) > 0 && features.CanUnshareNet {
|
||||
reverseBridge, err := NewReverseBridge(m.exposedPorts, m.debug)
|
||||
if err != nil {
|
||||
m.linuxBridge.Cleanup()
|
||||
_ = m.httpProxy.Stop()
|
||||
_ = m.socksProxy.Stop()
|
||||
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
|
||||
@@ -93,7 +87,11 @@ func (m *Manager) Initialize() error {
|
||||
}
|
||||
|
||||
m.initialized = true
|
||||
m.logDebug("Sandbox manager initialized (HTTP proxy: %d, SOCKS proxy: %d)", m.httpPort, m.socksPort)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -114,9 +112,9 @@ func (m *Manager) WrapCommand(command string) (string, error) {
|
||||
plat := platform.Detect()
|
||||
switch plat {
|
||||
case platform.MacOS:
|
||||
return WrapCommandMacOS(m.config, command, m.httpPort, m.socksPort, m.exposedPorts, m.debug)
|
||||
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.debug)
|
||||
case platform.Linux:
|
||||
return WrapCommandLinux(m.config, command, m.linuxBridge, m.reverseBridge, m.debug)
|
||||
return WrapCommandLinux(m.config, command, m.proxyBridge, m.reverseBridge, m.tun2socksPath, m.debug)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported platform: %s", plat)
|
||||
}
|
||||
@@ -127,14 +125,11 @@ func (m *Manager) Cleanup() {
|
||||
if m.reverseBridge != nil {
|
||||
m.reverseBridge.Cleanup()
|
||||
}
|
||||
if m.linuxBridge != nil {
|
||||
m.linuxBridge.Cleanup()
|
||||
if m.proxyBridge != nil {
|
||||
m.proxyBridge.Cleanup()
|
||||
}
|
||||
if m.httpProxy != nil {
|
||||
_ = m.httpProxy.Stop()
|
||||
}
|
||||
if m.socksProxy != nil {
|
||||
_ = m.socksProxy.Stop()
|
||||
if m.tun2socksPath != "" {
|
||||
os.Remove(m.tun2socksPath)
|
||||
}
|
||||
m.logDebug("Sandbox manager cleaned up")
|
||||
}
|
||||
@@ -144,13 +139,3 @@ func (m *Manager) logDebug(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, "[fence] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPPort returns the HTTP proxy port.
|
||||
func (m *Manager) HTTPPort() int {
|
||||
return m.httpPort
|
||||
}
|
||||
|
||||
// SOCKSPort returns the SOCKS proxy port.
|
||||
func (m *Manager) SOCKSPort() int {
|
||||
return m.socksPort
|
||||
}
|
||||
|
||||
53
internal/sandbox/tun2socks_embed.go
Normal file
53
internal/sandbox/tun2socks_embed.go
Normal file
@@ -0,0 +1,53 @@
|
||||
//go:build linux
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
//go:embed bin/tun2socks-linux-*
|
||||
var tun2socksFS embed.FS
|
||||
|
||||
// extractTun2Socks writes the embedded tun2socks binary to a temp file and returns its path.
|
||||
// The caller is responsible for removing the file when done.
|
||||
func extractTun2Socks() (string, error) {
|
||||
var arch string
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
arch = "amd64"
|
||||
case "arm64":
|
||||
arch = "arm64"
|
||||
default:
|
||||
return "", fmt.Errorf("tun2socks: unsupported architecture %s", runtime.GOARCH)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("bin/tun2socks-linux-%s", arch)
|
||||
data, err := fs.ReadFile(tun2socksFS, name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tun2socks: embedded binary not found for %s: %w", arch, err)
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "fence-tun2socks-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tun2socks: failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tmpFile.Write(data); err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFile.Name())
|
||||
return "", fmt.Errorf("tun2socks: failed to write binary: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
if err := os.Chmod(tmpFile.Name(), 0o755); err != nil {
|
||||
os.Remove(tmpFile.Name())
|
||||
return "", fmt.Errorf("tun2socks: failed to make executable: %w", err)
|
||||
}
|
||||
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
10
internal/sandbox/tun2socks_embed_stub.go
Normal file
10
internal/sandbox/tun2socks_embed_stub.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build !linux
|
||||
|
||||
package sandbox
|
||||
|
||||
import "fmt"
|
||||
|
||||
// extractTun2Socks is not available on non-Linux platforms.
|
||||
func extractTun2Socks() (string, error) {
|
||||
return "", fmt.Errorf("tun2socks is only available on Linux")
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -49,13 +48,14 @@ func NormalizePath(pathPattern string) string {
|
||||
}
|
||||
|
||||
// GenerateProxyEnvVars creates environment variables for proxy configuration.
|
||||
func GenerateProxyEnvVars(httpPort, socksPort int) []string {
|
||||
// Used on macOS where transparent proxying is not available.
|
||||
func GenerateProxyEnvVars(proxyURL string) []string {
|
||||
envVars := []string{
|
||||
"FENCE_SANDBOX=1",
|
||||
"TMPDIR=/tmp/fence",
|
||||
}
|
||||
|
||||
if httpPort == 0 && socksPort == 0 {
|
||||
if proxyURL == "" {
|
||||
return envVars
|
||||
}
|
||||
|
||||
@@ -75,32 +75,14 @@ func GenerateProxyEnvVars(httpPort, socksPort int) []string {
|
||||
envVars = append(envVars,
|
||||
"NO_PROXY="+noProxy,
|
||||
"no_proxy="+noProxy,
|
||||
"ALL_PROXY="+proxyURL,
|
||||
"all_proxy="+proxyURL,
|
||||
"HTTP_PROXY="+proxyURL,
|
||||
"HTTPS_PROXY="+proxyURL,
|
||||
"http_proxy="+proxyURL,
|
||||
"https_proxy="+proxyURL,
|
||||
)
|
||||
|
||||
if httpPort > 0 {
|
||||
proxyURL := "http://localhost:" + itoa(httpPort)
|
||||
envVars = append(envVars,
|
||||
"HTTP_PROXY="+proxyURL,
|
||||
"HTTPS_PROXY="+proxyURL,
|
||||
"http_proxy="+proxyURL,
|
||||
"https_proxy="+proxyURL,
|
||||
)
|
||||
}
|
||||
|
||||
if socksPort > 0 {
|
||||
socksURL := "socks5h://localhost:" + itoa(socksPort)
|
||||
envVars = append(envVars,
|
||||
"ALL_PROXY="+socksURL,
|
||||
"all_proxy="+socksURL,
|
||||
"FTP_PROXY="+socksURL,
|
||||
"ftp_proxy="+socksURL,
|
||||
)
|
||||
// Git SSH through SOCKS
|
||||
envVars = append(envVars,
|
||||
"GIT_SSH_COMMAND=ssh -o ProxyCommand='nc -X 5 -x localhost:"+itoa(socksPort)+" %h %p'",
|
||||
)
|
||||
}
|
||||
|
||||
return envVars
|
||||
}
|
||||
|
||||
@@ -121,6 +103,3 @@ func DecodeSandboxedCommand(encoded string) (string, error) {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
return strconv.Itoa(n)
|
||||
}
|
||||
|
||||
@@ -125,16 +125,14 @@ func TestNormalizePath(t *testing.T) {
|
||||
|
||||
func TestGenerateProxyEnvVars(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpPort int
|
||||
socksPort int
|
||||
wantEnvs []string
|
||||
dontWant []string
|
||||
name string
|
||||
proxyURL string
|
||||
wantEnvs []string
|
||||
dontWant []string
|
||||
}{
|
||||
{
|
||||
name: "no ports",
|
||||
httpPort: 0,
|
||||
socksPort: 0,
|
||||
name: "no proxy",
|
||||
proxyURL: "",
|
||||
wantEnvs: []string{
|
||||
"FENCE_SANDBOX=1",
|
||||
"TMPDIR=/tmp/fence",
|
||||
@@ -146,56 +144,34 @@ func TestGenerateProxyEnvVars(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http port only",
|
||||
httpPort: 8080,
|
||||
socksPort: 0,
|
||||
name: "socks5 proxy",
|
||||
proxyURL: "socks5://localhost:1080",
|
||||
wantEnvs: []string{
|
||||
"FENCE_SANDBOX=1",
|
||||
"HTTP_PROXY=http://localhost:8080",
|
||||
"HTTPS_PROXY=http://localhost:8080",
|
||||
"http_proxy=http://localhost:8080",
|
||||
"https_proxy=http://localhost:8080",
|
||||
"ALL_PROXY=socks5://localhost:1080",
|
||||
"all_proxy=socks5://localhost:1080",
|
||||
"HTTP_PROXY=socks5://localhost:1080",
|
||||
"HTTPS_PROXY=socks5://localhost:1080",
|
||||
"http_proxy=socks5://localhost:1080",
|
||||
"https_proxy=socks5://localhost:1080",
|
||||
"NO_PROXY=",
|
||||
"no_proxy=",
|
||||
},
|
||||
dontWant: []string{
|
||||
"ALL_PROXY=",
|
||||
"all_proxy=",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "socks port only",
|
||||
httpPort: 0,
|
||||
socksPort: 1080,
|
||||
name: "socks5h proxy",
|
||||
proxyURL: "socks5h://proxy.example.com:1080",
|
||||
wantEnvs: []string{
|
||||
"FENCE_SANDBOX=1",
|
||||
"ALL_PROXY=socks5h://localhost:1080",
|
||||
"all_proxy=socks5h://localhost:1080",
|
||||
"FTP_PROXY=socks5h://localhost:1080",
|
||||
"GIT_SSH_COMMAND=",
|
||||
},
|
||||
dontWant: []string{
|
||||
"HTTP_PROXY=",
|
||||
"HTTPS_PROXY=",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both ports",
|
||||
httpPort: 8080,
|
||||
socksPort: 1080,
|
||||
wantEnvs: []string{
|
||||
"FENCE_SANDBOX=1",
|
||||
"HTTP_PROXY=http://localhost:8080",
|
||||
"HTTPS_PROXY=http://localhost:8080",
|
||||
"ALL_PROXY=socks5h://localhost:1080",
|
||||
"GIT_SSH_COMMAND=",
|
||||
"ALL_PROXY=socks5h://proxy.example.com:1080",
|
||||
"HTTP_PROXY=socks5h://proxy.example.com:1080",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GenerateProxyEnvVars(tt.httpPort, tt.socksPort)
|
||||
got := GenerateProxyEnvVars(tt.proxyURL)
|
||||
|
||||
// Check expected env vars are present
|
||||
for _, want := range tt.wantEnvs {
|
||||
@@ -207,7 +183,7 @@ func TestGenerateProxyEnvVars(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("GenerateProxyEnvVars(%d, %d) missing %q", tt.httpPort, tt.socksPort, want)
|
||||
t.Errorf("GenerateProxyEnvVars(%q) missing %q", tt.proxyURL, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +191,7 @@ func TestGenerateProxyEnvVars(t *testing.T) {
|
||||
for _, dontWant := range tt.dontWant {
|
||||
for _, env := range got {
|
||||
if strings.HasPrefix(env, dontWant) {
|
||||
t.Errorf("GenerateProxyEnvVars(%d, %d) should not contain %q, got %q", tt.httpPort, tt.socksPort, dontWant, env)
|
||||
t.Errorf("GenerateProxyEnvVars(%q) should not contain %q, got %q", tt.proxyURL, dontWant, env)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user