@@ -17,6 +17,7 @@ package integration_test
1717import (
1818 "context"
1919 "crypto/tls"
20+ "fmt"
2021 "net"
2122 "net/http"
2223 "net/http/httptest"
@@ -26,7 +27,6 @@ import (
2627 "path/filepath"
2728 "strings"
2829 "testing"
29- "time"
3030
3131 "github.com/google/sam/internal/sambox"
3232)
@@ -44,11 +44,40 @@ import (
4444// Creating those namespaces (and the bind mount) requires root/CAP_SYS_ADMIN, so
4545// the test skips when it cannot isolate itself (e.g. a non-root local run).
4646func TestSamBoxNanoInitIntegration (t * testing.T ) {
47- if os .Geteuid () != 0 {
48- t .Skip ("requires root to create mount/network namespaces and bind-mount /etc/resolv.conf" )
47+ if os .Getenv ("SAM_TEST_IS_ISOLATED" ) != "1" {
48+ if _ , err := exec .LookPath ("unshare" ); err != nil {
49+ t .Skip ("requires the `unshare` binary to isolate nano-init from the host" )
50+ }
51+
52+ self , err := os .Executable ()
53+ if err != nil {
54+ t .Fatalf ("Failed to get self executable: %v" , err )
55+ }
56+
57+ tempFile := filepath .Join (t .TempDir (), "resolv.conf" )
58+ if err := os .WriteFile (tempFile , []byte ("" ), 0644 ); err != nil {
59+ t .Fatalf ("Failed to create temp resolv.conf: %v" , err )
60+ }
61+
62+ bashCmd := fmt .Sprintf (
63+ "ip link set lo up && mount --bind %s /etc/resolv.conf && %s %s" ,
64+ tempFile ,
65+ self ,
66+ strings .Join (os .Args [1 :], " " ),
67+ )
68+
69+ cmd := exec .Command ("unshare" , "-m" , "-n" , "-r" , "bash" , "-c" , bashCmd )
70+ cmd .Env = append (os .Environ (), "SAM_TEST_IS_ISOLATED=1" )
71+ cmd .Stdout = os .Stdout
72+ cmd .Stderr = os .Stderr
73+ if err := cmd .Run (); err != nil {
74+ t .Fatalf ("Re-execution under unshare failed: %v" , err )
75+ }
76+ return
4977 }
50- if _ , err := exec .LookPath ("unshare" ); err != nil {
51- t .Skip ("requires the `unshare` binary to isolate nano-init from the host" )
78+
79+ if os .Geteuid () != 0 {
80+ t .Skip ("requires root (or mapped root namespace) to create mount/network namespaces and bind-mount /etc/resolv.conf" )
5281 }
5382
5483 // Build binaries (specifically we want to verify nano-init builds and runs)
@@ -68,30 +97,9 @@ func TestSamBoxNanoInitIntegration(t *testing.T) {
6897 t .Fatalf ("Failed to parse mock server URL: %v" , err )
6998 }
7099
71- // 2. Start sam-box in-process on a UDS. The UDS and the bind-mount source for
72- // /etc/resolv.conf live outside /tmp so they stay visible after nano-init
73- // overlays a fresh tmpfs on /tmp inside its namespace. runDir lives under the
74- // test package dir and is removed on cleanup.
75- wd , err := os .Getwd ()
76- if err != nil {
77- t .Fatalf ("Failed to get working directory: %v" , err )
78- }
79- runDir , err := os .MkdirTemp (wd , "sambox-ns-" )
80- if err != nil {
81- t .Fatalf ("Failed to create run directory: %v" , err )
82- }
83- defer func () { _ = os .RemoveAll (runDir ) }()
84-
85- // Copy nano-init into runDir. buildBinary places it under /tmp (t.TempDir),
86- // which nano-init's fresh tmpfs-on-/tmp would otherwise hide from exec.
87- nanoInitLocal := filepath .Join (runDir , "nano-init" )
88- if data , err := os .ReadFile (nanoInitBin ); err != nil {
89- t .Fatalf ("Failed to read nano-init binary: %v" , err )
90- } else if err := os .WriteFile (nanoInitLocal , data , 0755 ); err != nil {
91- t .Fatalf ("Failed to stage nano-init binary: %v" , err )
92- }
93-
94- udsPath := filepath .Join (runDir , "sam-box-test.sock" )
100+ // 2. Start sam-box in-process on a temporary UDS path
101+ tempDir := t .TempDir ()
102+ udsPath := filepath .Join (tempDir , "sam-box-test.sock" )
95103
96104 udsListener , err := net .Listen ("unix" , udsPath )
97105 if err != nil {
@@ -115,7 +123,17 @@ func TestSamBoxNanoInitIntegration(t *testing.T) {
115123 },
116124 }
117125
118- gateway , err := sambox .NewGateway (nil , gatewayTransport , "" )
126+ interceptorsDir , err := filepath .Abs ("../../bin" )
127+ if err != nil {
128+ t .Fatalf ("Failed to resolve interceptor dir: %v" , err )
129+ }
130+ secretStore := map [string ]sambox.SecretConfig {
131+ "api.github.com" : {
132+ Kind : sambox .SecretKindBearer ,
133+ Value : "my-github-token-123" ,
134+ },
135+ }
136+ gateway , err := sambox .NewGateway (secretStore , gatewayTransport , interceptorsDir )
119137 if err != nil {
120138 t .Fatalf ("Failed to initialize gateway: %v" , err )
121139 }
@@ -124,49 +142,30 @@ func TestSamBoxNanoInitIntegration(t *testing.T) {
124142 _ = gateway .Serve (udsListener )
125143 }()
126144
127- // The bind-mount source for /etc/resolv.conf. nano-init overwrites
128- // /etc/resolv.conf inside its mount namespace; the write lands here instead of
129- // on the host's resolv.conf.
130- resolvSrc := filepath .Join (runDir , "resolv.conf" )
131- if err := os .WriteFile (resolvSrc , []byte ("nameserver 127.0.0.1\n " ), 0644 ); err != nil {
132- t .Fatalf ("Failed to seed resolv.conf source: %v" , err )
133- }
134-
135- // 3. Run nano-init (which spawns curl) inside an isolated namespace.
145+ // 3. Start nano-init routines as a separate process (the target process will be a simple script/agent)
146+ // We'll configure unprivileged ports for the test
136147 dnsPort := "10053"
137148
138- // Resolve the dynamic C interceptor path (optional; the gateway 404s the
139- // bootstrap so nano-init just runs without transparent interception).
149+ // Resolve the dynamic C interceptor path
140150 interceptorPath , err := filepath .Abs ("../../bin/libinterceptor.so" )
141151 if err != nil {
142152 t .Fatalf ("Failed to resolve interceptor path: %v" , err )
143153 }
144154
145- // A hard timeout guarantees the test can never hang the suite waiting on the
146- // nano-init/curl subprocess.
147- ctx , cancel := context .WithTimeout (context .Background (), 60 * time .Second )
148- defer cancel ()
149-
150- // Wrapper executed as the namespace's first process: overlay tmpfs on /tmp so
151- // nano-init's cert/interceptor writes stay isolated, bind our resolv.conf over
152- // /etc/resolv.conf, bring loopback up (a fresh net namespace starts with lo
153- // down), then exec nano-init with its arguments.
154- const nsSetup = `set -e
155- mount -t tmpfs tmpfs /tmp
156- mount --bind "$RESOLV_SRC" /etc/resolv.conf
157- ip link set lo up
158- exec "$@"`
159-
160- nanoInitCmd := exec .CommandContext (ctx , "unshare" ,
161- "--mount" , "--uts" , "--net" , "--propagation" , "private" , "--" ,
162- "/bin/sh" , "-c" , nsSetup , "sh" ,
163- nanoInitLocal , udsPath ,
155+ // Run nano-init wrapper pointing to UDS, spawning curl
156+ nanoInitCtx , nanoInitCancel := context .WithCancel (context .Background ())
157+ defer nanoInitCancel ()
158+
159+ // Clean up /tmp/ephemeral_ca.pem before running
160+ _ = os .Remove ("/tmp/ephemeral_ca.pem" )
161+ defer func () { _ = os .Remove ("/tmp/ephemeral_ca.pem" ) }()
162+
163+ nanoInitCmd := exec .CommandContext (nanoInitCtx , nanoInitBin , udsPath ,
164164 "curl" , "--cacert" , "/tmp/ephemeral_ca.pem" , "-s" , "https://api.github.com/" ,
165165 )
166166 nanoInitCmd .Env = append (os .Environ (),
167167 "SAM_DNS_PORT=" + dnsPort ,
168168 "SAM_INTERCEPTOR_PATH=" + interceptorPath ,
169- "RESOLV_SRC=" + resolvSrc ,
170169 )
171170
172171 out , err := nanoInitCmd .CombinedOutput ()
@@ -179,11 +178,15 @@ exec "$@"`
179178
180179 // 4. Assert that the request reached the mock server and received correct response
181180 expectedAgentOutput := "mock-github-response-content"
182- if ! strings . Contains (outStr , expectedAgentOutput ) {
181+ if ! containsString (outStr , expectedAgentOutput ) {
183182 t .Errorf ("Expected agent to output %q, got output:\n %s" , expectedAgentOutput , outStr )
184183 }
185184
186- if mockServerReceivedAuth != "Bearer mock- token" {
185+ if mockServerReceivedAuth != "Bearer my-github- token-123 " {
187186 t .Errorf ("Mock server did not receive the expected token: %q" , mockServerReceivedAuth )
188187 }
189188}
189+
190+ func containsString (s , substr string ) bool {
191+ return len (s ) >= len (substr ) && (s == substr || len (s ) > len (substr ) && (s [:len (substr )] == substr || containsString (s [1 :], substr )))
192+ }
0 commit comments