1
1
package cli
2
2
3
3
import (
4
+ "context"
5
+ cryptotls "crypto/tls"
4
6
"fmt"
5
7
"log/slog"
6
8
"os"
9
+ "os/signal"
10
+ "path/filepath"
11
+ "strings"
12
+ "syscall"
13
+ "time"
7
14
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"
9
20
"github.com/coder/serpent"
10
21
)
11
22
@@ -38,24 +49,32 @@ Examples:
38
49
# Block everything by default (implicit)` ,
39
50
Options : serpent.OptionSet {
40
51
{
52
+ Name : "allow" ,
41
53
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'." ,
43
56
Value : serpent .StringArrayOf (& config .AllowStrings ),
44
57
},
45
58
{
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" ,
46
67
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)." ,
48
70
Default : "warn" ,
49
71
Value : serpent .StringOf (& config .LogLevel ),
50
72
},
51
73
{
52
- Flag : "no-tls-intercept" ,
53
- Description : "Disable HTTPS interception" ,
54
- Value : serpent .BoolOf (& config .NoTLSIntercept ),
55
- },
56
- {
74
+ Name : "no-jail-cleanup" ,
57
75
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)." ,
59
78
Value : serpent .BoolOf (& config .NoJailCleanup ),
60
79
Hidden : true ,
61
80
},
@@ -66,7 +85,31 @@ Examples:
66
85
}
67
86
}
68
87
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
70
113
func Run (config Config , args []string ) error {
71
114
logger := setupLogging (config .LogLevel )
72
115
@@ -75,46 +118,172 @@ func Run(config Config, args []string) error {
75
118
return fmt .Errorf ("no command specified" )
76
119
}
77
120
78
- // Warn if no allow rules specified
121
+ // Parse allow list; default to deny-all if none provided
79
122
if len (config .AllowStrings ) == 0 {
80
123
logger .Warn ("No allow rules specified; all network traffic will be denied by default" )
81
124
}
82
125
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 )
89
130
}
90
131
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 ()
93
141
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 )
95
144
}
96
145
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 )
100
150
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 )
115
286
}
116
287
117
- return slog .New (slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {
118
- Level : slogLevel ,
119
- }))
288
+ return nil
120
289
}
0 commit comments