fix: add SOCKS5 auth, DNS bridge, and TUN capability support
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.
This commit is contained in:
@@ -28,6 +28,7 @@ var (
|
|||||||
monitor bool
|
monitor bool
|
||||||
settingsPath string
|
settingsPath string
|
||||||
proxyURL string
|
proxyURL string
|
||||||
|
dnsAddr string
|
||||||
cmdString string
|
cmdString string
|
||||||
exposePorts []string
|
exposePorts []string
|
||||||
exitCode int
|
exitCode int
|
||||||
@@ -92,6 +93,7 @@ Configuration file format:
|
|||||||
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations")
|
rootCmd.Flags().BoolVarP(&monitor, "monitor", "m", false, "Monitor and log sandbox violations")
|
||||||
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: OS config directory)")
|
rootCmd.Flags().StringVarP(&settingsPath, "settings", "s", "", "Path to settings file (default: OS config directory)")
|
||||||
rootCmd.Flags().StringVar(&proxyURL, "proxy", "", "External SOCKS5 proxy URL (e.g., socks5://localhost:1080)")
|
rootCmd.Flags().StringVar(&proxyURL, "proxy", "", "External SOCKS5 proxy URL (e.g., socks5://localhost:1080)")
|
||||||
|
rootCmd.Flags().StringVar(&dnsAddr, "dns", "", "DNS server address on host (e.g., localhost:3153)")
|
||||||
rootCmd.Flags().StringVarP(&cmdString, "c", "c", "", "Run command string directly (like sh -c)")
|
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)")
|
rootCmd.Flags().StringArrayVarP(&exposePorts, "port", "p", nil, "Expose port for inbound connections (can be used multiple times)")
|
||||||
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
|
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
|
||||||
@@ -173,10 +175,13 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI --proxy flag overrides config
|
// CLI flags override config
|
||||||
if proxyURL != "" {
|
if proxyURL != "" {
|
||||||
cfg.Network.ProxyURL = proxyURL
|
cfg.Network.ProxyURL = proxyURL
|
||||||
}
|
}
|
||||||
|
if dnsAddr != "" {
|
||||||
|
cfg.Network.DnsAddr = dnsAddr
|
||||||
|
}
|
||||||
|
|
||||||
manager := sandbox.NewManager(cfg, debug, monitor)
|
manager := sandbox.NewManager(cfg, debug, monitor)
|
||||||
manager.SetExposedPorts(ports)
|
manager.SetExposedPorts(ports)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Config struct {
|
|||||||
// NetworkConfig defines network restrictions.
|
// NetworkConfig defines network restrictions.
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
ProxyURL string `json:"proxyUrl,omitempty"` // External SOCKS5 proxy (e.g. socks5://host:1080)
|
ProxyURL string `json:"proxyUrl,omitempty"` // External SOCKS5 proxy (e.g. socks5://host:1080)
|
||||||
|
DnsAddr string `json:"dnsAddr,omitempty"` // DNS server address on host (e.g. localhost:3153)
|
||||||
AllowUnixSockets []string `json:"allowUnixSockets,omitempty"`
|
AllowUnixSockets []string `json:"allowUnixSockets,omitempty"`
|
||||||
AllowAllUnixSockets bool `json:"allowAllUnixSockets,omitempty"`
|
AllowAllUnixSockets bool `json:"allowAllUnixSockets,omitempty"`
|
||||||
AllowLocalBinding bool `json:"allowLocalBinding,omitempty"`
|
AllowLocalBinding bool `json:"allowLocalBinding,omitempty"`
|
||||||
@@ -196,6 +197,11 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("invalid network.proxyUrl %q: %w", c.Network.ProxyURL, err)
|
return fmt.Errorf("invalid network.proxyUrl %q: %w", c.Network.ProxyURL, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.Network.DnsAddr != "" {
|
||||||
|
if err := validateHostPort(c.Network.DnsAddr); err != nil {
|
||||||
|
return fmt.Errorf("invalid network.dnsAddr %q: %w", c.Network.DnsAddr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if slices.Contains(c.Filesystem.AllowRead, "") {
|
if slices.Contains(c.Filesystem.AllowRead, "") {
|
||||||
return errors.New("filesystem.allowRead contains empty path")
|
return errors.New("filesystem.allowRead contains empty path")
|
||||||
@@ -261,6 +267,16 @@ func validateProxyURL(proxyURL string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateHostPort validates a host:port address.
|
||||||
|
func validateHostPort(addr string) error {
|
||||||
|
// Must contain a colon separating host and port
|
||||||
|
host, port, found := strings.Cut(addr, ":")
|
||||||
|
if !found || host == "" || port == "" {
|
||||||
|
return errors.New("must be in host:port format (e.g. localhost:3153)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateHostPattern validates an SSH host pattern.
|
// validateHostPattern validates an SSH host pattern.
|
||||||
// Host patterns are more permissive than domain patterns:
|
// Host patterns are more permissive than domain patterns:
|
||||||
// - Can contain wildcards anywhere (e.g., prod-*.example.com, *.example.com)
|
// - Can contain wildcards anywhere (e.g., prod-*.example.com, *.example.com)
|
||||||
@@ -385,8 +401,9 @@ func Merge(base, override *Config) *Config {
|
|||||||
AllowPty: base.AllowPty || override.AllowPty,
|
AllowPty: base.AllowPty || override.AllowPty,
|
||||||
|
|
||||||
Network: NetworkConfig{
|
Network: NetworkConfig{
|
||||||
// ProxyURL: override wins if non-empty
|
// ProxyURL/DnsAddr: override wins if non-empty
|
||||||
ProxyURL: mergeString(base.Network.ProxyURL, override.Network.ProxyURL),
|
ProxyURL: mergeString(base.Network.ProxyURL, override.Network.ProxyURL),
|
||||||
|
DnsAddr: mergeString(base.Network.DnsAddr, override.Network.DnsAddr),
|
||||||
|
|
||||||
// Append slices (base first, then override additions)
|
// Append slices (base first, then override additions)
|
||||||
AllowUnixSockets: mergeStrings(base.Network.AllowUnixSockets, override.Network.AllowUnixSockets),
|
AllowUnixSockets: mergeStrings(base.Network.AllowUnixSockets, override.Network.AllowUnixSockets),
|
||||||
|
|||||||
@@ -23,10 +23,85 @@ type ProxyBridge struct {
|
|||||||
SocketPath string // Unix socket path
|
SocketPath string // Unix socket path
|
||||||
ProxyHost string // Parsed from ProxyURL
|
ProxyHost string // Parsed from ProxyURL
|
||||||
ProxyPort string // Parsed from ProxyURL
|
ProxyPort string // Parsed from ProxyURL
|
||||||
|
ProxyUser string // Username from ProxyURL (if any)
|
||||||
|
ProxyPass string // Password from ProxyURL (if any)
|
||||||
|
HasAuth bool // Whether credentials were provided
|
||||||
process *exec.Cmd
|
process *exec.Cmd
|
||||||
debug bool
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DnsBridge bridges DNS queries from the sandbox to a host-side DNS server via Unix socket.
|
||||||
|
// Inside the sandbox, a socat relay converts UDP DNS queries (port 53) to the Unix socket.
|
||||||
|
// On the host, socat forwards from the Unix socket to the actual DNS server (TCP).
|
||||||
|
type DnsBridge struct {
|
||||||
|
SocketPath string // Unix socket path
|
||||||
|
DnsAddr string // Host-side DNS address (host:port)
|
||||||
|
process *exec.Cmd
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDnsBridge creates a Unix socket bridge to a host-side DNS server.
|
||||||
|
func NewDnsBridge(dnsAddr string, debug bool) (*DnsBridge, error) {
|
||||||
|
if _, err := exec.LookPath("socat"); err != nil {
|
||||||
|
return nil, fmt.Errorf("socat is required for DNS bridge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(id); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate socket ID: %w", err)
|
||||||
|
}
|
||||||
|
socketID := hex.EncodeToString(id)
|
||||||
|
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
socketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-dns-%s.sock", socketID))
|
||||||
|
|
||||||
|
bridge := &DnsBridge{
|
||||||
|
SocketPath: socketPath,
|
||||||
|
DnsAddr: dnsAddr,
|
||||||
|
debug: debug,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start bridge: Unix socket -> DNS server TCP
|
||||||
|
socatArgs := []string{
|
||||||
|
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socketPath),
|
||||||
|
fmt.Sprintf("TCP:%s", dnsAddr),
|
||||||
|
}
|
||||||
|
bridge.process = exec.Command("socat", socatArgs...) //nolint:gosec // args constructed from trusted input
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:linux] Starting DNS bridge: socat %s\n", strings.Join(socatArgs, " "))
|
||||||
|
}
|
||||||
|
if err := bridge.process.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to start DNS bridge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for socket to be created
|
||||||
|
for range 50 {
|
||||||
|
if fileExists(socketPath) {
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:linux] DNS bridge ready (%s -> %s)\n", socketPath, dnsAddr)
|
||||||
|
}
|
||||||
|
return bridge, nil
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.Cleanup()
|
||||||
|
return nil, fmt.Errorf("timeout waiting for DNS bridge socket to be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup stops the DNS bridge and removes the socket file.
|
||||||
|
func (b *DnsBridge) Cleanup() {
|
||||||
|
if b.process != nil && b.process.Process != nil {
|
||||||
|
_ = b.process.Process.Kill()
|
||||||
|
_ = b.process.Wait()
|
||||||
|
}
|
||||||
|
_ = os.Remove(b.SocketPath)
|
||||||
|
|
||||||
|
if b.debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:linux] DNS bridge cleaned up\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ReverseBridge holds the socat bridge processes for inbound connections.
|
// ReverseBridge holds the socat bridge processes for inbound connections.
|
||||||
type ReverseBridge struct {
|
type ReverseBridge struct {
|
||||||
Ports []int
|
Ports []int
|
||||||
@@ -77,6 +152,13 @@ func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
|
|||||||
debug: debug,
|
debug: debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture credentials from the proxy URL (if any)
|
||||||
|
if u.User != nil {
|
||||||
|
bridge.HasAuth = true
|
||||||
|
bridge.ProxyUser = u.User.Username()
|
||||||
|
bridge.ProxyPass, _ = u.User.Password()
|
||||||
|
}
|
||||||
|
|
||||||
// Start bridge: Unix socket -> external SOCKS5 proxy TCP
|
// Start bridge: Unix socket -> external SOCKS5 proxy TCP
|
||||||
socatArgs := []string{
|
socatArgs := []string{
|
||||||
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socketPath),
|
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socketPath),
|
||||||
@@ -305,8 +387,8 @@ func getMandatoryDenyPaths(cwd string) []string {
|
|||||||
|
|
||||||
// WrapCommandLinux wraps a command with Linux bubblewrap sandbox.
|
// WrapCommandLinux wraps a command with Linux bubblewrap sandbox.
|
||||||
// It uses available security features (Landlock, seccomp) with graceful fallback.
|
// It uses available security features (Landlock, seccomp) with graceful fallback.
|
||||||
func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) {
|
func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, dnsBridge *DnsBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) {
|
||||||
return WrapCommandLinuxWithOptions(cfg, command, proxyBridge, reverseBridge, tun2socksPath, LinuxSandboxOptions{
|
return WrapCommandLinuxWithOptions(cfg, command, proxyBridge, dnsBridge, reverseBridge, tun2socksPath, LinuxSandboxOptions{
|
||||||
UseLandlock: true, // Enabled by default, will fall back if not available
|
UseLandlock: true, // Enabled by default, will fall back if not available
|
||||||
UseSeccomp: true, // Enabled by default
|
UseSeccomp: true, // Enabled by default
|
||||||
UseEBPF: true, // Enabled by default if available
|
UseEBPF: true, // Enabled by default if available
|
||||||
@@ -315,7 +397,7 @@ func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBrid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WrapCommandLinuxWithOptions wraps a command with configurable sandbox options.
|
// WrapCommandLinuxWithOptions wraps a command with configurable sandbox options.
|
||||||
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) {
|
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, dnsBridge *DnsBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) {
|
||||||
if _, err := exec.LookPath("bwrap"); err != nil {
|
if _, err := exec.LookPath("bwrap"); err != nil {
|
||||||
return "", fmt.Errorf("bubblewrap (bwrap) is required on Linux but not found: %w", err)
|
return "", fmt.Errorf("bubblewrap (bwrap) is required on Linux but not found: %w", err)
|
||||||
}
|
}
|
||||||
@@ -586,18 +668,50 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bind the proxy bridge Unix socket into the sandbox (needs to be writable)
|
// Bind the proxy bridge Unix socket into the sandbox (needs to be writable)
|
||||||
|
var dnsRelayResolvConf string // temp file path for custom resolv.conf
|
||||||
if proxyBridge != nil {
|
if proxyBridge != nil {
|
||||||
bwrapArgs = append(bwrapArgs,
|
bwrapArgs = append(bwrapArgs,
|
||||||
"--bind", proxyBridge.SocketPath, proxyBridge.SocketPath,
|
"--bind", proxyBridge.SocketPath, proxyBridge.SocketPath,
|
||||||
)
|
)
|
||||||
|
if tun2socksPath != "" && features.CanUseTransparentProxy() {
|
||||||
// Bind /dev/net/tun for TUN device creation inside the sandbox
|
// Bind /dev/net/tun for TUN device creation inside the sandbox
|
||||||
if features.HasDevNetTun {
|
if features.HasDevNetTun {
|
||||||
bwrapArgs = append(bwrapArgs, "--dev-bind", "/dev/net/tun", "/dev/net/tun")
|
bwrapArgs = append(bwrapArgs, "--dev-bind", "/dev/net/tun", "/dev/net/tun")
|
||||||
}
|
}
|
||||||
|
// Preserve CAP_NET_ADMIN (TUN device + network config) and
|
||||||
|
// CAP_NET_BIND_SERVICE (DNS relay on port 53) inside the namespace
|
||||||
|
bwrapArgs = append(bwrapArgs, "--cap-add", "CAP_NET_ADMIN")
|
||||||
|
bwrapArgs = append(bwrapArgs, "--cap-add", "CAP_NET_BIND_SERVICE")
|
||||||
// Bind the tun2socks binary into the sandbox (read-only)
|
// Bind the tun2socks binary into the sandbox (read-only)
|
||||||
if tun2socksPath != "" {
|
|
||||||
bwrapArgs = append(bwrapArgs, "--ro-bind", tun2socksPath, "/tmp/fence-tun2socks")
|
bwrapArgs = append(bwrapArgs, "--ro-bind", tun2socksPath, "/tmp/fence-tun2socks")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind DNS bridge socket if available
|
||||||
|
if dnsBridge != nil {
|
||||||
|
bwrapArgs = append(bwrapArgs,
|
||||||
|
"--bind", dnsBridge.SocketPath, dnsBridge.SocketPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override /etc/resolv.conf to point DNS at our local relay (port 53).
|
||||||
|
// Inside the sandbox, a socat relay on UDP :53 converts queries to the
|
||||||
|
// DNS bridge (Unix socket -> host DNS server) or to TCP through the tunnel.
|
||||||
|
if dnsBridge != nil || (tun2socksPath != "" && features.CanUseTransparentProxy()) {
|
||||||
|
tmpResolv, err := os.CreateTemp("", "fence-resolv-*.conf")
|
||||||
|
if err == nil {
|
||||||
|
_, _ = tmpResolv.WriteString("nameserver 127.0.0.1\n")
|
||||||
|
tmpResolv.Close()
|
||||||
|
dnsRelayResolvConf = tmpResolv.Name()
|
||||||
|
bwrapArgs = append(bwrapArgs, "--ro-bind", dnsRelayResolvConf, "/etc/resolv.conf")
|
||||||
|
if opts.Debug {
|
||||||
|
if dnsBridge != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:linux] DNS: overriding resolv.conf -> 127.0.0.1 (bridge to %s)\n", dnsBridge.DnsAddr)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence:linux] DNS: overriding resolv.conf -> 127.0.0.1 (TCP relay through tunnel)\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind reverse socket directory if needed (sockets created inside sandbox)
|
// Bind reverse socket directory if needed (sockets created inside sandbox)
|
||||||
@@ -632,8 +746,21 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
|
|||||||
innerScript.WriteString("export FENCE_SANDBOX=1\n")
|
innerScript.WriteString("export FENCE_SANDBOX=1\n")
|
||||||
|
|
||||||
if proxyBridge != nil && tun2socksPath != "" && features.CanUseTransparentProxy() {
|
if proxyBridge != nil && tun2socksPath != "" && features.CanUseTransparentProxy() {
|
||||||
|
// Build the tun2socks proxy URL with credentials if available
|
||||||
|
// Many SOCKS5 proxies require the username/password auth flow even
|
||||||
|
// without real credentials (e.g., gost always selects method 0x02).
|
||||||
|
// Including userinfo ensures tun2socks offers both auth methods.
|
||||||
|
tun2socksProxyURL := "socks5://127.0.0.1:${PROXY_PORT}"
|
||||||
|
if proxyBridge.HasAuth {
|
||||||
|
userinfo := url.UserPassword(proxyBridge.ProxyUser, proxyBridge.ProxyPass)
|
||||||
|
tun2socksProxyURL = fmt.Sprintf("socks5://%s@127.0.0.1:${PROXY_PORT}", userinfo.String())
|
||||||
|
}
|
||||||
|
|
||||||
// Set up transparent proxy via TUN device + tun2socks
|
// Set up transparent proxy via TUN device + tun2socks
|
||||||
innerScript.WriteString(fmt.Sprintf(`
|
innerScript.WriteString(fmt.Sprintf(`
|
||||||
|
# Bring up loopback interface (needed for socat to bind on 127.0.0.1)
|
||||||
|
ip link set lo up
|
||||||
|
|
||||||
# Set up TUN device for transparent proxying
|
# Set up TUN device for transparent proxying
|
||||||
ip tuntap add dev tun0 mode tun
|
ip tuntap add dev tun0 mode tun
|
||||||
ip addr add 198.18.0.1/15 dev tun0
|
ip addr add 198.18.0.1/15 dev tun0
|
||||||
@@ -646,13 +773,33 @@ socat TCP-LISTEN:${PROXY_PORT},fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:%s >/d
|
|||||||
BRIDGE_PID=$!
|
BRIDGE_PID=$!
|
||||||
|
|
||||||
# Start tun2socks (transparent proxy via gvisor netstack)
|
# Start tun2socks (transparent proxy via gvisor netstack)
|
||||||
/tmp/fence-tun2socks -device tun0 -proxy socks5://127.0.0.1:${PROXY_PORT} >/dev/null 2>&1 &
|
/tmp/fence-tun2socks -device tun0 -proxy %s >/dev/null 2>&1 &
|
||||||
TUN2SOCKS_PID=$!
|
TUN2SOCKS_PID=$!
|
||||||
|
|
||||||
`, proxyBridge.SocketPath))
|
`, proxyBridge.SocketPath, tun2socksProxyURL))
|
||||||
|
|
||||||
|
// DNS relay: convert UDP DNS queries on port 53 so apps can resolve names.
|
||||||
|
if dnsBridge != nil {
|
||||||
|
// Dedicated DNS bridge: UDP :53 -> Unix socket -> host DNS server
|
||||||
|
innerScript.WriteString(fmt.Sprintf(`# DNS relay: UDP queries -> Unix socket -> host DNS server (%s)
|
||||||
|
socat UDP4-RECVFROM:53,fork,reuseaddr UNIX-CONNECT:%s >/dev/null 2>&1 &
|
||||||
|
DNS_RELAY_PID=$!
|
||||||
|
|
||||||
|
`, dnsBridge.DnsAddr, dnsBridge.SocketPath))
|
||||||
|
} else {
|
||||||
|
// Fallback: UDP :53 -> TCP to public DNS through the tunnel
|
||||||
|
innerScript.WriteString(`# DNS relay: UDP queries -> TCP 1.1.1.1:53 (through tun2socks tunnel)
|
||||||
|
socat UDP4-RECVFROM:53,fork,reuseaddr TCP:1.1.1.1:53 >/dev/null 2>&1 &
|
||||||
|
DNS_RELAY_PID=$!
|
||||||
|
|
||||||
|
`)
|
||||||
|
}
|
||||||
} else if proxyBridge != nil {
|
} else if proxyBridge != nil {
|
||||||
// Fallback: no TUN support, use env-var-based proxying
|
// Fallback: no TUN support, use env-var-based proxying
|
||||||
innerScript.WriteString(fmt.Sprintf(`
|
innerScript.WriteString(fmt.Sprintf(`
|
||||||
|
# Bring up loopback interface (needed for socat to bind on 127.0.0.1)
|
||||||
|
ip link set lo up 2>/dev/null
|
||||||
|
|
||||||
# Set up SOCKS5 bridge (no TUN available, env-var-based proxying)
|
# Set up SOCKS5 bridge (no TUN available, env-var-based proxying)
|
||||||
PROXY_PORT=18321
|
PROXY_PORT=18321
|
||||||
socat TCP-LISTEN:${PROXY_PORT},fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:%s >/dev/null 2>&1 &
|
socat TCP-LISTEN:${PROXY_PORT},fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:%s >/dev/null 2>&1 &
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ type ProxyBridge struct {
|
|||||||
ProxyPort string
|
ProxyPort string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DnsBridge is a stub for non-Linux platforms.
|
||||||
|
type DnsBridge struct {
|
||||||
|
SocketPath string
|
||||||
|
DnsAddr string
|
||||||
|
}
|
||||||
|
|
||||||
// ReverseBridge is a stub for non-Linux platforms.
|
// ReverseBridge is a stub for non-Linux platforms.
|
||||||
type ReverseBridge struct {
|
type ReverseBridge struct {
|
||||||
Ports []int
|
Ports []int
|
||||||
@@ -38,6 +44,14 @@ func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
|
|||||||
// Cleanup is a no-op on non-Linux platforms.
|
// Cleanup is a no-op on non-Linux platforms.
|
||||||
func (b *ProxyBridge) Cleanup() {}
|
func (b *ProxyBridge) Cleanup() {}
|
||||||
|
|
||||||
|
// NewDnsBridge returns an error on non-Linux platforms.
|
||||||
|
func NewDnsBridge(dnsAddr string, debug bool) (*DnsBridge, error) {
|
||||||
|
return nil, fmt.Errorf("DNS bridge not available on this platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup is a no-op on non-Linux platforms.
|
||||||
|
func (b *DnsBridge) Cleanup() {}
|
||||||
|
|
||||||
// NewReverseBridge returns an error on non-Linux platforms.
|
// NewReverseBridge returns an error on non-Linux platforms.
|
||||||
func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
|
func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
|
||||||
return nil, fmt.Errorf("reverse bridge not available on this platform")
|
return nil, fmt.Errorf("reverse bridge not available on this platform")
|
||||||
@@ -47,12 +61,12 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
|
|||||||
func (b *ReverseBridge) Cleanup() {}
|
func (b *ReverseBridge) Cleanup() {}
|
||||||
|
|
||||||
// WrapCommandLinux returns an error on non-Linux platforms.
|
// WrapCommandLinux returns an error on non-Linux platforms.
|
||||||
func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) {
|
func WrapCommandLinux(cfg *config.Config, command string, proxyBridge *ProxyBridge, dnsBridge *DnsBridge, reverseBridge *ReverseBridge, tun2socksPath string, debug bool) (string, error) {
|
||||||
return "", fmt.Errorf("Linux sandbox not available on this platform")
|
return "", fmt.Errorf("Linux sandbox not available on this platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapCommandLinuxWithOptions returns an error on non-Linux platforms.
|
// WrapCommandLinuxWithOptions returns an error on non-Linux platforms.
|
||||||
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) {
|
func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge *ProxyBridge, dnsBridge *DnsBridge, reverseBridge *ReverseBridge, tun2socksPath string, opts LinuxSandboxOptions) (string, error) {
|
||||||
return "", fmt.Errorf("Linux sandbox not available on this platform")
|
return "", fmt.Errorf("Linux sandbox not available on this platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
proxyBridge *ProxyBridge
|
proxyBridge *ProxyBridge
|
||||||
|
dnsBridge *DnsBridge
|
||||||
reverseBridge *ReverseBridge
|
reverseBridge *ReverseBridge
|
||||||
tun2socksPath string // path to extracted tun2socks binary on host
|
tun2socksPath string // path to extracted tun2socks binary on host
|
||||||
exposedPorts []int
|
exposedPorts []int
|
||||||
@@ -64,6 +65,19 @@ func (m *Manager) Initialize() error {
|
|||||||
return fmt.Errorf("failed to initialize proxy bridge: %w", err)
|
return fmt.Errorf("failed to initialize proxy bridge: %w", err)
|
||||||
}
|
}
|
||||||
m.proxyBridge = bridge
|
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)
|
// Set up reverse bridge for exposed ports (inbound connections)
|
||||||
@@ -114,7 +128,7 @@ func (m *Manager) WrapCommand(command string) (string, error) {
|
|||||||
case platform.MacOS:
|
case platform.MacOS:
|
||||||
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.debug)
|
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.debug)
|
||||||
case platform.Linux:
|
case platform.Linux:
|
||||||
return WrapCommandLinux(m.config, command, m.proxyBridge, m.reverseBridge, m.tun2socksPath, m.debug)
|
return WrapCommandLinux(m.config, command, m.proxyBridge, m.dnsBridge, m.reverseBridge, m.tun2socksPath, m.debug)
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported platform: %s", plat)
|
return "", fmt.Errorf("unsupported platform: %s", plat)
|
||||||
}
|
}
|
||||||
@@ -125,6 +139,9 @@ func (m *Manager) Cleanup() {
|
|||||||
if m.reverseBridge != nil {
|
if m.reverseBridge != nil {
|
||||||
m.reverseBridge.Cleanup()
|
m.reverseBridge.Cleanup()
|
||||||
}
|
}
|
||||||
|
if m.dnsBridge != nil {
|
||||||
|
m.dnsBridge.Cleanup()
|
||||||
|
}
|
||||||
if m.proxyBridge != nil {
|
if m.proxyBridge != nil {
|
||||||
m.proxyBridge.Cleanup()
|
m.proxyBridge.Cleanup()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user