11package cli
22
33import (
4+ "context"
5+ cryptotls "crypto/tls"
46 "fmt"
57 "log/slog"
68 "os"
9+ "os/signal"
10+ "path/filepath"
11+ "strings"
12+ "syscall"
13+ "time"
714
8- "github.com/coder/jail"
15+ "github.com/coder/jail/audit"
16+ "github.com/coder/jail/network"
17+ "github.com/coder/jail/proxy"
18+ "github.com/coder/jail/rules"
19+ "github.com/coder/jail/tls"
920 "github.com/coder/serpent"
1021)
1122
@@ -38,24 +49,32 @@ Examples:
3849 # Block everything by default (implicit)` ,
3950 Options : serpent.OptionSet {
4051 {
52+ Name : "allow" ,
4153 Flag : "allow" ,
42- Description : "Allow rule (repeatable). Format: \" pattern\" or \" METHOD[,METHOD] pattern\" " ,
54+ Env : "JAIL_ALLOW" ,
55+ Description : "Allow rule (can be specified multiple times). Format: 'pattern' or 'METHOD[,METHOD] pattern'." ,
4356 Value : serpent .StringArrayOf (& config .AllowStrings ),
4457 },
4558 {
59+ Name : "no-tls-intercept" ,
60+ Flag : "no-tls-intercept" ,
61+ Env : "JAIL_NO_TLS_INTERCEPT" ,
62+ Description : "Disable HTTPS interception." ,
63+ Value : serpent .BoolOf (& config .NoTLSIntercept ),
64+ },
65+ {
66+ Name : "log-level" ,
4667 Flag : "log-level" ,
47- Description : "Set log level (error, warn, info, debug)" ,
68+ Env : "JAIL_LOG_LEVEL" ,
69+ Description : "Set log level (error, warn, info, debug)." ,
4870 Default : "warn" ,
4971 Value : serpent .StringOf (& config .LogLevel ),
5072 },
5173 {
52- Flag : "no-tls-intercept" ,
53- Description : "Disable HTTPS interception" ,
54- Value : serpent .BoolOf (& config .NoTLSIntercept ),
55- },
56- {
74+ Name : "no-jail-cleanup" ,
5775 Flag : "no-jail-cleanup" ,
58- Description : "Disable jail cleanup (for debugging)" ,
76+ Env : "JAIL_NO_JAIL_CLEANUP" ,
77+ Description : "Skip jail cleanup (hidden flag for testing)." ,
5978 Value : serpent .BoolOf (& config .NoJailCleanup ),
6079 Hidden : true ,
6180 },
@@ -66,7 +85,31 @@ Examples:
6685 }
6786}
6887
69- // Run executes the jail with the given configuration and command arguments
88+ // setupLogging creates a slog logger with the specified level
89+ func setupLogging (logLevel string ) * slog.Logger {
90+ var level slog.Level
91+ switch strings .ToLower (logLevel ) {
92+ case "error" :
93+ level = slog .LevelError
94+ case "warn" :
95+ level = slog .LevelWarn
96+ case "info" :
97+ level = slog .LevelInfo
98+ case "debug" :
99+ level = slog .LevelDebug
100+ default :
101+ level = slog .LevelWarn // Default to warn if invalid level
102+ }
103+
104+ // Create a standard slog logger with the appropriate level
105+ handler := slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {
106+ Level : level ,
107+ })
108+
109+ return slog .New (handler )
110+ }
111+
112+ // Run executes the jail command with the given configuration and arguments
70113func Run (config Config , args []string ) error {
71114 logger := setupLogging (config .LogLevel )
72115
@@ -75,46 +118,172 @@ func Run(config Config, args []string) error {
75118 return fmt .Errorf ("no command specified" )
76119 }
77120
78- // Warn if no allow rules specified
121+ // Parse allow list; default to deny-all if none provided
79122 if len (config .AllowStrings ) == 0 {
80123 logger .Warn ("No allow rules specified; all network traffic will be denied by default" )
81124 }
82125
83- // Create jail configuration
84- jailConfig := jail.Config {
85- AllowRules : config .AllowStrings ,
86- NoTLSIntercept : config .NoTLSIntercept ,
87- Logger : logger ,
88- SkipCleanup : config .NoJailCleanup ,
126+ allowRules , err := rules .ParseAllowSpecs (config .AllowStrings )
127+ if err != nil {
128+ logger .Error ("Failed to parse allow rules" , "error" , err )
129+ return fmt .Errorf ("failed to parse allow rules: %v" , err )
89130 }
90131
91- // Create jail instance
92- j , err := jail .New (jailConfig )
132+ // Implicit final deny-all is handled by the RuleEngine default behavior when no rules match.
133+ // Build final rules slice in order: user allows only.
134+ ruleList := allowRules
135+
136+ // Create rule engine
137+ ruleEngine := rules .NewRuleEngine (ruleList , logger )
138+
139+ // Get configuration directory
140+ configDir , err := tls .GetConfigDir ()
93141 if err != nil {
94- return fmt .Errorf ("failed to create jail: %v" , err )
142+ logger .Error ("Failed to get config directory" , "error" , err )
143+ return fmt .Errorf ("failed to get config directory: %v" , err )
95144 }
96145
97- // Run the command in the jail
98- return j .Run (args , nil )
99- }
146+ // Create certificate manager (if TLS interception is enabled)
147+ var certManager * tls.CertificateManager
148+ var tlsConfig * cryptotls.Config
149+ var extraEnv map [string ]string = make (map [string ]string )
100150
101- // setupLogging configures and returns a logger based on the log level
102- func setupLogging (level string ) * slog.Logger {
103- var slogLevel slog.Level
104- switch level {
105- case "debug" :
106- slogLevel = slog .LevelDebug
107- case "info" :
108- slogLevel = slog .LevelInfo
109- case "warn" :
110- slogLevel = slog .LevelWarn
111- case "error" :
112- slogLevel = slog .LevelError
113- default :
114- slogLevel = slog .LevelWarn
151+ if ! config .NoTLSIntercept {
152+ certManager , err = tls .NewCertificateManager (configDir , logger )
153+ if err != nil {
154+ logger .Error ("Failed to create certificate manager" , "error" , err )
155+ return fmt .Errorf ("failed to create certificate manager: %v" , err )
156+ }
157+
158+ tlsConfig = certManager .GetTLSConfig ()
159+
160+ // Get CA certificate for environment
161+ caCertPEM , err := certManager .GetCACertPEM ()
162+ if err != nil {
163+ logger .Error ("Failed to get CA certificate" , "error" , err )
164+ return fmt .Errorf ("failed to get CA certificate: %v" , err )
165+ }
166+
167+ // Write CA certificate to a temporary file for tools that need a file path
168+ caCertPath := filepath .Join (configDir , "ca-cert.pem" )
169+ err = os .WriteFile (caCertPath , caCertPEM , 0644 )
170+ if err != nil {
171+ logger .Error ("Failed to write CA certificate file" , "error" , err )
172+ return fmt .Errorf ("failed to write CA certificate file: %v" , err )
173+ }
174+
175+ // Set standard CA certificate environment variables for common tools
176+ // This makes tools like curl, git, etc. trust our dynamically generated CA
177+ extraEnv ["SSL_CERT_FILE" ] = caCertPath // OpenSSL/LibreSSL-based tools
178+ extraEnv ["SSL_CERT_DIR" ] = configDir // OpenSSL certificate directory
179+ extraEnv ["CURL_CA_BUNDLE" ] = caCertPath // curl
180+ extraEnv ["GIT_SSL_CAINFO" ] = caCertPath // Git
181+ extraEnv ["REQUESTS_CA_BUNDLE" ] = caCertPath // Python requests
182+ extraEnv ["NODE_EXTRA_CA_CERTS" ] = caCertPath // Node.js
183+ extraEnv ["JAIL_CA_CERT" ] = string (caCertPEM ) // Keep for backward compatibility
184+ }
185+
186+ // Create network jail configuration
187+ networkConfig := network.JailConfig {
188+ HTTPPort : 8040 ,
189+ HTTPSPort : 8043 ,
190+ NetJailName : "jail" ,
191+ SkipCleanup : config .NoJailCleanup ,
192+ }
193+
194+ // Create network jail
195+ networkInstance , err := network .NewJail (networkConfig , logger )
196+ if err != nil {
197+ logger .Error ("Failed to create network jail" , "error" , err )
198+ return fmt .Errorf ("failed to create network jail: %v" , err )
199+ }
200+
201+ // Setup signal handling BEFORE any network setup
202+ sigChan := make (chan os.Signal , 1 )
203+ signal .Notify (sigChan , syscall .SIGINT , syscall .SIGTERM )
204+
205+ // Handle signals immediately in background
206+ go func () {
207+ sig := <- sigChan
208+ logger .Info ("Received signal during setup, cleaning up..." , "signal" , sig )
209+ err := networkInstance .Cleanup ()
210+ if err != nil {
211+ logger .Error ("Emergency cleanup failed" , "error" , err )
212+ }
213+ os .Exit (1 )
214+ }()
215+
216+ // Ensure cleanup happens no matter what
217+ defer func () {
218+ logger .Debug ("Starting cleanup process" )
219+ err := networkInstance .Cleanup ()
220+ if err != nil {
221+ logger .Error ("Failed to cleanup network jail" , "error" , err )
222+ } else {
223+ logger .Debug ("Cleanup completed successfully" )
224+ }
225+ }()
226+
227+ // Setup network jail
228+ err = networkInstance .Setup (networkConfig .HTTPPort , networkConfig .HTTPSPort )
229+ if err != nil {
230+ logger .Error ("Failed to setup network jail" , "error" , err )
231+ return fmt .Errorf ("failed to setup network jail: %v" , err )
232+ }
233+
234+ // Create auditor
235+ auditor := audit .NewLoggingAuditor (logger )
236+
237+ // Create proxy server
238+ proxyConfig := proxy.Config {
239+ HTTPPort : networkConfig .HTTPPort ,
240+ HTTPSPort : networkConfig .HTTPSPort ,
241+ RuleEngine : ruleEngine ,
242+ Auditor : auditor ,
243+ Logger : logger ,
244+ TLSConfig : tlsConfig ,
245+ }
246+
247+ proxyServer := proxy .NewProxyServer (proxyConfig )
248+
249+ // Create context for graceful shutdown
250+ ctx , cancel := context .WithCancel (context .Background ())
251+ defer cancel ()
252+
253+ // Start proxy server in background
254+ go func () {
255+ err := proxyServer .Start (ctx )
256+ if err != nil {
257+ logger .Error ("Proxy server error" , "error" , err )
258+ }
259+ }()
260+
261+ // Give proxy time to start
262+ time .Sleep (100 * time .Millisecond )
263+
264+ // Execute command in network jail
265+ go func () {
266+ defer cancel ()
267+ err := networkInstance .Execute (args , extraEnv )
268+ if err != nil {
269+ logger .Error ("Command execution failed" , "error" , err )
270+ }
271+ }()
272+
273+ // Wait for signal or context cancellation
274+ select {
275+ case sig := <- sigChan :
276+ logger .Info ("Received signal, shutting down..." , "signal" , sig )
277+ cancel ()
278+ case <- ctx .Done ():
279+ // Context cancelled by command completion
280+ }
281+
282+ // Stop proxy server
283+ err = proxyServer .Stop ()
284+ if err != nil {
285+ logger .Error ("Failed to stop proxy server" , "error" , err )
115286 }
116287
117- return slog .New (slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {
118- Level : slogLevel ,
119- }))
288+ return nil
120289}
0 commit comments