diff --git a/cli/cli.go b/cli/cli.go index 76bc988..a34f8a1 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -142,7 +142,7 @@ func Run(config Config, args []string) error { // Create certificate manager (if TLS interception is enabled) var tlsConfig *cryptotls.Config if !config.NoTLSIntercept { - certManager, err := tls.NewCertificateManager("", logger) // Empty configDir since it will be determined internally + certManager, err := tls.NewCertificateManager(logger) if err != nil { logger.Error("Failed to create certificate manager", "error", err) return fmt.Errorf("failed to create certificate manager: %v", err) diff --git a/namespace/linux.go b/namespace/linux.go index 073d677..92be549 100644 --- a/namespace/linux.go +++ b/namespace/linux.go @@ -18,6 +18,7 @@ import ( type Linux struct { config Config namespace string + vethHost string // Host-side veth interface name for iptables rules logger *slog.Logger preparedEnv map[string]string procAttr *syscall.SysProcAttr @@ -26,9 +27,10 @@ type Linux struct { // newLinux creates a new Linux network jail instance func newLinux(config Config, logger *slog.Logger) (*Linux, error) { return &Linux{ - config: config, - namespace: newNamespaceName(), - logger: logger, + config: config, + namespace: newNamespaceName(), + logger: logger, + preparedEnv: make(map[string]string), }, nil } @@ -63,12 +65,14 @@ func (l *Linux) Open() error { // Prepare environment once during setup l.logger.Debug("Preparing environment") - l.preparedEnv = make(map[string]string) // Start with current environment for _, envVar := range os.Environ() { if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 { - l.preparedEnv[parts[0]] = parts[1] + // Only set if not already set by SetEnv + if _, exists := l.preparedEnv[parts[0]]; !exists { + l.preparedEnv[parts[0]] = parts[1] + } } } @@ -193,6 +197,9 @@ func (l *Linux) setupNetworking() error { vethHost := fmt.Sprintf("veth_h_%s", uniqueID) // veth_h_1234567 = 14 chars vethNetJail := fmt.Sprintf("veth_n_%s", uniqueID) // veth_n_1234567 = 14 chars + // Store veth interface name for iptables rules + l.vethHost = vethHost + setupCmds := []struct { description string command *exec.Cmd @@ -246,42 +253,40 @@ options timeout:2 attempts:2 return nil } -// setupIptables configures iptables rules for traffic redirection +// setupIptables configures iptables rules for comprehensive TCP traffic interception func (l *Linux) setupIptables() error { // Enable IP forwarding cmd := exec.Command("sysctl", "-w", "net.ipv4.ip_forward=1") cmd.Run() // Ignore error - // NAT rules for outgoing traffic + // NAT rules for outgoing traffic (MASQUERADE for return traffic) cmd = exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", "-s", "192.168.100.0/24", "-j", "MASQUERADE") err := cmd.Run() if err != nil { return fmt.Errorf("failed to add NAT rule: %v", err) } - // Redirect HTTP traffic to proxy - cmd = exec.Command("ip", "netns", "exec", l.namespace, "iptables", "-t", "nat", "-A", "OUTPUT", - "-p", "tcp", "--dport", "80", "-j", "DNAT", "--to-destination", fmt.Sprintf("192.168.100.1:%d", l.config.HTTPPort)) - err = cmd.Run() - if err != nil { - return fmt.Errorf("failed to add HTTP redirect rule: %v", err) - } - - // Redirect HTTPS traffic to proxy - cmd = exec.Command("ip", "netns", "exec", l.namespace, "iptables", "-t", "nat", "-A", "OUTPUT", - "-p", "tcp", "--dport", "443", "-j", "DNAT", "--to-destination", fmt.Sprintf("192.168.100.1:%d", l.config.HTTPSPort)) + // COMPREHENSIVE APPROACH: Intercept ALL TCP traffic from namespace + // Use PREROUTING on host to catch traffic after it exits namespace but before routing + // This ensures NO TCP traffic can bypass the proxy + cmd = exec.Command("iptables", "-t", "nat", "-A", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.config.HTTPSPort)) err = cmd.Run() if err != nil { - return fmt.Errorf("failed to add HTTPS redirect rule: %v", err) + return fmt.Errorf("failed to add comprehensive TCP redirect rule: %v", err) } + l.logger.Debug("Comprehensive TCP jailing enabled", "interface", l.vethHost, "proxy_port", l.config.HTTPSPort) return nil } // removeIptables removes iptables rules func (l *Linux) removeIptables() error { + // Remove comprehensive TCP redirect rule + cmd := exec.Command("iptables", "-t", "nat", "-D", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.config.HTTPSPort)) + cmd.Run() // Ignore errors during cleanup + // Remove NAT rule - cmd := exec.Command("iptables", "-t", "nat", "-D", "POSTROUTING", "-s", "192.168.100.0/24", "-j", "MASQUERADE") + cmd = exec.Command("iptables", "-t", "nat", "-D", "POSTROUTING", "-s", "192.168.100.0/24", "-j", "MASQUERADE") cmd.Run() // Ignore errors during cleanup return nil @@ -295,4 +300,4 @@ func (l *Linux) removeNamespace() error { return fmt.Errorf("failed to remove namespace: %v", err) } return nil -} +} \ No newline at end of file diff --git a/namespace/macos.go b/namespace/macos.go index 88e58e0..56b904e 100644 --- a/namespace/macos.go +++ b/namespace/macos.go @@ -40,6 +40,7 @@ func newMacOSJail(config Config, logger *slog.Logger) (*MacOSNetJail, error) { pfRulesPath: pfRulesPath, mainRulesPath: mainRulesPath, logger: logger, + preparedEnv: make(map[string]string), }, nil } @@ -63,12 +64,14 @@ func (m *MacOSNetJail) Open() error { // Prepare environment once during setup m.logger.Debug("Preparing environment") - m.preparedEnv = make(map[string]string) // Start with current environment for _, envVar := range os.Environ() { if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 { - m.preparedEnv[parts[0]] = parts[1] + // Only set if not already set by SetEnv + if _, exists := m.preparedEnv[parts[0]]; !exists { + m.preparedEnv[parts[0]] = parts[1] + } } } @@ -243,7 +246,7 @@ func (m *MacOSNetJail) getDefaultInterface() (string, error) { return "en0", nil } -// createPFRules creates PF rules for traffic diversion +// createPFRules creates PF rules for comprehensive TCP traffic diversion func (m *MacOSNetJail) createPFRules() (string, error) { // Get the default network interface iface, err := m.getDefaultInterface() @@ -251,35 +254,34 @@ func (m *MacOSNetJail) createPFRules() (string, error) { return "", fmt.Errorf("failed to get default interface: %v", err) } - // Create PF rules following httpjail's working pattern - rules := fmt.Sprintf(`# boundary PF rules for GID %d on interface %s -# First, redirect traffic arriving on lo0 to our proxy ports -rdr pass on lo0 inet proto tcp from any to any port 80 -> 127.0.0.1 port %d -rdr pass on lo0 inet proto tcp from any to any port 443 -> 127.0.0.1 port %d + // Create comprehensive PF rules for ALL TCP traffic interception + // This prevents bypass via non-standard ports (8080, 3306, 22, etc.) + rules := fmt.Sprintf(`# comprehensive TCP jailing PF rules for GID %d on interface %s +# COMPREHENSIVE APPROACH: Intercept ALL TCP traffic from the jailed group +# This ensures NO TCP traffic can bypass the proxy by using alternative ports + +# First, redirect ALL TCP traffic arriving on lo0 to our HTTPS proxy port +# The HTTPS proxy can handle both HTTP and HTTPS traffic +rdr pass on lo0 inet proto tcp from any to any -> 127.0.0.1 port %d -# Route boundary group traffic to lo0 where it will be redirected -pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any port 80 group %d keep state -pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any port 443 group %d keep state +# Route ALL TCP traffic from boundary group to lo0 where it will be redirected +pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any group %d keep state -# Also handle traffic on the specific interface -pass out on %s route-to (lo0 127.0.0.1) inet proto tcp from any to any port 80 group %d keep state -pass out on %s route-to (lo0 127.0.0.1) inet proto tcp from any to any port 443 group %d keep state +# Also handle ALL TCP traffic on the specific interface from the group +pass out on %s route-to (lo0 127.0.0.1) inet proto tcp from any to any group %d keep state # Allow all loopback traffic pass on lo0 all `, m.groupID, iface, - m.config.HTTPPort, - m.config.HTTPSPort, - m.groupID, - m.groupID, - iface, + m.config.HTTPSPort, // Use HTTPS proxy port for all TCP traffic m.groupID, iface, m.groupID, ) + m.logger.Debug("Comprehensive TCP jailing enabled for macOS", "group_id", m.groupID, "proxy_port", m.config.HTTPSPort) return rules, nil } diff --git a/tls/tls.go b/tls/tls.go index e5ca824..4222147 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -30,7 +30,12 @@ type CertificateManager struct { } // NewCertificateManager creates a new certificate manager -func NewCertificateManager(configDir string, logger *slog.Logger) (*CertificateManager, error) { +func NewCertificateManager(logger *slog.Logger) (*CertificateManager, error) { + configDir, err := getConfigDir() + if err != nil { + return nil, fmt.Errorf("failed to determine config directory: %v", err) + } + cm := &CertificateManager{ certCache: make(map[string]*tls.Certificate), logger: logger, @@ -38,7 +43,7 @@ func NewCertificateManager(configDir string, logger *slog.Logger) (*CertificateM } // Load or generate CA certificate - err := cm.loadOrGenerateCA() + err = cm.loadOrGenerateCA() if err != nil { return nil, fmt.Errorf("failed to load or generate CA: %v", err) }