From a41ca72b96a0b537de401464cb4a5b6ee2fec217 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 12 Dec 2025 20:57:31 +0000 Subject: [PATCH 01/10] feat: implement landjail --- cli/cli.go | 5 +++ landjail/landjail.go | 75 ++++++++++++++++++++++++++++++++++++++++ nsjail_manager/parent.go | 7 +--- nsjail_manager/run.go | 2 +- run/run.go | 11 ++++++ 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 landjail/landjail.go create mode 100644 run/run.go diff --git a/cli/cli.go b/cli/cli.go index 7d1a20c..17c025c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -134,6 +134,11 @@ func BaseCommand() *serpent.Command { } logger.Debug("Application config", "config", appConfigInJSON) + // Get command arguments + if len(inv.Args) == 0 { + return fmt.Errorf("no command specified") + } + return nsjail_manager.Run(inv.Context(), logger, appConfig, inv.Args) }, } diff --git a/landjail/landjail.go b/landjail/landjail.go new file mode 100644 index 0000000..58e9e8b --- /dev/null +++ b/landjail/landjail.go @@ -0,0 +1,75 @@ +package landjail + +import ( + "fmt" + "log" + "os" + "os/exec" + "syscall" + + "github.com/landlock-lsm/go-landlock/landlock" +) + +type Config struct { + //BindTCPPorts []int + ConnectTCPPorts []int +} + +func Apply(cfg Config) error { + // Get the Landlock version which works for Kernel 6.7+ + llCfg := landlock.V4 + + // Collect our rules + var netRules []landlock.Rule + + // Add rules for TCP port binding + //for _, port := range cfg.BindTCPPorts { + // log.Debug("Adding TCP bind port: %d", port) + // net_rules = append(net_rules, landlock.BindTCP(uint16(port))) + //} + + // Add rules for TCP connections + for _, port := range cfg.ConnectTCPPorts { + log.Printf("Adding TCP connect port: %d", port) + netRules = append(netRules, landlock.ConnectTCP(uint16(port))) + } + + err := llCfg.RestrictNet(netRules...) + if err != nil { + return fmt.Errorf("failed to apply Landlock network restrictions: %w", err) + } + + return nil +} + +func Run(args []string, env []string) error { + binary, err := exec.LookPath(args[0]) + if err != nil { + return err + } + + log.Printf("Executing: %v", args) + + // Only pass the explicitly specified environment variables + // If env is empty, no environment variables will be passed + return syscall.Exec(binary, args, env) +} + +func main() { + fmt.Printf("OK\n") + + cfg := Config{ + ConnectTCPPorts: []int{80}, + } + err := Apply(cfg) + if err != nil { + log.Fatalf("failed to apply Landlock network restrictions: %v", err) + } + + log.Printf("os.Args[1:]: %v", os.Args[1:]) + + err = Run(os.Args[1:], os.Environ()) + if err != nil { + log.Fatalf("failed to apply Landlock network restrictions: %v", err) + } +} diff --git a/nsjail_manager/parent.go b/nsjail_manager/parent.go index a289fdf..533abc7 100644 --- a/nsjail_manager/parent.go +++ b/nsjail_manager/parent.go @@ -13,14 +13,9 @@ import ( "github.com/coder/boundary/util" ) -func RunParent(ctx context.Context, logger *slog.Logger, args []string, config config.AppConfig) error { +func RunParent(ctx context.Context, logger *slog.Logger, config config.AppConfig) error { _, uid, gid, homeDir, configDir := util.GetUserInfo() - // Get command arguments - if len(args) == 0 { - return fmt.Errorf("no command specified") - } - if len(config.AllowRules) == 0 { logger.Warn("No allow rules specified; all network traffic will be denied by default") } diff --git a/nsjail_manager/run.go b/nsjail_manager/run.go index f8efb8d..dbce8f5 100644 --- a/nsjail_manager/run.go +++ b/nsjail_manager/run.go @@ -21,5 +21,5 @@ func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig, args return RunChild(logger, args) } - return RunParent(ctx, logger, args, config) + return RunParent(ctx, logger, config) } diff --git a/run/run.go b/run/run.go new file mode 100644 index 0000000..c075d0b --- /dev/null +++ b/run/run.go @@ -0,0 +1,11 @@ +package run + +import "context" + +func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig, args []string) error { + //if isChild() { + // return RunChild(logger, args) + //} + // + //return RunParent(ctx, logger, args, config) +} From ca4900e66abf3dc79d7d9577e4a25b2f39257378 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 12 Dec 2025 21:06:29 +0000 Subject: [PATCH 02/10] temporary commit --- cli/cli.go | 13 ++++++------- config/config.go | 4 +++- nsjail_manager/child.go | 6 +++--- nsjail_manager/run.go | 4 ++-- run/run.go | 9 +++++++-- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 17c025c..a1c78cf 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -121,7 +121,11 @@ func BaseCommand() *serpent.Command { }, }, Handler: func(inv *serpent.Invocation) error { - appConfig := config.NewAppConfigFromCliConfig(cliConfig) + appConfig := config.NewAppConfigFromCliConfig(cliConfig, inv.Args) + // Get command arguments + if len(appConfig.TargetCMD) == 0 { + return fmt.Errorf("no command specified") + } logger, err := log.SetupLogging(appConfig) if err != nil { @@ -134,12 +138,7 @@ func BaseCommand() *serpent.Command { } logger.Debug("Application config", "config", appConfigInJSON) - // Get command arguments - if len(inv.Args) == 0 { - return fmt.Errorf("no command specified") - } - - return nsjail_manager.Run(inv.Context(), logger, appConfig, inv.Args) + return nsjail_manager.Run(inv.Context(), logger, appConfig) }, } } diff --git a/config/config.go b/config/config.go index 76176b4..9801d8d 100644 --- a/config/config.go +++ b/config/config.go @@ -24,9 +24,10 @@ type AppConfig struct { PprofEnabled bool PprofPort int64 ConfigureDNSForLocalStubResolver bool + TargetCMD []string } -func NewAppConfigFromCliConfig(cfg CliConfig) AppConfig { +func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) AppConfig { // Merge allowlist from config file with allow from CLI flags allowListStrings := cfg.AllowListStrings.Value() allowStrings := cfg.AllowStrings.Value() @@ -42,5 +43,6 @@ func NewAppConfigFromCliConfig(cfg CliConfig) AppConfig { PprofEnabled: cfg.PprofEnabled.Value(), PprofPort: cfg.PprofPort.Value(), ConfigureDNSForLocalStubResolver: cfg.ConfigureDNSForLocalStubResolver.Value(), + TargetCMD: targetCMD, } } diff --git a/nsjail_manager/child.go b/nsjail_manager/child.go index 78f49a7..824816f 100644 --- a/nsjail_manager/child.go +++ b/nsjail_manager/child.go @@ -47,7 +47,7 @@ func waitForInterface(interfaceName string, timeout time.Duration) error { return nil } -func RunChild(logger *slog.Logger, args []string) error { +func RunChild(logger *slog.Logger, targetCMD []string) error { logger.Info("boundary CHILD process is started") vethNetJail := os.Getenv("VETH_JAIL_NAME") @@ -75,8 +75,8 @@ func RunChild(logger *slog.Logger, args []string) error { } // Program to run - bin := args[0] - args = args[1:] + bin := targetCMD[0] + args := targetCMD[1:] cmd := exec.Command(bin, args...) cmd.Stdin = os.Stdin diff --git a/nsjail_manager/run.go b/nsjail_manager/run.go index dbce8f5..e38e431 100644 --- a/nsjail_manager/run.go +++ b/nsjail_manager/run.go @@ -16,9 +16,9 @@ func isChild() bool { // If running as a child (CHILD env var is set), it sets up networking in the namespace // and executes the target command. Otherwise, it runs as the parent process, setting up the jail, // proxy server, and managing the child process lifecycle. -func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig, args []string) error { +func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig) error { if isChild() { - return RunChild(logger, args) + return RunChild(logger, config.TargetCMD) } return RunParent(ctx, logger, config) diff --git a/run/run.go b/run/run.go index c075d0b..e51cce9 100644 --- a/run/run.go +++ b/run/run.go @@ -1,8 +1,13 @@ package run -import "context" +import ( + "context" + "log/slog" -func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig, args []string) error { + "github.com/coder/boundary/config" +) + +func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig) error { //if isChild() { // return RunChild(logger, args) //} From 3552e4e7c9990b140e68db8de8c11d03b6115373 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 12 Dec 2025 21:10:24 +0000 Subject: [PATCH 03/10] fix ci --- run/run.go | 1 + 1 file changed, 1 insertion(+) diff --git a/run/run.go b/run/run.go index e51cce9..38efeb3 100644 --- a/run/run.go +++ b/run/run.go @@ -13,4 +13,5 @@ func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig) erro //} // //return RunParent(ctx, logger, args, config) + return nil } From bee5b3d684484161550f634c30bcf43e7d200bc3 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 12 Dec 2025 21:19:05 +0000 Subject: [PATCH 04/10] add boolean flag --- cli/cli.go | 7 +++++++ config/config.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/cli/cli.go b/cli/cli.go index a1c78cf..1decaa7 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -119,6 +119,13 @@ func BaseCommand() *serpent.Command { Value: &cliConfig.ConfigureDNSForLocalStubResolver, YAML: "configure_dns_for_local_stub_resolver", }, + { + Flag: "landjail", + Env: "BOUNDARY_LANDJAIL", + Description: "Use landjail instead of nsjail for network isolation.", + Value: &cliConfig.Landjail, + YAML: "landjail", + }, }, Handler: func(inv *serpent.Invocation) error { appConfig := config.NewAppConfigFromCliConfig(cliConfig, inv.Args) diff --git a/config/config.go b/config/config.go index 9801d8d..325ac23 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type CliConfig struct { PprofEnabled serpent.Bool `yaml:"pprof_enabled"` PprofPort serpent.Int64 `yaml:"pprof_port"` ConfigureDNSForLocalStubResolver serpent.Bool `yaml:"configure_dns_for_local_stub_resolver"` + Landjail serpent.Bool `yaml:"landjail"` } type AppConfig struct { @@ -24,6 +25,7 @@ type AppConfig struct { PprofEnabled bool PprofPort int64 ConfigureDNSForLocalStubResolver bool + Landjail bool TargetCMD []string } @@ -43,6 +45,7 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) AppConfig { PprofEnabled: cfg.PprofEnabled.Value(), PprofPort: cfg.PprofPort.Value(), ConfigureDNSForLocalStubResolver: cfg.ConfigureDNSForLocalStubResolver.Value(), + Landjail: cfg.Landjail.Value(), TargetCMD: targetCMD, } } From f2cb89d972e25f4e413a2a827db798a1f1503bde Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 12 Dec 2025 21:52:41 +0000 Subject: [PATCH 05/10] minor chagnes --- cli/cli.go | 11 ++++++----- config/config.go | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 1decaa7..5f0e8a1 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -120,11 +120,12 @@ func BaseCommand() *serpent.Command { YAML: "configure_dns_for_local_stub_resolver", }, { - Flag: "landjail", - Env: "BOUNDARY_LANDJAIL", - Description: "Use landjail instead of nsjail for network isolation.", - Value: &cliConfig.Landjail, - YAML: "landjail", + Flag: "jail-type", + Env: "BOUNDARY_JAIL_TYPE", + Description: "Jail type to use for network isolation. Options: nsjail (default), landjail.", + Default: "nsjail", + Value: &cliConfig.JailType, + YAML: "jail_type", }, }, Handler: func(inv *serpent.Invocation) error { diff --git a/config/config.go b/config/config.go index 325ac23..5b0d1b7 100644 --- a/config/config.go +++ b/config/config.go @@ -14,7 +14,7 @@ type CliConfig struct { PprofEnabled serpent.Bool `yaml:"pprof_enabled"` PprofPort serpent.Int64 `yaml:"pprof_port"` ConfigureDNSForLocalStubResolver serpent.Bool `yaml:"configure_dns_for_local_stub_resolver"` - Landjail serpent.Bool `yaml:"landjail"` + JailType serpent.String `yaml:"jail_type"` } type AppConfig struct { @@ -25,7 +25,7 @@ type AppConfig struct { PprofEnabled bool PprofPort int64 ConfigureDNSForLocalStubResolver bool - Landjail bool + JailType string TargetCMD []string } @@ -45,7 +45,7 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) AppConfig { PprofEnabled: cfg.PprofEnabled.Value(), PprofPort: cfg.PprofPort.Value(), ConfigureDNSForLocalStubResolver: cfg.ConfigureDNSForLocalStubResolver.Value(), - Landjail: cfg.Landjail.Value(), + JailType: cfg.JailType.Value(), TargetCMD: targetCMD, } } From a8e16667d3833e7351b043653dd0699b40268bbc Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 12 Dec 2025 22:10:26 +0000 Subject: [PATCH 06/10] minor changes --- cli/cli.go | 6 +++++- config/config.go | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 5f0e8a1..1b2af73 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -129,7 +129,11 @@ func BaseCommand() *serpent.Command { }, }, Handler: func(inv *serpent.Invocation) error { - appConfig := config.NewAppConfigFromCliConfig(cliConfig, inv.Args) + appConfig, err := config.NewAppConfigFromCliConfig(cliConfig, inv.Args) + if err != nil { + return fmt.Errorf("failed to parse cli config file: %v", err) + } + // Get command arguments if len(appConfig.TargetCMD) == 0 { return fmt.Errorf("no command specified") diff --git a/config/config.go b/config/config.go index 5b0d1b7..538137e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,9 +1,30 @@ package config import ( + "fmt" + "github.com/coder/serpent" ) +// JailType represents the type of jail to use for network isolation +type JailType string + +const ( + NSJailType JailType = "nsjail" + LandjailType JailType = "landjail" +) + +func NewJailTypeFromString(str string) (JailType, error) { + switch str { + case "nsjail": + return NSJailType, nil + case "landjail": + return LandjailType, nil + default: + return NSJailType, fmt.Errorf("invalid JailType: %s", str) + } +} + type CliConfig struct { Config serpent.YAMLConfigPath `yaml:"-"` AllowListStrings serpent.StringArray `yaml:"allowlist"` // From config file @@ -25,11 +46,11 @@ type AppConfig struct { PprofEnabled bool PprofPort int64 ConfigureDNSForLocalStubResolver bool - JailType string + JailType JailType TargetCMD []string } -func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) AppConfig { +func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) (AppConfig, error) { // Merge allowlist from config file with allow from CLI flags allowListStrings := cfg.AllowListStrings.Value() allowStrings := cfg.AllowStrings.Value() @@ -37,6 +58,11 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) AppConfig { // Combine allowlist (config file) with allow (CLI flags) allAllowStrings := append(allowListStrings, allowStrings...) + jailType, err := NewJailTypeFromString(cfg.JailType.Value()) + if err != nil { + return AppConfig{}, err + } + return AppConfig{ AllowRules: allAllowStrings, LogLevel: cfg.LogLevel.Value(), @@ -45,7 +71,7 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) AppConfig { PprofEnabled: cfg.PprofEnabled.Value(), PprofPort: cfg.PprofPort.Value(), ConfigureDNSForLocalStubResolver: cfg.ConfigureDNSForLocalStubResolver.Value(), - JailType: cfg.JailType.Value(), + JailType: jailType, TargetCMD: targetCMD, - } + }, nil } From e8abce640b3a10a15a2942ea4c3c38e8d0d871a7 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 13 Dec 2025 17:50:54 +0000 Subject: [PATCH 07/10] minor changes --- cli/cli.go | 4 ++-- run/run.go | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 1b2af73..1777e60 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -8,7 +8,7 @@ import ( "github.com/coder/boundary/config" "github.com/coder/boundary/log" - "github.com/coder/boundary/nsjail_manager" + "github.com/coder/boundary/run" "github.com/coder/serpent" ) @@ -150,7 +150,7 @@ func BaseCommand() *serpent.Command { } logger.Debug("Application config", "config", appConfigInJSON) - return nsjail_manager.Run(inv.Context(), logger, appConfig) + return run.Run(inv.Context(), logger, appConfig) }, } } diff --git a/run/run.go b/run/run.go index 38efeb3..2474a43 100644 --- a/run/run.go +++ b/run/run.go @@ -2,16 +2,21 @@ package run import ( "context" + "fmt" "log/slog" "github.com/coder/boundary/config" + "github.com/coder/boundary/nsjail_manager" ) -func Run(ctx context.Context, logger *slog.Logger, config config.AppConfig) error { - //if isChild() { - // return RunChild(logger, args) - //} - // - //return RunParent(ctx, logger, args, config) - return nil +func Run(ctx context.Context, logger *slog.Logger, cfg config.AppConfig) error { + switch cfg.JailType { + case config.NSJailType: + return nsjail_manager.Run(ctx, logger, cfg) + case config.LandjailType: + //return landjail.Run() + return nil + default: + return fmt.Errorf("unknown jail type: %s", cfg.JailType) + } } From 4a54f36a1c813eee2d5ea602bd7e6d773fd75206 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 13 Dec 2025 18:11:30 +0000 Subject: [PATCH 08/10] add jailType option --- e2e_tests/boundary_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/e2e_tests/boundary_test.go b/e2e_tests/boundary_test.go index 4f6236d..78f866c 100644 --- a/e2e_tests/boundary_test.go +++ b/e2e_tests/boundary_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/coder/boundary/config" "github.com/coder/boundary/util" "github.com/stretchr/testify/require" ) @@ -23,6 +24,7 @@ type BoundaryTest struct { binaryPath string allowedDomains []string logLevel string + jailType config.JailType cmd *exec.Cmd pid int startupDelay time.Duration @@ -42,6 +44,7 @@ func NewBoundaryTest(t *testing.T, opts ...BoundaryTestOption) *BoundaryTest { binaryPath: binaryPath, allowedDomains: []string{}, logLevel: "warn", + jailType: config.NSJailType, startupDelay: 2 * time.Second, } @@ -81,6 +84,13 @@ func WithStartupDelay(delay time.Duration) BoundaryTestOption { } } +// WithJailType sets the jail type (nsjail or landjail) +func WithJailType(jailType config.JailType) BoundaryTestOption { + return func(bt *BoundaryTest) { + bt.jailType = jailType + } +} + // Build builds the boundary binary func (bt *BoundaryTest) Build() *BoundaryTest { buildCmd := exec.Command("go", "build", "-o", bt.binaryPath, "./cmd/...") @@ -101,6 +111,9 @@ func (bt *BoundaryTest) Start(command ...string) *BoundaryTest { args := []string{ "--log-level", bt.logLevel, } + if bt.jailType != "" { + args = append(args, "--jail-type", string(bt.jailType)) + } for _, domain := range bt.allowedDomains { args = append(args, "--allow", domain) } From ff6dce246538e679279b4eafa2a8b8789944ca77 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 13 Dec 2025 19:18:10 +0000 Subject: [PATCH 09/10] adding landjail test (IN_PROGRESS) --- e2e_tests/boundary_test.go | 21 ++++++++ e2e_tests/landjail_test.go | 50 +++++++++++++++++++ .../{e2e_boundary_test.go => ns_jail_test.go} | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 e2e_tests/landjail_test.go rename e2e_tests/{e2e_boundary_test.go => ns_jail_test.go} (96%) diff --git a/e2e_tests/boundary_test.go b/e2e_tests/boundary_test.go index 78f866c..8251bc0 100644 --- a/e2e_tests/boundary_test.go +++ b/e2e_tests/boundary_test.go @@ -215,6 +215,27 @@ func (bt *BoundaryTest) makeRequest(url string) []byte { return output } +func (bt *BoundaryTest) ExpectDenyContains(url string, containsText string) {} + +func (bt *BoundaryTest) getNsCurlCmd(url string) *exec.Cmd { + pid := fmt.Sprintf("%v", bt.pid) + _, _, _, _, configDir := util.GetUserInfo() + certPath := fmt.Sprintf("%v/ca-cert.pem", configDir) + + args := []string{"nsenter", "-t", pid, "-n", "--", + "env", fmt.Sprintf("SSL_CERT_FILE=%v", certPath), "curl", "-sS", url} + curlCmd := exec.Command("sudo", args...) + + return curlCmd +} + +func (bt *BoundaryTest) getHostCurlCmd(url string) *exec.Cmd { + args := []string{"-sS", url} + curlCmd := exec.Command("curl", args...) + + return curlCmd +} + // getTargetProcessPID gets the PID of the boundary target process. // Target process is associated with a network namespace, so you can exec into it, using this PID. // pgrep -f boundary-test -n is doing two things: diff --git a/e2e_tests/landjail_test.go b/e2e_tests/landjail_test.go new file mode 100644 index 0000000..195a7b9 --- /dev/null +++ b/e2e_tests/landjail_test.go @@ -0,0 +1,50 @@ +package e2e_tests + +import ( + "testing" + + "github.com/coder/boundary/config" +) + +func TestLandJail(t *testing.T) { + // Create and configure boundary test + bt := NewBoundaryTest(t, + WithAllowedDomain("dev.coder.com"), + WithAllowedDomain("jsonplaceholder.typicode.com"), + WithLogLevel("debug"), + WithJailType(config.LandjailType), + ). + Build(). + Start() + + // Ensure cleanup + defer bt.Stop() + + // Test allowed HTTP request + t.Run("HTTPRequestThroughBoundary", func(t *testing.T) { + expectedResponse := `{ + "userId": 1, + "id": 1, + "title": "delectus aut autem", + "completed": false +}` + bt.ExpectAllowed("http://jsonplaceholder.typicode.com/todos/1", expectedResponse) + }) + + // Test allowed HTTPS request + t.Run("HTTPSRequestThroughBoundary", func(t *testing.T) { + expectedResponse := `{"message":"👋"} +` + bt.ExpectAllowed("https://dev.coder.com/api/v2", expectedResponse) + }) + + // Test blocked HTTP request + //t.Run("HTTPBlockedDomainTest", func(t *testing.T) { + // bt.ExpectDeny("http://example.com") + //}) + // + // // Test blocked HTTPS request + // t.Run("HTTPSBlockedDomainTest", func(t *testing.T) { + // bt.ExpectDeny("https://example.com") + // }) +} diff --git a/e2e_tests/e2e_boundary_test.go b/e2e_tests/ns_jail_test.go similarity index 96% rename from e2e_tests/e2e_boundary_test.go rename to e2e_tests/ns_jail_test.go index 7d2e6ac..1fabff6 100644 --- a/e2e_tests/e2e_boundary_test.go +++ b/e2e_tests/ns_jail_test.go @@ -2,7 +2,7 @@ package e2e_tests import "testing" -func TestE2EBoundary(t *testing.T) { +func TestNamespaceJail(t *testing.T) { // Create and configure boundary test bt := NewBoundaryTest(t, WithAllowedDomain("dev.coder.com"), From 522c593dff0cee56d5afe341ada409bcabf9589a Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 13 Dec 2025 21:09:48 +0000 Subject: [PATCH 10/10] seems working a bit --- e2e_tests/boundary_test.go | 23 ++- e2e_tests/landjail_test.go | 282 ++++++++++++++++++++++++++++++++++--- run/run.go | 5 +- 3 files changed, 276 insertions(+), 34 deletions(-) diff --git a/e2e_tests/boundary_test.go b/e2e_tests/boundary_test.go index 8251bc0..3fc26c5 100644 --- a/e2e_tests/boundary_test.go +++ b/e2e_tests/boundary_test.go @@ -12,19 +12,17 @@ import ( "testing" "time" - "github.com/coder/boundary/config" "github.com/coder/boundary/util" "github.com/stretchr/testify/require" ) -// BoundaryTest is a high-level test framework for boundary e2e tests +// BoundaryTest is a high-level test framework for boundary e2e tests using nsjail type BoundaryTest struct { t *testing.T projectRoot string binaryPath string allowedDomains []string logLevel string - jailType config.JailType cmd *exec.Cmd pid int startupDelay time.Duration @@ -44,7 +42,6 @@ func NewBoundaryTest(t *testing.T, opts ...BoundaryTestOption) *BoundaryTest { binaryPath: binaryPath, allowedDomains: []string{}, logLevel: "warn", - jailType: config.NSJailType, startupDelay: 2 * time.Second, } @@ -84,13 +81,6 @@ func WithStartupDelay(delay time.Duration) BoundaryTestOption { } } -// WithJailType sets the jail type (nsjail or landjail) -func WithJailType(jailType config.JailType) BoundaryTestOption { - return func(bt *BoundaryTest) { - bt.jailType = jailType - } -} - // Build builds the boundary binary func (bt *BoundaryTest) Build() *BoundaryTest { buildCmd := exec.Command("go", "build", "-o", bt.binaryPath, "./cmd/...") @@ -110,9 +100,7 @@ func (bt *BoundaryTest) Start(command ...string) *BoundaryTest { // Build command args args := []string{ "--log-level", bt.logLevel, - } - if bt.jailType != "" { - args = append(args, "--jail-type", string(bt.jailType)) + "--jail-type", "nsjail", } for _, domain := range bt.allowedDomains { args = append(args, "--allow", domain) @@ -215,7 +203,12 @@ func (bt *BoundaryTest) makeRequest(url string) []byte { return output } -func (bt *BoundaryTest) ExpectDenyContains(url string, containsText string) {} +// ExpectDenyContains makes an HTTP/HTTPS request and expects it to be denied, checking that response contains the given text +func (bt *BoundaryTest) ExpectDenyContains(url string, containsText string) { + bt.t.Helper() + output := bt.makeRequest(url) + require.Contains(bt.t, string(output), containsText, "Response does not contain expected denial text") +} func (bt *BoundaryTest) getNsCurlCmd(url string) *exec.Cmd { pid := fmt.Sprintf("%v", bt.pid) diff --git a/e2e_tests/landjail_test.go b/e2e_tests/landjail_test.go index 195a7b9..bcf6fae 100644 --- a/e2e_tests/landjail_test.go +++ b/e2e_tests/landjail_test.go @@ -1,24 +1,272 @@ package e2e_tests import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" "testing" + "time" - "github.com/coder/boundary/config" + "github.com/coder/boundary/util" + "github.com/stretchr/testify/require" ) -func TestLandJail(t *testing.T) { - // Create and configure boundary test - bt := NewBoundaryTest(t, - WithAllowedDomain("dev.coder.com"), - WithAllowedDomain("jsonplaceholder.typicode.com"), - WithLogLevel("debug"), - WithJailType(config.LandjailType), +// LandjailTest is a high-level test framework for boundary e2e tests using landjail +type LandjailTest struct { + t *testing.T + projectRoot string + binaryPath string + allowedDomains []string + logLevel string + cmd *exec.Cmd + startupDelay time.Duration + // Pipes to communicate with the bash process + bashStdin io.WriteCloser + bashStdout io.ReadCloser + bashStderr io.ReadCloser +} + +// LandjailTestOption is a function that configures LandjailTest +type LandjailTestOption func(*LandjailTest) + +// NewLandjailTest creates a new LandjailTest instance +func NewLandjailTest(t *testing.T, opts ...LandjailTestOption) *LandjailTest { + projectRoot := findProjectRoot(t) + binaryPath := "/tmp/boundary-landjail-test" + + lt := &LandjailTest{ + t: t, + projectRoot: projectRoot, + binaryPath: binaryPath, + allowedDomains: []string{}, + logLevel: "warn", + startupDelay: 2 * time.Second, + } + + // Apply options + for _, opt := range opts { + opt(lt) + } + + return lt +} + +// WithAllowedDomain adds an allowed domain rule +func WithLandjailAllowedDomain(domain string) LandjailTestOption { + return func(lt *LandjailTest) { + lt.allowedDomains = append(lt.allowedDomains, fmt.Sprintf("domain=%s", domain)) + } +} + +// WithAllowedRule adds a full allow rule (e.g., "method=GET domain=example.com path=/api/*") +func WithLandjailAllowedRule(rule string) LandjailTestOption { + return func(lt *LandjailTest) { + lt.allowedDomains = append(lt.allowedDomains, rule) + } +} + +// WithLogLevel sets the log level +func WithLandjailLogLevel(level string) LandjailTestOption { + return func(lt *LandjailTest) { + lt.logLevel = level + } +} + +// WithStartupDelay sets how long to wait after starting boundary before making requests +func WithLandjailStartupDelay(delay time.Duration) LandjailTestOption { + return func(lt *LandjailTest) { + lt.startupDelay = delay + } +} + +// Build builds the boundary binary +func (lt *LandjailTest) Build() *LandjailTest { + buildCmd := exec.Command("go", "build", "-o", lt.binaryPath, "./cmd/...") + buildCmd.Dir = lt.projectRoot + err := buildCmd.Run() + require.NoError(lt.t, err, "Failed to build boundary binary") + return lt +} + +// Start starts the boundary process with a bash process that reads commands from stdin +func (lt *LandjailTest) Start(command ...string) *LandjailTest { + // Build command args + args := []string{ + "--log-level", lt.logLevel, + "--jail-type", "landjail", + } + for _, domain := range lt.allowedDomains { + args = append(args, "--allow", domain) + } + args = append(args, "--") + + // Bash command that reads and executes commands from stdin + // Each command should end with a newline, and we use a marker to detect completion + // Using a unique marker to avoid conflicts with command output + if len(command) == 0 { + command = []string{"/bin/bash", "-c", "while IFS= read -r cmd; do if [ \"$cmd\" = \"exit\" ]; then exit 0; fi; eval \"$cmd\"; echo \"__BOUNDARY_CMD_DONE__\"; done"} + } + args = append(args, command...) + + lt.cmd = exec.Command(lt.binaryPath, args...) + + // Capture pipes for communication with bash + var err error + lt.bashStdin, err = lt.cmd.StdinPipe() + require.NoError(lt.t, err, "Failed to create stdin pipe for landjail") + + lt.bashStdout, err = lt.cmd.StdoutPipe() + require.NoError(lt.t, err, "Failed to create stdout pipe for landjail") + + lt.bashStderr, err = lt.cmd.StderrPipe() + require.NoError(lt.t, err, "Failed to create stderr pipe for landjail") + + // Forward stderr to os.Stderr for debugging + go io.Copy(os.Stderr, lt.bashStderr) //nolint:errcheck + + err = lt.cmd.Start() + require.NoError(lt.t, err, "Failed to start boundary process with landjail") + + // Wait for boundary to start + time.Sleep(lt.startupDelay) + + return lt +} + +// Stop gracefully stops the boundary process +func (lt *LandjailTest) Stop() { + if lt.cmd == nil || lt.cmd.Process == nil { + return + } + + // Send "exit" command to bash, then close stdin + if lt.bashStdin != nil { + _, _ = lt.bashStdin.Write([]byte("exit\n")) + lt.bashStdin.Close() + } + + time.Sleep(1 * time.Second) + + // Wait for process to finish + if lt.cmd != nil { + err := lt.cmd.Wait() + if err != nil { + lt.t.Logf("Boundary process finished with error: %v", err) + } + } + + // Close pipes if they're still open + if lt.bashStdout != nil { + lt.bashStdout.Close() + } + if lt.bashStderr != nil { + lt.bashStderr.Close() + } + + // Clean up binary + err := os.Remove(lt.binaryPath) + if err != nil { + lt.t.Logf("Failed to remove boundary binary: %v", err) + } +} + +// ExpectAllowed makes an HTTP/HTTPS request and expects it to be allowed with the given response body +func (lt *LandjailTest) ExpectAllowed(url string, expectedBody string) { + lt.t.Helper() + output := lt.makeRequest(url) + require.Equal(lt.t, expectedBody, string(output), "Expected response body does not match") +} + +// ExpectAllowedContains makes an HTTP/HTTPS request and expects it to be allowed, checking that response contains the given text +func (lt *LandjailTest) ExpectAllowedContains(url string, containsText string) { + lt.t.Helper() + output := lt.makeRequest(url) + require.Contains(lt.t, string(output), containsText, "Response does not contain expected text") +} + +// ExpectDeny makes an HTTP/HTTPS request and expects it to be denied +func (lt *LandjailTest) ExpectDeny(url string) { + lt.t.Helper() + output := lt.makeRequest(url) + require.Contains(lt.t, string(output), "Request Blocked by Boundary", "Expected request to be blocked") +} + +// ExpectDenyContains makes an HTTP/HTTPS request and expects it to be denied, checking that response contains the given text +func (lt *LandjailTest) ExpectDenyContains(url string, containsText string) { + lt.t.Helper() + output := lt.makeRequest(url) + require.Contains(lt.t, string(output), containsText, "Response does not contain expected denial text") +} + +// makeRequest executes a curl command in the landjail bash process +// Always sets SSL_CERT_FILE for HTTPS support (harmless for HTTP requests) +func (lt *LandjailTest) makeRequest(url string) []byte { + lt.t.Helper() + + if lt.bashStdin == nil || lt.bashStdout == nil { + lt.t.Fatalf("landjail pipes not initialized") + } + + _, _, _, _, configDir := util.GetUserInfo() + certPath := fmt.Sprintf("%v/ca-cert.pem", configDir) + + // Build curl command with SSL_CERT_FILE + curlCmd := fmt.Sprintf("env SSL_CERT_FILE=%s curl -sS %s\n", certPath, url) + + // Write command to stdin + _, err := lt.bashStdin.Write([]byte(curlCmd)) + require.NoError(lt.t, err, "Failed to write command to landjail stdin") + + // Read output until we see the completion marker + var output bytes.Buffer + doneMarker := []byte("__BOUNDARY_CMD_DONE__") + buf := make([]byte, 4096) + + for { + n, err := lt.bashStdout.Read(buf) + if n > 0 { + // Check if we've received the completion marker + data := buf[:n] + if idx := bytes.Index(data, doneMarker); idx != -1 { + // Found the marker, add everything before it to output + output.Write(data[:idx]) + // Remove the marker and newline + remaining := data[idx+len(doneMarker):] + if len(remaining) > 0 && remaining[0] == '\n' { + remaining = remaining[1:] + } + if len(remaining) > 0 { + output.Write(remaining) + } + break + } + output.Write(data) + } + if err == io.EOF { + break + } + if err != nil { + lt.t.Fatalf("Failed to read from landjail stdout: %v", err) + } + } + + return output.Bytes() +} + +func TestLandjail(t *testing.T) { + // Create and configure landjail test + lt := NewLandjailTest(t, + WithLandjailAllowedDomain("dev.coder.com"), + WithLandjailAllowedDomain("jsonplaceholder.typicode.com"), + WithLandjailLogLevel("debug"), ). Build(). Start() // Ensure cleanup - defer bt.Stop() + defer lt.Stop() // Test allowed HTTP request t.Run("HTTPRequestThroughBoundary", func(t *testing.T) { @@ -28,23 +276,23 @@ func TestLandJail(t *testing.T) { "title": "delectus aut autem", "completed": false }` - bt.ExpectAllowed("http://jsonplaceholder.typicode.com/todos/1", expectedResponse) + lt.ExpectAllowed("http://jsonplaceholder.typicode.com/todos/1", expectedResponse) }) // Test allowed HTTPS request t.Run("HTTPSRequestThroughBoundary", func(t *testing.T) { expectedResponse := `{"message":"👋"} ` - bt.ExpectAllowed("https://dev.coder.com/api/v2", expectedResponse) + lt.ExpectAllowed("https://dev.coder.com/api/v2", expectedResponse) }) - // Test blocked HTTP request + //// Test blocked HTTP request //t.Run("HTTPBlockedDomainTest", func(t *testing.T) { - // bt.ExpectDeny("http://example.com") + // lt.ExpectDeny("http://example.com") //}) // - // // Test blocked HTTPS request - // t.Run("HTTPSBlockedDomainTest", func(t *testing.T) { - // bt.ExpectDeny("https://example.com") - // }) + //// Test blocked HTTPS request + //t.Run("HTTPSBlockedDomainTest", func(t *testing.T) { + // lt.ExpectDeny("https://example.com") + //}) } diff --git a/run/run.go b/run/run.go index 2474a43..7777bb8 100644 --- a/run/run.go +++ b/run/run.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "log/slog" + "os" "github.com/coder/boundary/config" + "github.com/coder/boundary/landjail" "github.com/coder/boundary/nsjail_manager" ) @@ -14,8 +16,7 @@ func Run(ctx context.Context, logger *slog.Logger, cfg config.AppConfig) error { case config.NSJailType: return nsjail_manager.Run(ctx, logger, cfg) case config.LandjailType: - //return landjail.Run() - return nil + return landjail.Run(cfg.TargetCMD, os.Environ()) default: return fmt.Errorf("unknown jail type: %s", cfg.JailType) }