Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 26 additions & 21 deletions namespace/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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]
}
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -295,4 +300,4 @@ func (l *Linux) removeNamespace() error {
return fmt.Errorf("failed to remove namespace: %v", err)
}
return nil
}
}
40 changes: 21 additions & 19 deletions namespace/macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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]
}
}
}

Expand Down Expand Up @@ -243,43 +246,42 @@ 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()
if err != nil {
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
}

Expand Down
9 changes: 7 additions & 2 deletions tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,20 @@ 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,
configDir: configDir,
}

// 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)
}
Expand Down
Loading