@@ -5,9 +5,11 @@ import (
5
5
"fmt"
6
6
"log/slog"
7
7
"os"
8
+ "os/exec"
8
9
"os/signal"
9
10
"os/user"
10
11
"path/filepath"
12
+ "runtime"
11
13
"strconv"
12
14
"strings"
13
15
"syscall"
@@ -24,6 +26,7 @@ import (
24
26
type Config struct {
25
27
AllowStrings []string
26
28
LogLevel string
29
+ Unprivileged bool // Enable unprivileged mode (user namespace + iptables)
27
30
}
28
31
29
32
// NewCommand creates and returns the root serpent command
@@ -37,14 +40,22 @@ func NewCommand() *serpent.Command {
37
40
intercepting all HTTP/HTTPS traffic through a transparent proxy that enforces
38
41
user-defined rules.
39
42
43
+ Modes:
44
+ Default (privileged): Uses network namespaces + iptables (requires sudo)
45
+ Unprivileged: Uses user namespaces + iptables (no sudo required)
46
+
40
47
Examples:
41
- # Allow only requests to github.com
42
- jail --allow "github.com" -- curl https://github.com
48
+ # Privileged mode (original behavior)
49
+ sudo jail --allow "github.com" -- curl https://github.com
50
+
51
+ # Unprivileged mode (NEW!)
52
+ jail --unprivileged --allow "github.com" -- curl https://github.com
43
53
44
- # Monitor all requests to specific domains (allow only those)
45
- jail --allow "github.com/api/issues/*" --allow "GET,HEAD github.com" -- npm install
54
+ # Monitor all requests to specific domains
55
+ jail --unprivileged -- allow "github.com/api/issues/*" --allow "GET,HEAD github.com" -- npm install
46
56
47
- # Block everything by default (implicit)` ,
57
+ # Block everything by default (implicit)
58
+ jail --unprivileged --allow "api.example.com" -- ./my-app` ,
48
59
Options : serpent.OptionSet {
49
60
{
50
61
Name : "allow" ,
@@ -61,6 +72,13 @@ Examples:
61
72
Default : "warn" ,
62
73
Value : serpent .StringOf (& config .LogLevel ),
63
74
},
75
+ {
76
+ Name : "unprivileged" ,
77
+ Flag : "unprivileged" ,
78
+ Env : "JAIL_UNPRIVILEGED" ,
79
+ Description : "Use unprivileged mode (user namespace + iptables, no sudo required, Linux only)." ,
80
+ Value : serpent .BoolOf (& config .Unprivileged ),
81
+ },
64
82
},
65
83
Handler : func (inv * serpent.Invocation ) error {
66
84
return Run (inv .Context (), config , inv .Args )
@@ -75,6 +93,16 @@ func Run(ctx context.Context, config Config, args []string) error {
75
93
logger := setupLogging (config .LogLevel )
76
94
userInfo := getUserInfo ()
77
95
96
+ // Validate unprivileged mode if requested
97
+ if config .Unprivileged {
98
+ if err := validateUnprivilegedMode (logger ); err != nil {
99
+ return fmt .Errorf ("unprivileged mode validation failed: %v" , err )
100
+ }
101
+ logger .Info ("Using unprivileged mode (user namespace + iptables, no sudo required)" )
102
+ } else {
103
+ logger .Info ("Using privileged mode (network namespace + iptables, requires sudo)" )
104
+ }
105
+
78
106
// Get command arguments
79
107
if len (args ) == 0 {
80
108
return fmt .Errorf ("no command specified" )
@@ -109,12 +137,26 @@ func Run(ctx context.Context, config Config, args []string) error {
109
137
}
110
138
111
139
// Create jail instance
112
- jailInstance , err := jail .New (ctx , jail.Config {
113
- RuleEngine : ruleEngine ,
114
- Auditor : auditor ,
115
- CertManager : certManager ,
116
- Logger : logger ,
117
- })
140
+ var jailInstance JailInterface
141
+ if config .Unprivileged {
142
+ // Use enhanced jail with unprivileged mode
143
+ enhancedConfig := jail.EnhancedConfig {
144
+ RuleEngine : ruleEngine ,
145
+ Auditor : auditor ,
146
+ CertManager : certManager ,
147
+ Logger : logger ,
148
+ Unprivileged : true ,
149
+ }
150
+ jailInstance , err = jail .NewEnhanced (ctx , enhancedConfig )
151
+ } else {
152
+ // Use regular jail (privileged mode)
153
+ jailInstance , err = jail .New (ctx , jail.Config {
154
+ RuleEngine : ruleEngine ,
155
+ Auditor : auditor ,
156
+ CertManager : certManager ,
157
+ Logger : logger ,
158
+ })
159
+ }
118
160
if err != nil {
119
161
return fmt .Errorf ("failed to create jail instance: %v" , err )
120
162
}
@@ -255,3 +297,39 @@ func getConfigDir(homeDir string) string {
255
297
}
256
298
return filepath .Join (homeDir , ".config" , "coder_jail" )
257
299
}
300
+
301
+ // JailInterface defines the common interface for both jail types
302
+ type JailInterface interface {
303
+ Start () error
304
+ Command (command []string ) * exec.Cmd
305
+ Close () error
306
+ }
307
+
308
+ // validateUnprivilegedMode checks if the system supports unprivileged mode
309
+ func validateUnprivilegedMode (logger * slog.Logger ) error {
310
+ // Check if we're on Linux
311
+ if runtime .GOOS != "linux" {
312
+ return fmt .Errorf ("unprivileged mode only supports Linux, got: %s" , runtime .GOOS )
313
+ }
314
+
315
+ // Check if user namespaces are enabled
316
+ userNSFile := "/proc/sys/kernel/unprivileged_userns_clone"
317
+ if data , err := os .ReadFile (userNSFile ); err == nil {
318
+ if len (data ) > 0 && strings .TrimSpace (string (data )) != "1" {
319
+ return fmt .Errorf ("user namespaces are disabled. Enable with: sudo sysctl -w kernel.unprivileged_userns_clone=1" )
320
+ }
321
+ } else {
322
+ logger .Warn ("Could not check user namespace support" , "error" , err )
323
+ }
324
+
325
+ // Check for required tools
326
+ requiredTools := []string {"unshare" , "nsenter" , "iptables" , "ip" }
327
+ for _ , tool := range requiredTools {
328
+ if _ , err := exec .LookPath (tool ); err != nil {
329
+ return fmt .Errorf ("required tool %s not found. Install with: sudo apt-get install util-linux iptables iproute2" , tool )
330
+ }
331
+ }
332
+
333
+ logger .Debug ("Unprivileged mode validation passed" )
334
+ return nil
335
+ }
0 commit comments