From e190a985d5d11b2a8ad2debaf2e1793cae3e81fa Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 10 Sep 2025 21:41:22 -0400 Subject: [PATCH 1/3] moving stuff --- audit/request.go | 4 +--- cli/cli.go | 31 ++++++++-------------------- jail.go | 45 +++++++++++++++++++++-------------------- namespace/linux.go | 31 +++++++++++++++------------- namespace/linux_stub.go | 3 +-- namespace/macos.go | 31 +++++++++++++++------------- namespace/macos_stub.go | 4 +--- namespace/namespace.go | 11 +++++----- proxy/proxy.go | 41 ++++++++++++------------------------- 9 files changed, 86 insertions(+), 115 deletions(-) diff --git a/audit/request.go b/audit/request.go index b0e7bae..6183c0d 100644 --- a/audit/request.go +++ b/audit/request.go @@ -1,8 +1,6 @@ package audit -import ( - "net/http" -) +import "net/http" // Request represents information about an HTTP request for auditing type Request struct { diff --git a/cli/cli.go b/cli/cli.go index 9ca1f4e..dc81805 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -10,9 +10,7 @@ import ( "syscall" "github.com/coder/jail" - "github.com/coder/jail/audit" "github.com/coder/jail/namespace" - "github.com/coder/jail/proxy" "github.com/coder/jail/rules" "github.com/coder/jail/tls" "github.com/coder/serpent" @@ -115,16 +113,12 @@ func Run(config Config, args []string) error { ruleEngine := rules.NewRuleEngine(allowRules, logger) // Create auditor - auditor := audit.NewLoggingAuditor(logger) - - // Create network namespace configuration - nsConfig := namespace.Config{ - HTTPPort: 8040, - HTTPSPort: 8043, - } + // auditor := audit.NewLoggingAuditor(logger) // Create commander - commander, err := namespace.New(nsConfig, logger) + commander, err := namespace.New(namespace.Config{ + Logger: logger, + }) if err != nil { logger.Error("Failed to create network namespace", "error", err) return fmt.Errorf("failed to create network namespace: %v", err) @@ -154,23 +148,14 @@ func Run(config Config, args []string) error { commander.SetEnv("REQUESTS_CA_BUNDLE", caCertPath) // Python requests commander.SetEnv("NODE_EXTRA_CA_CERTS", caCertPath) // Node.js - // Create proxy server - proxyServer := proxy.NewProxyServer(proxy.Config{ - HTTPPort: 8040, - HTTPSPort: 8043, + // Create jail instance + jailInstance := jail.New(jail.Config{ + Commander: commander, RuleEngine: ruleEngine, - Auditor: auditor, Logger: logger, TLSConfig: tlsConfig, }) - // Create jail instance - jailInstance := jail.New(jail.Config{ - Commander: commander, - ProxyServer: proxyServer, - Logger: logger, - }) - // Setup signal handling BEFORE any setup sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) @@ -198,7 +183,7 @@ func Run(config Config, args []string) error { }() // Open jail (starts network namespace and proxy server) - err = jailInstance.Open() + err = jailInstance.Start() if err != nil { logger.Error("Failed to open jail", "error", err) return fmt.Errorf("failed to open jail: %v", err) diff --git a/jail.go b/jail.go index f0e5a84..2f4a771 100644 --- a/jail.go +++ b/jail.go @@ -2,36 +2,36 @@ package jail import ( "context" + "crypto/tls" "fmt" "log/slog" "os/exec" "time" + + "github.com/coder/jail/proxy" + "github.com/coder/jail/rules" ) type Commander interface { - Open() error + Start(httpProxyPort int, httpsProxyPort int) error SetEnv(key string, value string) Command(command []string) *exec.Cmd Close() error } -type ProxyServer interface { - Start(ctx context.Context) error - Stop() error -} - type Config struct { - Commander Commander - ProxyServer ProxyServer - Logger *slog.Logger + Commander Commander + RuleEngine *rules.RuleEngine + Logger *slog.Logger + TLSConfig *tls.Config } type Jail struct { commandExecutor Commander - proxyServer ProxyServer + proxyServer *proxy.ProxyServer logger *slog.Logger - cancel context.CancelFunc ctx context.Context + cancel context.CancelFunc } func New(config Config) *Jail { @@ -39,16 +39,22 @@ func New(config Config) *Jail { return &Jail{ commandExecutor: config.Commander, - proxyServer: config.ProxyServer, - logger: config.Logger, - ctx: ctx, - cancel: cancel, + proxyServer: proxy.NewProxyServer(proxy.Config{ + HTTPPort: 8080, + HTTPSPort: 8443, + RuleEngine: config.RuleEngine, + Logger: config.Logger, + TLSConfig: config.TLSConfig, + }), + logger: config.Logger, + ctx: ctx, + cancel: cancel, } } -func (j *Jail) Open() error { +func (j *Jail) Start() error { // Open the command executor (network namespace) - err := j.commandExecutor.Open() + err := j.commandExecutor.Start(8080, 8443) if err != nil { return fmt.Errorf("failed to open command executor: %v", err) } @@ -72,11 +78,6 @@ func (j *Jail) Command(command []string) *exec.Cmd { } func (j *Jail) Close() error { - // Cancel context to stop proxy server - if j.cancel != nil { - j.cancel() - } - // Stop proxy server if j.proxyServer != nil { err := j.proxyServer.Stop() diff --git a/namespace/linux.go b/namespace/linux.go index 92be549..dcbf768 100644 --- a/namespace/linux.go +++ b/namespace/linux.go @@ -16,28 +16,31 @@ import ( // Linux implements jail.Commander using Linux network namespaces 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 + namespace string + vethHost string // Host-side veth interface name for iptables rules + logger *slog.Logger + preparedEnv map[string]string + procAttr *syscall.SysProcAttr + httpProxyPort int + httpsProxyPort int } // newLinux creates a new Linux network jail instance -func newLinux(config Config, logger *slog.Logger) (*Linux, error) { +func newLinux(config Config) (*Linux, error) { return &Linux{ - config: config, namespace: newNamespaceName(), - logger: logger, + logger: config.Logger, preparedEnv: make(map[string]string), }, nil } // Setup creates network namespace and configures iptables rules -func (l *Linux) Open() error { +func (l *Linux) Start(httpProxyPort int, httpsProxyPort int) error { l.logger.Debug("Setup called") + l.httpProxyPort = httpProxyPort + l.httpsProxyPort = httpsProxyPort + // Setup DNS configuration BEFORE creating namespace // This ensures the namespace-specific resolv.conf is available when namespace is created err := l.setupDNS() @@ -269,20 +272,20 @@ func (l *Linux) setupIptables() error { // 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)) + cmd = exec.Command("iptables", "-t", "nat", "-A", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpsProxyPort)) err = cmd.Run() if err != nil { 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) + l.logger.Debug("Comprehensive TCP jailing enabled", "interface", l.vethHost, "proxy_port", l.httpsProxyPort) 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 := exec.Command("iptables", "-t", "nat", "-D", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpsProxyPort)) cmd.Run() // Ignore errors during cleanup // Remove NAT rule @@ -300,4 +303,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/linux_stub.go b/namespace/linux_stub.go index 63d0d7d..d7103fb 100644 --- a/namespace/linux_stub.go +++ b/namespace/linux_stub.go @@ -4,12 +4,11 @@ package namespace import ( "fmt" - "log/slog" "github.com/coder/jail" ) // newLinux is not available on non-Linux platforms -func newLinux(_ Config, _ *slog.Logger) (jail.Commander, error) { +func newLinux(_ Config) (jail.Commander, error) { return nil, fmt.Errorf("linux network jail not supported on this platform") } diff --git a/namespace/macos.go b/namespace/macos.go index 56b904e..c57fd16 100644 --- a/namespace/macos.go +++ b/namespace/macos.go @@ -20,34 +20,37 @@ const ( // MacOSNetJail implements network jail using macOS PF (Packet Filter) and group-based isolation type MacOSNetJail struct { - config Config - groupID int - pfRulesPath string - mainRulesPath string - logger *slog.Logger - preparedEnv map[string]string - procAttr *syscall.SysProcAttr + groupID int + pfRulesPath string + mainRulesPath string + logger *slog.Logger + preparedEnv map[string]string + procAttr *syscall.SysProcAttr + httpProxyPort int + httpsProxyPort int } // newMacOSJail creates a new macOS network jail instance -func newMacOSJail(config Config, logger *slog.Logger) (*MacOSNetJail, error) { +func newMacOSJail(config Config) (*MacOSNetJail, error) { ns := newNamespaceName() pfRulesPath := fmt.Sprintf("/tmp/%s.pf", ns) mainRulesPath := fmt.Sprintf("/tmp/%s_main.pf", ns) return &MacOSNetJail{ - config: config, pfRulesPath: pfRulesPath, mainRulesPath: mainRulesPath, - logger: logger, + logger: config.Logger, preparedEnv: make(map[string]string), }, nil } // Setup creates the network jail group and configures PF rules -func (m *MacOSNetJail) Open() error { +func (m *MacOSNetJail) Start(httpProxyPort int, httpsProxyPort int) error { m.logger.Debug("Setup called") + m.httpProxyPort = httpProxyPort + m.httpsProxyPort = httpsProxyPort + // Create or get network jail group m.logger.Debug("Creating or ensuring network jail group") err := m.ensureGroup() @@ -275,13 +278,13 @@ pass on lo0 all `, m.groupID, iface, - m.config.HTTPSPort, // Use HTTPS proxy port for all TCP traffic + m.httpsProxyPort, // 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) + m.logger.Debug("Comprehensive TCP jailing enabled for macOS", "group_id", m.groupID, "proxy_port", m.httpsProxyPort) return rules, nil } @@ -367,4 +370,4 @@ func (m *MacOSNetJail) cleanupTempFiles() { if m.mainRulesPath != "" { os.Remove(m.mainRulesPath) } -} \ No newline at end of file +} diff --git a/namespace/macos_stub.go b/namespace/macos_stub.go index 9368ae3..f4470c7 100644 --- a/namespace/macos_stub.go +++ b/namespace/macos_stub.go @@ -3,12 +3,10 @@ package namespace import ( - "log/slog" - "github.com/coder/jail" ) // newMacOSJail is not available on non-macOS platforms -func newMacOSJail(config Config, logger *slog.Logger) (jail.Commander, error) { +func newMacOSJail(_ Config) (jail.Commander, error) { panic("macOS network jail not available on this platform") } diff --git a/namespace/namespace.go b/namespace/namespace.go index a71ca22..cadc31c 100644 --- a/namespace/namespace.go +++ b/namespace/namespace.go @@ -15,17 +15,16 @@ const ( // JailConfig holds configuration for network jail type Config struct { - HTTPPort int - HTTPSPort int + Logger *slog.Logger } // NewJail creates a new NetJail instance for the current platform -func New(config Config, logger *slog.Logger) (jail.Commander, error) { +func New(config Config) (jail.Commander, error) { switch runtime.GOOS { case "darwin": - return newMacOSJail(config, logger) + return newMacOSJail(config) case "linux": - return newLinux(config, logger) + return newLinux(config) default: return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) } @@ -33,4 +32,4 @@ func New(config Config, logger *slog.Logger) (jail.Commander, error) { func newNamespaceName() string { return fmt.Sprintf("%s_%d", namespacePrefix, time.Now().UnixNano()%10000000) -} \ No newline at end of file +} diff --git a/proxy/proxy.go b/proxy/proxy.go index cedc223..233c33d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -10,7 +10,6 @@ import ( "net/url" "time" - "github.com/coder/jail/audit" "github.com/coder/jail/rules" ) @@ -19,7 +18,6 @@ type ProxyServer struct { httpServer *http.Server httpsServer *http.Server ruleEngine *rules.RuleEngine - auditor *audit.LoggingAuditor logger *slog.Logger tlsConfig *tls.Config httpPort int @@ -31,7 +29,6 @@ type Config struct { HTTPPort int HTTPSPort int RuleEngine *rules.RuleEngine - Auditor *audit.LoggingAuditor Logger *slog.Logger TLSConfig *tls.Config } @@ -40,7 +37,6 @@ type Config struct { func NewProxyServer(config Config) *ProxyServer { return &ProxyServer{ ruleEngine: config.RuleEngine, - auditor: config.Auditor, logger: config.Logger, tlsConfig: config.TLSConfig, httpPort: config.HTTPPort, @@ -110,11 +106,11 @@ func (p *ProxyServer) handleHTTP(w http.ResponseWriter, r *http.Request) { // Check if request should be allowed result := p.ruleEngine.Evaluate(r.Method, r.URL.String()) - // Audit the request - auditReq := audit.HTTPRequestToAuditRequest(r) - auditReq.Allowed = result.Allowed - auditReq.Rule = result.Rule - p.auditRequest(auditReq) + // // Audit the request + // auditReq := audit.HTTPRequestToAuditRequest(r) + // auditReq.Allowed = result.Allowed + // auditReq.Rule = result.Rule + // p.auditRequest(auditReq) if !result.Allowed { p.writeBlockedResponse(w, r) @@ -127,23 +123,17 @@ func (p *ProxyServer) handleHTTP(w http.ResponseWriter, r *http.Request) { // handleHTTPS handles HTTPS requests (after TLS termination) func (p *ProxyServer) handleHTTPS(w http.ResponseWriter, r *http.Request) { - // Reconstruct the full URL for HTTPS requests - fullURL := fmt.Sprintf("https://%s%s", r.Host, r.URL.Path) - if r.URL.RawQuery != "" { - fullURL += "?" + r.URL.RawQuery - } - // Check if request should be allowed - result := p.ruleEngine.Evaluate(r.Method, fullURL) + result := p.ruleEngine.Evaluate(r.Method, r.URL.String()) // Audit the request - auditReq := &audit.Request{ - Method: r.Method, - URL: fullURL, - Allowed: result.Allowed, - Rule: result.Rule, - } - p.auditRequest(auditReq) + // auditReq := &audit.Request{ + // Method: r.Method, + // URL: fullURL, + // Allowed: result.Allowed, + // Rule: result.Rule, + // } + // p.auditRequest(auditReq) if !result.Allowed { p.writeBlockedResponse(w, r) @@ -291,8 +281,3 @@ For more help: https://github.com/coder/jail `, r.Method, r.URL.Path, host, host, r.Method, host, r.Method) } - -// auditRequest handles auditing of requests -func (p *ProxyServer) auditRequest(req *audit.Request) { - p.auditor.AuditRequest(req) -} From 663b7da9b83d646c1e0caf742fd936dba985f594 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 11 Sep 2025 09:56:09 -0400 Subject: [PATCH 2/3] moving code around --- audit/logging_auditor.go | 2 +- audit/logging_auditor_test.go | 48 +++++++++---------- cli/cli.go | 42 ++++------------- jail.go | 89 ++++++++++++++++++++++++++--------- namespace/linux.go | 17 ++++--- namespace/linux_stub.go | 6 +-- namespace/macos.go | 19 ++++---- namespace/macos_stub.go | 8 +--- namespace/namespace.go | 30 ++++++------ namespace/noop.go | 23 +++++++++ proxy/proxy.go | 41 ++++++++++------ 11 files changed, 187 insertions(+), 138 deletions(-) create mode 100644 namespace/noop.go diff --git a/audit/logging_auditor.go b/audit/logging_auditor.go index 31de3d2..f2f81d8 100644 --- a/audit/logging_auditor.go +++ b/audit/logging_auditor.go @@ -15,7 +15,7 @@ func NewLoggingAuditor(logger *slog.Logger) *LoggingAuditor { } // AuditRequest logs the request using structured logging -func (a *LoggingAuditor) AuditRequest(req *Request) { +func (a *LoggingAuditor) AuditRequest(req Request) { if req.Allowed { a.logger.Info("ALLOW", "method", req.Method, diff --git a/audit/logging_auditor_test.go b/audit/logging_auditor_test.go index 3dac8ca..b72651f 100644 --- a/audit/logging_auditor_test.go +++ b/audit/logging_auditor_test.go @@ -11,13 +11,13 @@ import ( func TestLoggingAuditor(t *testing.T) { tests := []struct { name string - request *Request + request Request expectedLevel string expectedFields []string }{ { name: "allow request", - request: &Request{ + request: Request{ Method: "GET", URL: "https://github.com", Allowed: true, @@ -28,7 +28,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "deny request", - request: &Request{ + request: Request{ Method: "POST", URL: "https://example.com", Allowed: false, @@ -38,7 +38,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "allow with empty rule", - request: &Request{ + request: Request{ Method: "PUT", URL: "https://api.github.com/repos", Allowed: true, @@ -49,7 +49,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "deny HTTPS request", - request: &Request{ + request: Request{ Method: "GET", URL: "https://malware.bad.com/payload", Allowed: false, @@ -59,7 +59,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "allow with wildcard rule", - request: &Request{ + request: Request{ Method: "POST", URL: "https://api.github.com/graphql", Allowed: true, @@ -70,7 +70,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "deny HTTP request", - request: &Request{ + request: Request{ Method: "GET", URL: "http://insecure.example.com", Allowed: false, @@ -80,7 +80,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "allow HEAD request", - request: &Request{ + request: Request{ Method: "HEAD", URL: "https://cdn.jsdelivr.net/health", Allowed: true, @@ -91,7 +91,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "deny OPTIONS request", - request: &Request{ + request: Request{ Method: "OPTIONS", URL: "https://restricted.api.com/cors", Allowed: false, @@ -101,7 +101,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "allow with port number", - request: &Request{ + request: Request{ Method: "GET", URL: "https://localhost:3000/api/health", Allowed: true, @@ -112,7 +112,7 @@ func TestLoggingAuditor(t *testing.T) { }, { name: "deny DELETE request", - request: &Request{ + request: Request{ Method: "DELETE", URL: "https://api.production.com/users/admin", Allowed: false, @@ -153,13 +153,13 @@ func TestLoggingAuditor(t *testing.T) { func TestLoggingAuditor_EdgeCases(t *testing.T) { tests := []struct { name string - request *Request + request Request expectedLevel string expectedFields []string }{ { name: "empty fields", - request: &Request{ + request: Request{ Method: "", URL: "", Allowed: true, @@ -170,7 +170,7 @@ func TestLoggingAuditor_EdgeCases(t *testing.T) { }, { name: "special characters in URL", - request: &Request{ + request: Request{ Method: "POST", URL: "https://api.example.com/users?name=John%20Doe&id=123", Allowed: true, @@ -181,7 +181,7 @@ func TestLoggingAuditor_EdgeCases(t *testing.T) { }, { name: "very long URL", - request: &Request{ + request: Request{ Method: "GET", URL: "https://example.com/" + strings.Repeat("a", 1000), Allowed: false, @@ -191,7 +191,7 @@ func TestLoggingAuditor_EdgeCases(t *testing.T) { }, { name: "deny with custom URL", - request: &Request{ + request: Request{ Method: "DELETE", URL: "https://malicious.com", Allowed: false, @@ -233,13 +233,13 @@ func TestLoggingAuditor_DifferentLogLevels(t *testing.T) { tests := []struct { name string logLevel slog.Level - request *Request + request Request expectOutput bool }{ { name: "info level allows info logs", logLevel: slog.LevelInfo, - request: &Request{ + request: Request{ Method: "GET", URL: "https://github.com", Allowed: true, @@ -250,7 +250,7 @@ func TestLoggingAuditor_DifferentLogLevels(t *testing.T) { { name: "warn level blocks info logs", logLevel: slog.LevelWarn, - request: &Request{ + request: Request{ Method: "GET", URL: "https://github.com", Allowed: true, @@ -261,7 +261,7 @@ func TestLoggingAuditor_DifferentLogLevels(t *testing.T) { { name: "warn level allows warn logs", logLevel: slog.LevelWarn, - request: &Request{ + request: Request{ Method: "POST", URL: "https://example.com", Allowed: false, @@ -271,7 +271,7 @@ func TestLoggingAuditor_DifferentLogLevels(t *testing.T) { { name: "error level blocks warn logs", logLevel: slog.LevelError, - request: &Request{ + request: Request{ Method: "POST", URL: "https://example.com", Allowed: false, @@ -312,7 +312,7 @@ func TestLoggingAuditor_NilLogger(t *testing.T) { }() auditor := &LoggingAuditor{logger: nil} - req := &Request{ + req := Request{ Method: "GET", URL: "https://example.com", Allowed: true, @@ -331,7 +331,7 @@ func TestLoggingAuditor_JSONHandler(t *testing.T) { })) auditor := NewLoggingAuditor(logger) - req := &Request{ + req := Request{ Method: "GET", URL: "https://github.com", Allowed: true, @@ -364,7 +364,7 @@ func TestLoggingAuditor_DiscardHandler(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) auditor := NewLoggingAuditor(logger) - req := &Request{ + req := Request{ Method: "GET", URL: "https://example.com", Allowed: true, diff --git a/cli/cli.go b/cli/cli.go index dc81805..37afae7 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -10,7 +10,7 @@ import ( "syscall" "github.com/coder/jail" - "github.com/coder/jail/namespace" + "github.com/coder/jail/audit" "github.com/coder/jail/rules" "github.com/coder/jail/tls" "github.com/coder/serpent" @@ -115,15 +115,6 @@ func Run(config Config, args []string) error { // Create auditor // auditor := audit.NewLoggingAuditor(logger) - // Create commander - commander, err := namespace.New(namespace.Config{ - Logger: logger, - }) - if err != nil { - logger.Error("Failed to create network namespace", "error", err) - return fmt.Errorf("failed to create network namespace: %v", err) - } - // Create certificate manager certManager, err := tls.NewCertificateManager(logger) if err != nil { @@ -131,30 +122,17 @@ func Run(config Config, args []string) error { return fmt.Errorf("failed to create certificate manager: %v", err) } - // Setup TLS config and write CA certificate to file - var caCertPath, configDir string - tlsConfig, caCertPath, configDir, err := certManager.SetupTLSAndWriteCACert() - if err != nil { - logger.Error("Failed to setup TLS and CA certificate", "error", err) - return fmt.Errorf("failed to setup TLS and CA certificate: %v", err) - } - - // Set standard CA certificate environment variables for common tools - // This makes tools like curl, git, etc. trust our dynamically generated CA - commander.SetEnv("SSL_CERT_FILE", caCertPath) // OpenSSL/LibreSSL-based tools - commander.SetEnv("SSL_CERT_DIR", configDir) // OpenSSL certificate directory - commander.SetEnv("CURL_CA_BUNDLE", caCertPath) // curl - commander.SetEnv("GIT_SSL_CAINFO", caCertPath) // Git - commander.SetEnv("REQUESTS_CA_BUNDLE", caCertPath) // Python requests - commander.SetEnv("NODE_EXTRA_CA_CERTS", caCertPath) // Node.js - // Create jail instance - jailInstance := jail.New(jail.Config{ - Commander: commander, - RuleEngine: ruleEngine, - Logger: logger, - TLSConfig: tlsConfig, + jailInstance, err := jail.New(context.Background(), jail.Config{ + RuleEngine: ruleEngine, + Auditor: audit.NewLoggingAuditor(logger), + Logger: logger, + CertManager: certManager, }) + if err != nil { + logger.Error("Failed to create jail instance", "error", err) + return fmt.Errorf("failed to create jail instance: %v", err) + } // Setup signal handling BEFORE any setup sigChan := make(chan os.Signal, 1) diff --git a/jail.go b/jail.go index 2f4a771..6f27ee4 100644 --- a/jail.go +++ b/jail.go @@ -2,28 +2,28 @@ package jail import ( "context" - "crypto/tls" "fmt" "log/slog" "os/exec" + "runtime" "time" + "github.com/coder/jail/namespace" "github.com/coder/jail/proxy" - "github.com/coder/jail/rules" + "github.com/coder/jail/tls" ) type Commander interface { - Start(httpProxyPort int, httpsProxyPort int) error - SetEnv(key string, value string) + Start() error Command(command []string) *exec.Cmd Close() error } type Config struct { - Commander Commander - RuleEngine *rules.RuleEngine - Logger *slog.Logger - TLSConfig *tls.Config + RuleEngine proxy.RuleEvaluator + Auditor proxy.Auditor + CertManager *tls.CertificateManager + Logger *slog.Logger } type Jail struct { @@ -34,27 +34,58 @@ type Jail struct { cancel context.CancelFunc } -func New(config Config) *Jail { - ctx, cancel := context.WithCancel(context.Background()) +func New(ctx context.Context, config Config) (*Jail, error) { + // Setup TLS config and write CA certificate to file + tlsConfig, caCertPath, configDir, err := config.CertManager.SetupTLSAndWriteCACert() + if err != nil { + return nil, fmt.Errorf("failed to setup TLS and CA certificate: %v", err) + } - return &Jail{ - commandExecutor: config.Commander, - proxyServer: proxy.NewProxyServer(proxy.Config{ - HTTPPort: 8080, - HTTPSPort: 8443, - RuleEngine: config.RuleEngine, - Logger: config.Logger, - TLSConfig: config.TLSConfig, - }), - logger: config.Logger, - ctx: ctx, - cancel: cancel, + // Create proxy server + proxyServer := proxy.NewProxyServer(proxy.Config{ + HTTPPort: 8080, + HTTPSPort: 8443, + Auditor: config.Auditor, + RuleEngine: config.RuleEngine, + Logger: config.Logger, + TLSConfig: tlsConfig, + }) + + // Create commander + commander, err := newCommander(namespace.Config{ + Logger: config.Logger, + HttpProxyPort: 8080, + HttpsProxyPort: 8443, + Env: map[string]string{ + // Set standard CA certificate environment variables for common tools + // This makes tools like curl, git, etc. trust our dynamically generated CA + "SSL_CERT_FILE": caCertPath, // OpenSSL/LibreSSL-based tools + "SSL_CERT_DIR": configDir, // OpenSSL certificate directory + "CURL_CA_BUNDLE": caCertPath, // curl + "GIT_SSL_CAINFO": caCertPath, // Git + "REQUESTS_CA_BUNDLE": caCertPath, // Python requests + "NODE_EXTRA_CA_CERTS": caCertPath, // Node.js + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to create commander: %v", err) } + + // Create cancellable context for jail + ctx, cancel := context.WithCancel(ctx) + + return &Jail{ + commandExecutor: commander, + proxyServer: proxyServer, + logger: config.Logger, + ctx: ctx, + cancel: cancel, + }, nil } func (j *Jail) Start() error { // Open the command executor (network namespace) - err := j.commandExecutor.Start(8080, 8443) + err := j.commandExecutor.Start() if err != nil { return fmt.Errorf("failed to open command executor: %v", err) } @@ -89,3 +120,15 @@ func (j *Jail) Close() error { // Close command executor return j.commandExecutor.Close() } + +// newCommander creates a new NetJail instance for the current platform +func newCommander(config namespace.Config) (Commander, error) { + switch runtime.GOOS { + case "darwin": + return namespace.NewMacOS(config) + case "linux": + return namespace.NewLinux(config) + default: + return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } +} diff --git a/namespace/linux.go b/namespace/linux.go index dcbf768..9764c0d 100644 --- a/namespace/linux.go +++ b/namespace/linux.go @@ -25,22 +25,21 @@ type Linux struct { httpsProxyPort int } -// newLinux creates a new Linux network jail instance -func newLinux(config Config) (*Linux, error) { +// NewLinux creates a new Linux network jail instance +func NewLinux(config Config) (*Linux, error) { return &Linux{ - namespace: newNamespaceName(), - logger: config.Logger, - preparedEnv: make(map[string]string), + namespace: newNamespaceName(), + logger: config.Logger, + preparedEnv: make(map[string]string), + httpProxyPort: config.HttpProxyPort, + httpsProxyPort: config.HttpsProxyPort, }, nil } // Setup creates network namespace and configures iptables rules -func (l *Linux) Start(httpProxyPort int, httpsProxyPort int) error { +func (l *Linux) Start() error { l.logger.Debug("Setup called") - l.httpProxyPort = httpProxyPort - l.httpsProxyPort = httpsProxyPort - // Setup DNS configuration BEFORE creating namespace // This ensures the namespace-specific resolv.conf is available when namespace is created err := l.setupDNS() diff --git a/namespace/linux_stub.go b/namespace/linux_stub.go index d7103fb..29a304b 100644 --- a/namespace/linux_stub.go +++ b/namespace/linux_stub.go @@ -4,11 +4,9 @@ package namespace import ( "fmt" - - "github.com/coder/jail" ) -// newLinux is not available on non-Linux platforms -func newLinux(_ Config) (jail.Commander, error) { +// NewLinux is not available on non-Linux platforms +func NewLinux(_ Config) (*noop, error) { return nil, fmt.Errorf("linux network jail not supported on this platform") } diff --git a/namespace/macos.go b/namespace/macos.go index c57fd16..b51212f 100644 --- a/namespace/macos.go +++ b/namespace/macos.go @@ -30,27 +30,26 @@ type MacOSNetJail struct { httpsProxyPort int } -// newMacOSJail creates a new macOS network jail instance -func newMacOSJail(config Config) (*MacOSNetJail, error) { +// NewMacOS creates a new macOS network jail instance +func NewMacOS(config Config) (*MacOSNetJail, error) { ns := newNamespaceName() pfRulesPath := fmt.Sprintf("/tmp/%s.pf", ns) mainRulesPath := fmt.Sprintf("/tmp/%s_main.pf", ns) return &MacOSNetJail{ - pfRulesPath: pfRulesPath, - mainRulesPath: mainRulesPath, - logger: config.Logger, - preparedEnv: make(map[string]string), + pfRulesPath: pfRulesPath, + mainRulesPath: mainRulesPath, + logger: config.Logger, + preparedEnv: make(map[string]string), + httpProxyPort: config.HttpProxyPort, + httpsProxyPort: config.HttpsProxyPort, }, nil } // Setup creates the network jail group and configures PF rules -func (m *MacOSNetJail) Start(httpProxyPort int, httpsProxyPort int) error { +func (m *MacOSNetJail) Start() error { m.logger.Debug("Setup called") - m.httpProxyPort = httpProxyPort - m.httpsProxyPort = httpsProxyPort - // Create or get network jail group m.logger.Debug("Creating or ensuring network jail group") err := m.ensureGroup() diff --git a/namespace/macos_stub.go b/namespace/macos_stub.go index f4470c7..224a9f8 100644 --- a/namespace/macos_stub.go +++ b/namespace/macos_stub.go @@ -2,11 +2,7 @@ package namespace -import ( - "github.com/coder/jail" -) - -// newMacOSJail is not available on non-macOS platforms -func newMacOSJail(_ Config) (jail.Commander, error) { +// NewMacOS is not available on non-macOS platforms +func NewMacOS(_ Config) (*noop, error) { panic("macOS network jail not available on this platform") } diff --git a/namespace/namespace.go b/namespace/namespace.go index cadc31c..1cf01ef 100644 --- a/namespace/namespace.go +++ b/namespace/namespace.go @@ -3,10 +3,7 @@ package namespace import ( "fmt" "log/slog" - "runtime" "time" - - "github.com/coder/jail" ) const ( @@ -15,20 +12,23 @@ const ( // JailConfig holds configuration for network jail type Config struct { - Logger *slog.Logger + Logger *slog.Logger + HttpProxyPort int + HttpsProxyPort int + Env map[string]string } -// NewJail creates a new NetJail instance for the current platform -func New(config Config) (jail.Commander, error) { - switch runtime.GOOS { - case "darwin": - return newMacOSJail(config) - case "linux": - return newLinux(config) - default: - return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) - } -} +// // NewJail creates a new NetJail instance for the current platform +// func New(config Config) (jail.Commander, error) { +// switch runtime.GOOS { +// case "darwin": +// return NewMacOS(config) +// case "linux": +// return NewLinux(config) +// default: +// return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) +// } +// } func newNamespaceName() string { return fmt.Sprintf("%s_%d", namespacePrefix, time.Now().UnixNano()%10000000) diff --git a/namespace/noop.go b/namespace/noop.go new file mode 100644 index 0000000..aed145c --- /dev/null +++ b/namespace/noop.go @@ -0,0 +1,23 @@ +package namespace + +import ( + "os/exec" +) + +type noop struct{} + +func newNoop(_ Config) (*noop, error) { + return &noop{}, nil +} + +func (n *noop) Command(_ []string) *exec.Cmd { + return exec.Command("true") +} + +func (n *noop) Start() error { + return nil +} + +func (n *noop) Close() error { + return nil +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 233c33d..5fce72c 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -10,14 +10,24 @@ import ( "net/url" "time" + "github.com/coder/jail/audit" "github.com/coder/jail/rules" ) +type RuleEvaluator interface { + Evaluate(method, url string) rules.EvaluationResult +} + +type Auditor interface { + AuditRequest(req audit.Request) +} + // ProxyServer handles HTTP and HTTPS requests with rule-based filtering type ProxyServer struct { httpServer *http.Server httpsServer *http.Server - ruleEngine *rules.RuleEngine + ruleEngine RuleEvaluator + auditor Auditor logger *slog.Logger tlsConfig *tls.Config httpPort int @@ -28,7 +38,8 @@ type ProxyServer struct { type Config struct { HTTPPort int HTTPSPort int - RuleEngine *rules.RuleEngine + RuleEngine RuleEvaluator + Auditor Auditor Logger *slog.Logger TLSConfig *tls.Config } @@ -37,6 +48,7 @@ type Config struct { func NewProxyServer(config Config) *ProxyServer { return &ProxyServer{ ruleEngine: config.RuleEngine, + auditor: config.Auditor, logger: config.Logger, tlsConfig: config.TLSConfig, httpPort: config.HTTPPort, @@ -106,11 +118,13 @@ func (p *ProxyServer) handleHTTP(w http.ResponseWriter, r *http.Request) { // Check if request should be allowed result := p.ruleEngine.Evaluate(r.Method, r.URL.String()) - // // Audit the request - // auditReq := audit.HTTPRequestToAuditRequest(r) - // auditReq.Allowed = result.Allowed - // auditReq.Rule = result.Rule - // p.auditRequest(auditReq) + // Audit the request + p.auditor.AuditRequest(audit.Request{ + Method: r.Method, + URL: r.URL.String(), + Allowed: result.Allowed, + Rule: result.Rule, + }) if !result.Allowed { p.writeBlockedResponse(w, r) @@ -127,13 +141,12 @@ func (p *ProxyServer) handleHTTPS(w http.ResponseWriter, r *http.Request) { result := p.ruleEngine.Evaluate(r.Method, r.URL.String()) // Audit the request - // auditReq := &audit.Request{ - // Method: r.Method, - // URL: fullURL, - // Allowed: result.Allowed, - // Rule: result.Rule, - // } - // p.auditRequest(auditReq) + p.auditor.AuditRequest(audit.Request{ + Method: r.Method, + URL: r.URL.String(), + Allowed: result.Allowed, + Rule: result.Rule, + }) if !result.Allowed { p.writeBlockedResponse(w, r) From 901735ddb00dd7ee674b4f74452a0f94716369e9 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:02:21 +0000 Subject: [PATCH 3/3] Fix environment variables not being passed to commands - Initialize preparedEnv with config.Env in both macOS and Linux constructors - Remove unused SetEnv methods - Update comments to reflect the new approach Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- namespace/linux.go | 15 ++++++++------- namespace/macos.go | 17 +++++++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/namespace/linux.go b/namespace/linux.go index 9764c0d..accc168 100644 --- a/namespace/linux.go +++ b/namespace/linux.go @@ -27,10 +27,16 @@ type Linux struct { // NewLinux creates a new Linux network jail instance func NewLinux(config Config) (*Linux, error) { + // Initialize preparedEnv with config environment variables + preparedEnv := make(map[string]string) + for key, value := range config.Env { + preparedEnv[key] = value + } + return &Linux{ namespace: newNamespaceName(), logger: config.Logger, - preparedEnv: make(map[string]string), + preparedEnv: preparedEnv, httpProxyPort: config.HttpProxyPort, httpsProxyPort: config.HttpsProxyPort, }, nil @@ -71,7 +77,7 @@ func (l *Linux) Start() error { // Start with current environment for _, envVar := range os.Environ() { if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 { - // Only set if not already set by SetEnv + // Only set if not already set by config if _, exists := l.preparedEnv[parts[0]]; !exists { l.preparedEnv[parts[0]] = parts[1] } @@ -121,11 +127,6 @@ func (l *Linux) Start() error { return nil } -// SetEnv sets an environment variable for commands run in the namespace -func (l *Linux) SetEnv(key string, value string) { - l.preparedEnv[key] = value -} - // Command returns an exec.Cmd configured to run within the network namespace func (l *Linux) Command(command []string) *exec.Cmd { l.logger.Debug("Command called", "command", command) diff --git a/namespace/macos.go b/namespace/macos.go index b51212f..a9db968 100644 --- a/namespace/macos.go +++ b/namespace/macos.go @@ -36,11 +36,17 @@ func NewMacOS(config Config) (*MacOSNetJail, error) { pfRulesPath := fmt.Sprintf("/tmp/%s.pf", ns) mainRulesPath := fmt.Sprintf("/tmp/%s_main.pf", ns) + // Initialize preparedEnv with config environment variables + preparedEnv := make(map[string]string) + for key, value := range config.Env { + preparedEnv[key] = value + } + return &MacOSNetJail{ pfRulesPath: pfRulesPath, mainRulesPath: mainRulesPath, logger: config.Logger, - preparedEnv: make(map[string]string), + preparedEnv: preparedEnv, httpProxyPort: config.HttpProxyPort, httpsProxyPort: config.HttpsProxyPort, }, nil @@ -70,7 +76,7 @@ func (m *MacOSNetJail) Start() error { // Start with current environment for _, envVar := range os.Environ() { if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 { - // Only set if not already set by SetEnv + // Only set if not already set by config if _, exists := m.preparedEnv[parts[0]]; !exists { m.preparedEnv[parts[0]] = parts[1] } @@ -125,12 +131,7 @@ func (m *MacOSNetJail) Start() error { return nil } -// SetEnv sets an environment variable for commands run in the namespace -func (m *MacOSNetJail) SetEnv(key string, value string) { - m.preparedEnv[key] = value -} - -// Execute runs the command with the network jail group membership +// Command runs the command with the network jail group membership func (m *MacOSNetJail) Command(command []string) *exec.Cmd { m.logger.Debug("Command called", "command", command)