Files
visor/test/root/portforward_test.go
2025-03-25 03:41:42 -07:00

273 lines
7.6 KiB
Go

// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package portforward_test holds a docker test for port forward. It is separate
// from other root tests so that both hostinet and netstack can be tested in
// one Makefile target.
package portforward_test
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path"
"regexp"
"strings"
"testing"
"time"
"github.com/moby/sys/capability"
"golang.org/x/sync/errgroup"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/test/dockerutil"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/flag"
"gvisor.dev/gvisor/runsc/specutils"
)
func TestPortForwardLocalMode(t *testing.T) {
ctx := context.Background()
server := dockerutil.MakeContainer(ctx, t)
defer server.CleanUp(ctx)
redisPort := 6379
if err := server.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/redis",
}); err != nil {
t.Fatalf("failed to create redis server: %v", err)
}
if err := waitUntilServerIsUp(ctx, server, redisPort); err != nil {
t.Fatalf("failed to wait for redis server to be up: %v", err)
}
localPort, err := getUnusedPort()
if err != nil {
t.Fatalf("failed to pick unused port: %v", err)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
pf, err := newPortForwardProcess(ctx, server, localPort, redisPort)
if err != nil {
t.Fatalf("failed to create port forward process: %v", err)
}
var g errgroup.Group
g.Go(func() error {
// To end this test, we kill the portforward process, which will result in a "signal: killed"
// error. Just ignore this error.
if err := pf.Wait(); err != nil && !strings.Contains(err.Error(), "signal: killed") {
return fmt.Errorf("portforward command: err: %v process error: %v out: %s", err, pf.Error(), pf.Output())
}
return nil
})
cu := cleanup.Make(pf.Kill)
defer cu.Clean()
client := dockerutil.MakeNativeContainer(ctx, t)
defer client.CleanUp(ctx)
out, err := client.Run(ctx, dockerutil.RunOpts{
Image: "basic/redis",
NetworkMode: "host",
}, "redis-cli", "--verbose", "-p", fmt.Sprintf("%d", localPort), "-i", "1", "-r", "5", "ping")
if err != nil {
t.Logf("portforward command: err: %v out: %s", pf.Error(), pf.Output())
t.Fatalf("failed to run client: %v out: %s", err, out)
}
if !strings.Contains(out, "PONG") {
t.Logf("portforward command: err: %v out: %s", pf.Error(), pf.Output())
t.Fatalf("could not reach redis server: %s", out)
}
cu.Clean()
if err := g.Wait(); err != nil {
t.Fatalf("failed to kill portforward process: %v", err)
}
}
func TestPortForwardStreamMode(t *testing.T) {
ctx := context.Background()
sockAddrDir, err := os.MkdirTemp("", "temp-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(sockAddrDir)
sockAddr := path.Join(sockAddrDir, "echo.sock")
server := dockerutil.MakeContainer(ctx, t)
defer server.CleanUp(ctx)
nginxPort := 80
if err := server.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/nginx",
}); err != nil {
t.Fatalf("failed to create nginx server: %v", err)
}
if err := waitUntilServerIsUp(ctx, server, nginxPort); err != nil {
t.Fatalf("failed to wait for nginx server to be up: %v", err)
}
socket, err := net.Listen("unix", sockAddr)
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer socket.Close()
pf, err := newPortForwardStreamProcess(ctx, server, sockAddr, nginxPort)
if err != nil {
t.Fatalf("failed to create port forward process: %v", err)
}
if err := pf.Wait(); err != nil {
t.Fatalf("failed to wait: %v out: %s", err, pf.Output())
}
conn, err := socket.Accept()
if err != nil {
t.Fatalf("failed to accept: %v", err)
}
defer conn.Close()
const getMsg = "GET / HTTP/1.0\r\n\r\n"
if n, err := io.Copy(conn, bytes.NewBufferString(getMsg)); err != nil {
t.Fatalf("failed to copy: %v n: %d", err, n)
}
buf, err := io.ReadAll(conn)
if err != nil {
t.Fatalf("failed to read: %v out: %s", err, string(buf))
}
const want = "Thank you for using nginx."
if !strings.Contains(string(buf), want) {
t.Fatalf("could not find %q in output: %s", want, string(buf))
}
}
func getUnusedPort() (int, error) {
l, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}
func waitUntilServerIsUp(ctx context.Context, server *dockerutil.Container, port int) error {
// This is a bit crude, but we need to make sure the server is up without exposing a port to the
// host. When the server container boots, the nginx process should run first. If we run nginx
// again, it will fail to bind to port 80. Run exec calls until we get that failure.
serverUpChan := make(chan struct{})
var upOut string
var upErr error
reg := regexp.MustCompile(fmt.Sprintf(`0\.0\.0\.0:%d[\s]*0\.0\.0\.0:\*[\s]*LISTEN`, port))
go func() {
for {
time.Sleep(time.Millisecond * 500)
upOut, upErr = server.Exec(ctx, dockerutil.ExecOpts{}, []string{"netstat", "-l"}...)
if reg.MatchString(upOut) {
close(serverUpChan)
return
}
}
}()
// If the server isn't up after 30 seconds, there is probably something wrong.
select {
case <-serverUpChan:
break
case <-time.After(time.Second * 30):
return fmt.Errorf("could not verify server is up: err: %v out: %s", upErr, upOut)
}
return nil
}
type portForwardProcess struct {
cmd *exec.Cmd
buf bytes.Buffer
}
func newPortForwardProcess(ctx context.Context, c *dockerutil.Container, localPort, containerPort int) (*portForwardProcess, error) {
rootDir, err := c.RootDirectory()
if err != nil {
return nil, err
}
args := []string{"-root", rootDir, "port-forward", c.ID(), fmt.Sprintf("%d:%d", localPort, containerPort)}
return startPortForwardPorcess(ctx, args)
}
func newPortForwardStreamProcess(ctx context.Context, c *dockerutil.Container, uds string, containerPort int) (*portForwardProcess, error) {
rootDir, err := c.RootDirectory()
if err != nil {
return nil, err
}
args := []string{"-root", rootDir, "-alsologtostderr", "port-forward", "-stream", uds, c.ID(), fmt.Sprintf("%d", containerPort)}
return startPortForwardPorcess(ctx, args)
}
func startPortForwardPorcess(ctx context.Context, args []string) (*portForwardProcess, error) {
cmd := exec.CommandContext(ctx, specutils.ExePath, args...)
ret := &portForwardProcess{cmd: cmd}
ret.cmd.Stdout = &ret.buf
ret.cmd.Stderr = &ret.buf
if err := cmd.Start(); err != nil {
return nil, err
}
return ret, nil
}
func (p *portForwardProcess) Close() error {
return p.cmd.Wait()
}
func (p *portForwardProcess) Wait() error { return p.cmd.Wait() }
func (p *portForwardProcess) Kill() { p.cmd.Process.Kill() }
func (p *portForwardProcess) Output() string { return p.buf.String() }
func (p *portForwardProcess) Error() error { return p.cmd.Err }
func TestMain(m *testing.M) {
config.RegisterFlags(flag.CommandLine)
if !flag.CommandLine.Parsed() {
flag.Parse()
}
if !specutils.HasCapabilities(capability.CAP_SYS_ADMIN, capability.CAP_DAC_OVERRIDE) {
fmt.Println("Test requires sysadmin privileges to run. Try again with sudo.")
os.Exit(1)
}
dockerutil.EnsureSupportedDockerVersion()
// Configure exe for tests.
path, err := dockerutil.RuntimePath()
if err != nil {
panic(err.Error())
}
specutils.ExePath = path
os.Exit(m.Run())
}