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  }