//go:build darwin package platform import ( "context" "fmt" "net" "os" "os/exec" "testing" "time" "github.com/safedep/dry/utils" "github.com/safedep/pmg/sandbox" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( lockdownHelperEnv = "PMG_LOCKDOWN_HELPER" lockdownSandboxExecPath = "/usr/bin/sandbox-exec" ) // TestLockdownHelperProcess is not a test: it is the child process re-executed // under sandbox-exec by the lockdown integration test below. It performs the // network checks described by its environment and exits 0 only when every // check agrees with its expectation. func TestLockdownHelperProcess(t *testing.T) { if os.Getenv(lockdownHelperEnv) != "1" { t.Skip("helper process only") } failed := false report := func(name string, ok bool, err error) { fmt.Printf("check %s: ok=%v err=%v\n", name, ok, err) if !ok { failed = true } } if addr := os.Getenv("PMG_DIAL_MUST_PASS"); addr != "" { conn, err := net.DialTimeout("tcp", addr, 2*time.Second) report("dial-must-pass "+addr, err == nil, err) if conn != nil { _ = conn.Close() } } if addr := os.Getenv("PMG_DIAL_MUST_FAIL"); addr != "" { conn, err := net.DialTimeout("tcp", addr, 2*time.Second) report("dial-must-fail "+addr, err != nil, err) if conn != nil { _ = conn.Close() } } if host := os.Getenv("PMG_RESOLVE"); host != "" { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() _, err := net.DefaultResolver.LookupHost(ctx, host) wantPass := os.Getenv("PMG_RESOLVE_MUST_PASS") == "1" report(fmt.Sprintf("resolve %s (wantPass=%v)", host, wantPass), (err == nil) == wantPass, err) // Informational probes to localize where resolution is blocked; they // do not affect the pass/fail outcome. goResolver := &net.Resolver{PreferGo: true} _, goErr := goResolver.LookupHost(ctx, host) fmt.Printf("probe prefergo-resolve %s: err=%v\n", host, goErr) conn, dialErr := net.DialTimeout("udp", "1.1.1.1:53", 2*time.Second) if dialErr != nil { fmt.Printf("probe udp53-dial: err=%v\n", dialErr) } else { _, writeErr := conn.Write([]byte{0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x01, 0x00, 0x01}) _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) buf := make([]byte, 512) _, readErr := conn.Read(buf) fmt.Printf("probe udp53 write err=%v read err=%v\n", writeErr, readErr) _ = conn.Close() } } if os.Getenv("PMG_SELF_DIAL") == "1" { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { report("self-dial listen", false, err) } else { go func() { conn, aerr := ln.Accept() if aerr == nil { _ = conn.Close() } }() conn, derr := net.DialTimeout("tcp", ln.Addr().String(), 2*time.Second) report("self-dial "+ln.Addr().String(), derr == nil, derr) if conn != nil { _ = conn.Close() } _ = ln.Close() } } if failed { t.Fatal("one or more sandboxed checks disagreed with expectations") } } func requireSandboxExec(t *testing.T) { t.Helper() if _, err := os.Stat(lockdownSandboxExecPath); err != nil { // Skips must not hide security tests in CI (see ci.yml). if os.Getenv("CI") != "" { t.Fatal("sandbox-exec required in CI") } t.Skip("sandbox-exec not available") } } func mustListenLoopback(t *testing.T) net.Listener { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) t.Cleanup(func() { _ = ln.Close() }) go func() { for { conn, err := ln.Accept() if err != nil { return } _ = conn.Close() } }() return ln } // lockdownTestPolicy mirrors the shipped profiles' filesystem posture // (broad read like go.yml, temp-only writes): filesystem breadth is not // under test here, network confinement is. func lockdownTestPolicy(t *testing.T) *sandbox.SandboxPolicy { t.Helper() return &sandbox.SandboxPolicy{ Name: "lockdown-e2e", PackageManagers: []string{"npm"}, Filesystem: sandbox.FilesystemPolicy{ AllowRead: []string{"/"}, AllowWrite: []string{ os.TempDir() + "/**", "/tmp/**", "/private/tmp/**", "/dev/null", }, }, NetworkViaProxyOnly: utils.PtrTo(true), } } func writeTempProfile(t *testing.T, policy *sandbox.SandboxPolicy, proxyAddr string) string { t.Helper() profile, err := newSeatbeltPolicyTranslator().translate(policy, &sandbox.ExecutionContext{ProxyAddr: proxyAddr}) require.NoError(t, err) f, err := os.CreateTemp("", "pmg-lockdown-e2e-*.sb") require.NoError(t, err) t.Cleanup(func() { _ = os.Remove(f.Name()) }) _, err = f.WriteString(profile) require.NoError(t, err) require.NoError(t, f.Close()) return f.Name() } func runSandboxedHelper(t *testing.T, profilePath string, env map[string]string) error { t.Helper() cmd := exec.Command(lockdownSandboxExecPath, "-f", profilePath, os.Args[0], "-test.run", "^TestLockdownHelperProcess$", "-test.v") cmd.Env = append(os.Environ(), lockdownHelperEnv+"=1") for k, v := range env { cmd.Env = append(cmd.Env, k+"="+v) } out, err := cmd.CombinedOutput() t.Logf("sandboxed helper output:\n%s", out) return err } func TestLockdownEnforcementDarwin(t *testing.T) { requireSandboxExec(t) proxyLn := mustListenLoopback(t) otherLn := mustListenLoopback(t) t.Run("base lockdown", func(t *testing.T) { profile := writeTempProfile(t, lockdownTestPolicy(t), proxyLn.Addr().String()) err := runSandboxedHelper(t, profile, map[string]string{ "PMG_DIAL_MUST_PASS": proxyLn.Addr().String(), "PMG_DIAL_MUST_FAIL": otherLn.Addr().String(), "PMG_RESOLVE": "example.com", }) assert.NoError(t, err, "proxy dial must pass, direct dial and DNS must fail") }) t.Run("allow_direct_dns re-opens DNS", func(t *testing.T) { policy := lockdownTestPolicy(t) policy.AllowDirectDNS = utils.PtrTo(true) profile := writeTempProfile(t, policy, proxyLn.Addr().String()) err := runSandboxedHelper(t, profile, map[string]string{ "PMG_DIAL_MUST_PASS": proxyLn.Addr().String(), "PMG_RESOLVE": "example.com", "PMG_RESOLVE_MUST_PASS": "1", }) assert.NoError(t, err, "DNS must pass with allow_direct_dns") }) t.Run("allow_direct_dns with network bind", func(t *testing.T) { policy := lockdownTestPolicy(t) policy.AllowDirectDNS = utils.PtrTo(true) policy.AllowNetworkBind = utils.PtrTo(true) profile := writeTempProfile(t, policy, proxyLn.Addr().String()) err := runSandboxedHelper(t, profile, map[string]string{ "PMG_RESOLVE": "example.com", "PMG_RESOLVE_MUST_PASS": "1", }) assert.NoError(t, err, "DNS with allow_direct_dns and allow_network_bind") }) // Gating check for the milestone: allow_network_bind's // (allow network* (local ip "localhost:*")) rules are emitted after the // lockdown deny and are expected to keep loopback->loopback connects // working. If this sub-test fails, the committed fallback is to emit // (allow network-outbound (remote ip "localhost:*")) under lockdown when // AllowNetworkBind is set. t.Run("allow_network_bind keeps loopback self-dial working", func(t *testing.T) { policy := lockdownTestPolicy(t) policy.AllowNetworkBind = utils.PtrTo(true) profile := writeTempProfile(t, policy, proxyLn.Addr().String()) err := runSandboxedHelper(t, profile, map[string]string{ "PMG_DIAL_MUST_PASS": proxyLn.Addr().String(), "PMG_SELF_DIAL": "1", }) assert.NoError(t, err, "loopback self-dial must pass under lockdown with allow_network_bind") }) }