gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/root/portforward_test.go (about) 1 // Copyright 2023 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package portforward_test holds a docker test for port forward. It is separate 16 // from other root tests so that both hostinet and netstack can be tested in 17 // one Makefile target. 18 package portforward_test 19 20 import ( 21 "bytes" 22 "context" 23 "fmt" 24 "io" 25 "net" 26 "os" 27 "os/exec" 28 "path" 29 "regexp" 30 "strings" 31 "testing" 32 "time" 33 34 "github.com/syndtr/gocapability/capability" 35 "golang.org/x/sync/errgroup" 36 "gvisor.dev/gvisor/pkg/cleanup" 37 "gvisor.dev/gvisor/pkg/test/dockerutil" 38 "gvisor.dev/gvisor/runsc/config" 39 "gvisor.dev/gvisor/runsc/flag" 40 "gvisor.dev/gvisor/runsc/specutils" 41 ) 42 43 func TestPortForwardLocalMode(t *testing.T) { 44 ctx := context.Background() 45 server := dockerutil.MakeContainer(ctx, t) 46 defer server.CleanUp(ctx) 47 48 redisPort := 6379 49 if err := server.Spawn(ctx, dockerutil.RunOpts{ 50 Image: "basic/redis", 51 }); err != nil { 52 t.Fatalf("failed to create redis server: %v", err) 53 } 54 55 if err := waitUntilServerIsUp(ctx, server, redisPort); err != nil { 56 t.Fatalf("failed to wait for redis server to be up: %v", err) 57 } 58 59 localPort, err := getUnusedPort() 60 if err != nil { 61 t.Fatalf("failed to pick unused port: %v", err) 62 } 63 ctx, cancel := context.WithCancel(ctx) 64 defer cancel() 65 pf, err := newPortForwardProcess(ctx, server, localPort, redisPort) 66 if err != nil { 67 t.Fatalf("failed to create port forward process: %v", err) 68 } 69 70 var g errgroup.Group 71 g.Go(func() error { 72 // To end this test, we kill the portforward process, which will result in a "signal: killed" 73 // error. Just ignore this error. 74 if err := pf.Wait(); err != nil && !strings.Contains(err.Error(), "signal: killed") { 75 return fmt.Errorf("portforward command: err: %v process error: %v out: %s", err, pf.Error(), pf.Output()) 76 } 77 return nil 78 }) 79 cu := cleanup.Make(pf.Kill) 80 defer cu.Clean() 81 82 client := dockerutil.MakeNativeContainer(ctx, t) 83 defer client.CleanUp(ctx) 84 85 out, err := client.Run(ctx, dockerutil.RunOpts{ 86 Image: "basic/redis", 87 NetworkMode: "host", 88 }, "redis-cli", "--verbose", "-p", fmt.Sprintf("%d", localPort), "-i", "1", "-r", "5", "ping") 89 90 if err != nil { 91 t.Logf("portforward command: err: %v out: %s", pf.Error(), pf.Output()) 92 t.Fatalf("failed to run client: %v out: %s", err, out) 93 } 94 95 if !strings.Contains(out, "PONG") { 96 t.Logf("portforward command: err: %v out: %s", pf.Error(), pf.Output()) 97 t.Fatalf("could not reach redis server: %s", out) 98 } 99 100 cu.Clean() 101 if err := g.Wait(); err != nil { 102 t.Fatalf("failed to kill portforward process: %v", err) 103 } 104 } 105 106 func TestPortForwardStreamMode(t *testing.T) { 107 ctx := context.Background() 108 sockAddrDir, err := os.MkdirTemp("", "temp-") 109 if err != nil { 110 t.Fatalf("failed to create temp dir: %v", err) 111 } 112 defer os.RemoveAll(sockAddrDir) 113 sockAddr := path.Join(sockAddrDir, "echo.sock") 114 115 server := dockerutil.MakeContainer(ctx, t) 116 defer server.CleanUp(ctx) 117 118 nginxPort := 80 119 if err := server.Spawn(ctx, dockerutil.RunOpts{ 120 Image: "basic/nginx", 121 }); err != nil { 122 t.Fatalf("failed to create nginx server: %v", err) 123 } 124 125 if err := waitUntilServerIsUp(ctx, server, nginxPort); err != nil { 126 t.Fatalf("failed to wait for nginx server to be up: %v", err) 127 } 128 129 socket, err := net.Listen("unix", sockAddr) 130 if err != nil { 131 t.Fatalf("failed to listen: %v", err) 132 } 133 defer socket.Close() 134 135 pf, err := newPortForwardStreamProcess(ctx, server, sockAddr, nginxPort) 136 if err != nil { 137 t.Fatalf("failed to create port forward process: %v", err) 138 } 139 140 if err := pf.Wait(); err != nil { 141 t.Fatalf("failed to wait: %v out: %s", err, pf.Output()) 142 } 143 144 conn, err := socket.Accept() 145 if err != nil { 146 t.Fatalf("failed to accept: %v", err) 147 } 148 defer conn.Close() 149 150 const getMsg = "GET / HTTP/1.0\r\n\r\n" 151 if n, err := io.Copy(conn, bytes.NewBufferString(getMsg)); err != nil { 152 t.Fatalf("failed to copy: %v n: %d", err, n) 153 } 154 155 buf, err := io.ReadAll(conn) 156 if err != nil { 157 t.Fatalf("failed to read: %v out: %s", err, string(buf)) 158 } 159 160 const want = "Thank you for using nginx." 161 if !strings.Contains(string(buf), want) { 162 t.Fatalf("could not find %q in output: %s", want, string(buf)) 163 } 164 } 165 166 func getUnusedPort() (int, error) { 167 l, err := net.Listen("tcp", ":0") 168 if err != nil { 169 return 0, err 170 } 171 defer l.Close() 172 return l.Addr().(*net.TCPAddr).Port, nil 173 } 174 175 func waitUntilServerIsUp(ctx context.Context, server *dockerutil.Container, port int) error { 176 // This is a bit crude, but we need to make sure the server is up without exposing a port to the 177 // host. When the server container boots, the nginx process should run first. If we run nginx 178 // again, it will fail to bind to port 80. Run exec calls until we get that failure. 179 serverUpChan := make(chan struct{}) 180 var upOut string 181 var upErr error 182 reg := regexp.MustCompile(fmt.Sprintf(`0\.0\.0\.0:%d[\s]*0\.0\.0\.0:\*[\s]*LISTEN`, port)) 183 go func() { 184 for { 185 time.Sleep(time.Millisecond * 500) 186 upOut, upErr = server.Exec(ctx, dockerutil.ExecOpts{}, []string{"netstat", "-l"}...) 187 if reg.MatchString(upOut) { 188 close(serverUpChan) 189 return 190 } 191 } 192 }() 193 194 // If the server isn't up after 30 seconds, there is probably something wrong. 195 select { 196 case <-serverUpChan: 197 break 198 case <-time.After(time.Second * 30): 199 return fmt.Errorf("could not verify server is up: err: %v out: %s", upErr, upOut) 200 } 201 202 return nil 203 } 204 205 type portForwardProcess struct { 206 cmd *exec.Cmd 207 buf bytes.Buffer 208 } 209 210 func newPortForwardProcess(ctx context.Context, c *dockerutil.Container, localPort, containerPort int) (*portForwardProcess, error) { 211 rootDir, err := c.RootDirectory() 212 if err != nil { 213 return nil, err 214 } 215 args := []string{"-root", rootDir, "port-forward", c.ID(), fmt.Sprintf("%d:%d", localPort, containerPort)} 216 return startPortForwardPorcess(ctx, args) 217 } 218 219 func newPortForwardStreamProcess(ctx context.Context, c *dockerutil.Container, uds string, containerPort int) (*portForwardProcess, error) { 220 rootDir, err := c.RootDirectory() 221 if err != nil { 222 return nil, err 223 } 224 args := []string{"-root", rootDir, "-alsologtostderr", "port-forward", "-stream", uds, c.ID(), fmt.Sprintf("%d", containerPort)} 225 return startPortForwardPorcess(ctx, args) 226 } 227 228 func startPortForwardPorcess(ctx context.Context, args []string) (*portForwardProcess, error) { 229 cmd := exec.CommandContext(ctx, specutils.ExePath, args...) 230 ret := &portForwardProcess{cmd: cmd} 231 ret.cmd.Stdout = &ret.buf 232 ret.cmd.Stderr = &ret.buf 233 if err := cmd.Start(); err != nil { 234 return nil, err 235 } 236 return ret, nil 237 } 238 239 func (p *portForwardProcess) Close() error { 240 return p.cmd.Wait() 241 } 242 243 func (p *portForwardProcess) Wait() error { return p.cmd.Wait() } 244 245 func (p *portForwardProcess) Kill() { p.cmd.Process.Kill() } 246 247 func (p *portForwardProcess) Output() string { return p.buf.String() } 248 249 func (p *portForwardProcess) Error() error { return p.cmd.Err } 250 251 func TestMain(m *testing.M) { 252 config.RegisterFlags(flag.CommandLine) 253 if !flag.CommandLine.Parsed() { 254 flag.Parse() 255 } 256 257 if !specutils.HasCapabilities(capability.CAP_SYS_ADMIN, capability.CAP_DAC_OVERRIDE) { 258 fmt.Println("Test requires sysadmin privileges to run. Try again with sudo.") 259 os.Exit(1) 260 } 261 262 dockerutil.EnsureSupportedDockerVersion() 263 264 // Configure exe for tests. 265 path, err := dockerutil.RuntimePath() 266 if err != nil { 267 panic(err.Error()) 268 } 269 specutils.ExePath = path 270 271 os.Exit(m.Run()) 272 }