Skip to content

Commit 60ba91d

Browse files
refactor: cli and linux jail (#104)
* refactor: cli and linux jail * refactor: linux jail; remove platform-specific abstractions * refactor: linux jail
1 parent 623edd7 commit 60ba91d

File tree

11 files changed

+656
-645
lines changed

11 files changed

+656
-645
lines changed

app/app.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"time"
12+
13+
"github.com/coder/serpent"
14+
)
15+
16+
// Config holds all configuration for the CLI
17+
type Config struct {
18+
Config serpent.YAMLConfigPath `yaml:"-"`
19+
AllowListStrings serpent.StringArray `yaml:"allowlist"` // From config file
20+
AllowStrings serpent.StringArray `yaml:"-"` // From CLI flags only
21+
LogLevel serpent.String `yaml:"log_level"`
22+
LogDir serpent.String `yaml:"log_dir"`
23+
ProxyPort serpent.Int64 `yaml:"proxy_port"`
24+
PprofEnabled serpent.Bool `yaml:"pprof_enabled"`
25+
PprofPort serpent.Int64 `yaml:"pprof_port"`
26+
}
27+
28+
func isChild() bool {
29+
return os.Getenv("CHILD") == "true"
30+
}
31+
32+
// Run executes the boundary command with the given configuration and arguments
33+
func Run(ctx context.Context, config Config, args []string) error {
34+
logger, err := setupLogging(config)
35+
if err != nil {
36+
return fmt.Errorf("could not set up logging: %v", err)
37+
}
38+
39+
configInJSON, err := json.Marshal(config)
40+
if err != nil {
41+
return err
42+
}
43+
logger.Debug("config", "json_config", configInJSON)
44+
45+
if isChild() {
46+
return RunChild(logger, args)
47+
}
48+
49+
return RunParent(ctx, logger, args, config)
50+
}
51+
52+
// setupLogging creates a slog logger with the specified level
53+
func setupLogging(config Config) (*slog.Logger, error) {
54+
var level slog.Level
55+
switch strings.ToLower(config.LogLevel.Value()) {
56+
case "error":
57+
level = slog.LevelError
58+
case "warn":
59+
level = slog.LevelWarn
60+
case "info":
61+
level = slog.LevelInfo
62+
case "debug":
63+
level = slog.LevelDebug
64+
default:
65+
level = slog.LevelWarn // Default to warn if invalid level
66+
}
67+
68+
logTarget := os.Stderr
69+
70+
logDir := config.LogDir.Value()
71+
if logDir != "" {
72+
// Set up the logging directory if it doesn't exist yet
73+
if err := os.MkdirAll(logDir, 0755); err != nil {
74+
return nil, fmt.Errorf("could not set up log dir %s: %v", logDir, err)
75+
}
76+
77+
// Create a logfile (timestamp and pid to avoid race conditions with multiple boundary calls running)
78+
logFilePath := fmt.Sprintf("boundary-%s-%d.log",
79+
time.Now().Format("2006-01-02_15-04-05"),
80+
os.Getpid())
81+
82+
logFile, err := os.Create(filepath.Join(logDir, logFilePath))
83+
if err != nil {
84+
return nil, fmt.Errorf("could not create log file %s: %v", logFilePath, err)
85+
}
86+
87+
// Set the log target to the file rather than stderr.
88+
logTarget = logFile
89+
}
90+
91+
// Create a standard slog logger with the appropriate level
92+
handler := slog.NewTextHandler(logTarget, &slog.HandlerOptions{
93+
Level: level,
94+
})
95+
96+
return slog.New(handler), nil
97+
}

app/child.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package app
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"log/slog"
7+
"os"
8+
"os/exec"
9+
10+
"github.com/coder/boundary/jail"
11+
)
12+
13+
func RunChild(logger *slog.Logger, args []string) error {
14+
logger.Info("boundary CHILD process is started")
15+
16+
vethNetJail := os.Getenv("VETH_JAIL_NAME")
17+
err := jail.SetupChildNetworking(vethNetJail)
18+
if err != nil {
19+
return fmt.Errorf("failed to setup child networking: %v", err)
20+
}
21+
logger.Info("child networking is successfully configured")
22+
23+
// Program to run
24+
bin := args[0]
25+
args = args[1:]
26+
27+
cmd := exec.Command(bin, args...)
28+
cmd.Stdin = os.Stdin
29+
cmd.Stdout = os.Stdout
30+
cmd.Stderr = os.Stderr
31+
err = cmd.Run()
32+
if err != nil {
33+
log.Printf("failed to run %s: %v", bin, err)
34+
return err
35+
}
36+
37+
return nil
38+
}

app/parent.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"os"
8+
"os/signal"
9+
"strings"
10+
"syscall"
11+
12+
"github.com/coder/boundary"
13+
"github.com/coder/boundary/audit"
14+
"github.com/coder/boundary/jail"
15+
"github.com/coder/boundary/rulesengine"
16+
"github.com/coder/boundary/tls"
17+
"github.com/coder/boundary/util"
18+
)
19+
20+
func RunParent(ctx context.Context, logger *slog.Logger, args []string, config Config) error {
21+
ctx, cancel := context.WithCancel(ctx)
22+
defer cancel()
23+
24+
username, uid, gid, homeDir, configDir := util.GetUserInfo()
25+
26+
// Get command arguments
27+
if len(args) == 0 {
28+
return fmt.Errorf("no command specified")
29+
}
30+
31+
// Merge allowlist from config file with allow from CLI flags
32+
allowListStrings := config.AllowListStrings.Value()
33+
allowStrings := config.AllowStrings.Value()
34+
35+
// Combine allowlist (config file) with allow (CLI flags)
36+
allAllowStrings := append(allowListStrings, allowStrings...)
37+
38+
if len(allAllowStrings) == 0 {
39+
logger.Warn("No allow rules specified; all network traffic will be denied by default")
40+
}
41+
42+
// Parse allow rules
43+
allowRules, err := rulesengine.ParseAllowSpecs(allAllowStrings)
44+
if err != nil {
45+
logger.Error("Failed to parse allow rules", "error", err)
46+
return fmt.Errorf("failed to parse allow rules: %v", err)
47+
}
48+
49+
// Create rule engine
50+
ruleEngine := rulesengine.NewRuleEngine(allowRules, logger)
51+
52+
// Create auditor
53+
auditor := audit.NewLogAuditor(logger)
54+
55+
// Create TLS certificate manager
56+
certManager, err := tls.NewCertificateManager(tls.Config{
57+
Logger: logger,
58+
ConfigDir: configDir,
59+
Uid: uid,
60+
Gid: gid,
61+
})
62+
if err != nil {
63+
logger.Error("Failed to create certificate manager", "error", err)
64+
return fmt.Errorf("failed to create certificate manager: %v", err)
65+
}
66+
67+
// Setup TLS to get cert path for jailer
68+
tlsConfig, caCertPath, configDir, err := certManager.SetupTLSAndWriteCACert()
69+
if err != nil {
70+
return fmt.Errorf("failed to setup TLS and CA certificate: %v", err)
71+
}
72+
73+
// Create jailer with cert path from TLS setup
74+
jailer, err := jail.NewLinuxJail(jail.Config{
75+
Logger: logger,
76+
HttpProxyPort: int(config.ProxyPort.Value()),
77+
Username: username,
78+
Uid: uid,
79+
Gid: gid,
80+
HomeDir: homeDir,
81+
ConfigDir: configDir,
82+
CACertPath: caCertPath,
83+
})
84+
if err != nil {
85+
return fmt.Errorf("failed to create jailer: %v", err)
86+
}
87+
88+
// Create boundary instance
89+
boundaryInstance, err := boundary.New(ctx, boundary.Config{
90+
RuleEngine: ruleEngine,
91+
Auditor: auditor,
92+
TLSConfig: tlsConfig,
93+
Logger: logger,
94+
Jailer: jailer,
95+
ProxyPort: int(config.ProxyPort.Value()),
96+
PprofEnabled: config.PprofEnabled.Value(),
97+
PprofPort: int(config.PprofPort.Value()),
98+
})
99+
if err != nil {
100+
return fmt.Errorf("failed to create boundary instance: %v", err)
101+
}
102+
103+
// Setup signal handling BEFORE any setup
104+
sigChan := make(chan os.Signal, 1)
105+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
106+
107+
// Open boundary (starts network namespace and proxy server)
108+
err = boundaryInstance.Start()
109+
if err != nil {
110+
return fmt.Errorf("failed to open boundary: %v", err)
111+
}
112+
defer func() {
113+
logger.Info("Closing boundary...")
114+
err := boundaryInstance.Close()
115+
if err != nil {
116+
logger.Error("Failed to close boundary", "error", err)
117+
}
118+
}()
119+
120+
// Execute command in boundary
121+
go func() {
122+
defer cancel()
123+
cmd := boundaryInstance.Command(os.Args)
124+
125+
logger.Debug("Executing command in boundary", "command", strings.Join(os.Args, " "))
126+
err := cmd.Start()
127+
if err != nil {
128+
logger.Error("Command failed to start", "error", err)
129+
return
130+
}
131+
132+
err = boundaryInstance.ConfigureAfterCommandExecution(cmd.Process.Pid)
133+
if err != nil {
134+
logger.Error("configuration after command execution failed", "error", err)
135+
return
136+
}
137+
138+
logger.Debug("waiting on a child process to finish")
139+
err = cmd.Wait()
140+
if err != nil {
141+
logger.Error("Command execution failed", "error", err)
142+
return
143+
}
144+
}()
145+
146+
// Wait for signal or context cancellation
147+
select {
148+
case sig := <-sigChan:
149+
logger.Info("Received signal, shutting down...", "signal", sig)
150+
cancel()
151+
case <-ctx.Done():
152+
// Context cancelled by command completion
153+
logger.Info("Command completed, shutting down...")
154+
}
155+
156+
return nil
157+
}

0 commit comments

Comments
 (0)