Skip to content

Commit e81272d

Browse files
authored
Merge branch 'main' into bcpeinhardt/simple-allow-impl
2 parents a2174de + f528a5d commit e81272d

File tree

15 files changed

+913
-784
lines changed

15 files changed

+913
-784
lines changed

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,36 @@ jobs:
7373
- name: Download and verify dependencies
7474
run: make deps
7575

76+
# Before (default):
77+
# - /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf
78+
# - stub-resolv.conf points to 127.0.0.53 (systemd-resolved stub listener)
79+
# - systemd-resolved forwards to the real upstream file:
80+
# /run/systemd/resolve/resolv.conf
81+
# Flow: /etc/resolv.conf -> stub-resolv.conf (127.0.0.53) -> systemd-resolved -> /run/systemd/resolve/resolv.conf
82+
#
83+
# After (bind-mount):
84+
# - /etc/resolv.conf is bind-mounted to /run/systemd/resolve/resolv.conf
85+
# - processes read upstream nameservers directly from /run/systemd/resolve/resolv.conf
86+
# Flow: /etc/resolv.conf -> /run/systemd/resolve/resolv.conf
87+
#
88+
# This makes processes talk directly to the upstream DNS servers and
89+
# bypasses the systemd-resolved *stub listener* (127.0.0.53).
90+
#
91+
# Important nuance: systemd-resolved itself is NOT stopped; it still runs and updates
92+
# /run/systemd/resolve/resolv.conf. Because this is a bind (not a copy), updates to the
93+
# upstream list are visible. Trade-off: we lose the stub’s features (caching,
94+
# split-DNS/VPN per-interface behavior, DNSSEC/DoT/DoH mediation, mDNS/LLMNR).
95+
#
96+
# Reason: network namespaces have their own network stack (interfaces, routes and loopback).
97+
# A process inside a network namespace resolves 127.0.0.53 against that namespace’s loopback, not the host’s,
98+
# and systemd-resolved usually listens on the host loopback. As a result the stub at 127.0.0.53 is often
99+
# unreachable from an isolated namespace and DNS lookups fail.
100+
# Bind-mounting /run/systemd/resolve/resolv.conf over /etc/resolv.conf forces processes to use the upstream
101+
# nameservers directly, avoiding that failure.
102+
- name: Change DNS configuration
103+
if: runner.os == 'Linux'
104+
run: sudo mount --bind /run/systemd/resolve/resolv.conf /etc/resolv.conf
105+
76106
- name: Run unit tests
77107
run: make unit-test
78108

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ e2e-test:
6262
echo "E2E tests require Linux platform. Current platform: $$(uname)"; \
6363
exit 1; \
6464
fi
65-
sudo $(shell which go) test -v -race ./e2e_tests
65+
sudo $(shell which go) test -v -race ./e2e_tests -count=1
6666
@echo "✓ E2E tests passed!"
6767

6868
# Run tests with coverage (needs sudo for E2E tests)

audit/log_auditor.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ func (a *LogAuditor) AuditRequest(req Request) {
2020
a.logger.Info("ALLOW",
2121
"method", req.Method,
2222
"url", req.URL,
23+
"host", req.Host,
2324
"rule", req.Rule)
2425
} else {
2526
a.logger.Warn("DENY",
2627
"method", req.Method,
27-
"url", req.URL)
28+
"url", req.URL,
29+
"host", req.Host,
30+
)
2831
}
2932
}

audit/request.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Auditor interface {
88
type Request struct {
99
Method string
1010
URL string
11+
Host string
1112
Allowed bool
1213
Rule string // The rule that matched (if any)
1314
}

boundary.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ type Config struct {
2020
TLSConfig *tls.Config
2121
Logger *slog.Logger
2222
Jailer jail.Jailer
23+
ProxyPort int
24+
PprofEnabled bool
25+
PprofPort int
2326
}
2427

2528
type Boundary struct {
@@ -34,11 +37,13 @@ type Boundary struct {
3437
func New(ctx context.Context, config Config) (*Boundary, error) {
3538
// Create proxy server
3639
proxyServer := proxy.NewProxyServer(proxy.Config{
37-
HTTPPort: 8080,
38-
RuleEngine: config.RuleEngine,
39-
Auditor: config.Auditor,
40-
Logger: config.Logger,
41-
TLSConfig: config.TLSConfig,
40+
HTTPPort: config.ProxyPort,
41+
RuleEngine: config.RuleEngine,
42+
Auditor: config.Auditor,
43+
Logger: config.Logger,
44+
TLSConfig: config.TLSConfig,
45+
PprofEnabled: config.PprofEnabled,
46+
PprofPort: config.PprofPort,
4247
})
4348

4449
// Create cancellable context for boundary
@@ -55,8 +60,8 @@ func New(ctx context.Context, config Config) (*Boundary, error) {
5560
}
5661

5762
func (b *Boundary) Start() error {
58-
// Start the jailer (network isolation)
59-
err := b.jailer.Start()
63+
// Configure the jailer (network isolation)
64+
err := b.jailer.ConfigureBeforeCommandExecution()
6065
if err != nil {
6166
return fmt.Errorf("failed to start jailer: %v", err)
6267
}
@@ -78,6 +83,10 @@ func (b *Boundary) Command(command []string) *exec.Cmd {
7883
return b.jailer.Command(command)
7984
}
8085

86+
func (b *Boundary) ConfigureAfterCommandExecution(processPID int) error {
87+
return b.jailer.ConfigureAfterCommandExecution(processPID)
88+
}
89+
8190
func (b *Boundary) Close() error {
8291
// Stop proxy server
8392
if b.proxyServer != nil {

cli/cli.go

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package cli
33
import (
44
"context"
55
"fmt"
6+
"log"
67
"log/slog"
78
"os"
9+
"os/exec"
810
"os/signal"
911
"path/filepath"
1012
"strings"
@@ -26,6 +28,9 @@ type Config struct {
2628
LogLevel string
2729
LogDir string
2830
Unprivileged bool
31+
ProxyPort int64
32+
PprofEnabled bool
33+
PprofPort int64
2934
}
3035

3136
// NewCommand creates and returns the root serpent command
@@ -84,6 +89,26 @@ func BaseCommand() *serpent.Command {
8489
Description: "Run in unprivileged mode (no network isolation, uses proxy environment variables).",
8590
Value: serpent.BoolOf(&config.Unprivileged),
8691
},
92+
{
93+
Flag: "proxy-port",
94+
Env: "PROXY_PORT",
95+
Description: "Set a port for HTTP proxy.",
96+
Default: "8080",
97+
Value: serpent.Int64Of(&config.ProxyPort),
98+
},
99+
{
100+
Flag: "pprof",
101+
Env: "BOUNDARY_PPROF",
102+
Description: "Enable pprof profiling server.",
103+
Value: serpent.BoolOf(&config.PprofEnabled),
104+
},
105+
{
106+
Flag: "pprof-port",
107+
Env: "BOUNDARY_PPROF_PORT",
108+
Description: "Set port for pprof profiling server.",
109+
Default: "6060",
110+
Value: serpent.Int64Of(&config.PprofPort),
111+
},
87112
},
88113
Handler: func(inv *serpent.Invocation) error {
89114
args := inv.Args
@@ -92,15 +117,47 @@ func BaseCommand() *serpent.Command {
92117
}
93118
}
94119

120+
func isChild() bool {
121+
return os.Getenv("CHILD") == "true"
122+
}
123+
95124
// Run executes the boundary command with the given configuration and arguments
96125
func Run(ctx context.Context, config Config, args []string) error {
97-
ctx, cancel := context.WithCancel(ctx)
98-
defer cancel()
99-
100126
logger, err := setupLogging(config)
101127
if err != nil {
102128
return fmt.Errorf("could not set up logging: %v", err)
103129
}
130+
131+
if isChild() {
132+
logger.Info("boundary CHILD process is started")
133+
134+
vethNetJail := os.Getenv("VETH_JAIL_NAME")
135+
err := jail.SetupChildNetworking(vethNetJail)
136+
if err != nil {
137+
return fmt.Errorf("failed to setup child networking: %v", err)
138+
}
139+
logger.Info("child networking is successfully configured")
140+
141+
// Program to run
142+
bin := args[0]
143+
args = args[1:]
144+
145+
cmd := exec.Command(bin, args...)
146+
cmd.Stdin = os.Stdin
147+
cmd.Stdout = os.Stdout
148+
cmd.Stderr = os.Stderr
149+
err = cmd.Run()
150+
if err != nil {
151+
log.Printf("failed to run %s: %v", bin, err)
152+
return err
153+
}
154+
155+
return nil
156+
}
157+
158+
ctx, cancel := context.WithCancel(ctx)
159+
defer cancel()
160+
104161
username, uid, gid, homeDir, configDir := util.GetUserInfo()
105162

106163
// Get command arguments
@@ -147,7 +204,7 @@ func Run(ctx context.Context, config Config, args []string) error {
147204
// Create jailer with cert path from TLS setup
148205
jailer, err := createJailer(jail.Config{
149206
Logger: logger,
150-
HttpProxyPort: 8080,
207+
HttpProxyPort: int(config.ProxyPort),
151208
Username: username,
152209
Uid: uid,
153210
Gid: gid,
@@ -161,11 +218,14 @@ func Run(ctx context.Context, config Config, args []string) error {
161218

162219
// Create boundary instance
163220
boundaryInstance, err := boundary.New(ctx, boundary.Config{
164-
RuleEngine: ruleEngine,
165-
Auditor: auditor,
166-
TLSConfig: tlsConfig,
167-
Logger: logger,
168-
Jailer: jailer,
221+
RuleEngine: ruleEngine,
222+
Auditor: auditor,
223+
TLSConfig: tlsConfig,
224+
Logger: logger,
225+
Jailer: jailer,
226+
ProxyPort: int(config.ProxyPort),
227+
PprofEnabled: config.PprofEnabled,
228+
PprofPort: int(config.PprofPort),
169229
})
170230
if err != nil {
171231
return fmt.Errorf("failed to create boundary instance: %v", err)
@@ -191,15 +251,26 @@ func Run(ctx context.Context, config Config, args []string) error {
191251
// Execute command in boundary
192252
go func() {
193253
defer cancel()
194-
cmd := boundaryInstance.Command(args)
195-
cmd.Stderr = os.Stderr
196-
cmd.Stdout = os.Stdout
197-
cmd.Stdin = os.Stdin
254+
cmd := boundaryInstance.Command(os.Args)
255+
256+
logger.Debug("Executing command in boundary", "command", strings.Join(os.Args, " "))
257+
err := cmd.Start()
258+
if err != nil {
259+
logger.Error("Command failed to start", "error", err)
260+
return
261+
}
262+
263+
err = boundaryInstance.ConfigureAfterCommandExecution(cmd.Process.Pid)
264+
if err != nil {
265+
logger.Error("configuration after command execution failed", "error", err)
266+
return
267+
}
198268

199-
logger.Debug("Executing command in boundary", "command", strings.Join(args, " "))
200-
err := cmd.Run()
269+
logger.Debug("waiting on a child process to finish")
270+
err = cmd.Wait()
201271
if err != nil {
202272
logger.Error("Command execution failed", "error", err)
273+
return
203274
}
204275
}()
205276

0 commit comments

Comments
 (0)