mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Add portforward command to runsc
Add portforward comand so that we can use runsc to forward connections to container ports. This will eventually be supported in k8s. PiperOrigin-RevId: 520739913
This commit is contained in:
committed by
gVisor bot
parent
0101b166b4
commit
f92957314c
@@ -271,7 +271,7 @@ steps:
|
||||
# See above: not truly a source test.
|
||||
<<: *source_test
|
||||
label: ":docker: Docker tests (cgroupv2)"
|
||||
command: make docker-tests
|
||||
command: make portforward-tests docker-tests
|
||||
agents:
|
||||
<<: *ubuntu_agents
|
||||
arch: "amd64"
|
||||
|
||||
@@ -262,6 +262,13 @@ arm-qemu-smoke-test: $(RUNTIME_BIN) load-arm-qemu
|
||||
simple-tests: unit-tests # Compatibility target.
|
||||
.PHONY: simple-tests
|
||||
|
||||
portforward-tests: load-basic_redis $(RUNTIME_BIN)
|
||||
@$(call install_runtime,$(RUNTIME),--network=sandbox)
|
||||
@$(call sudo,test/root:portforward_test,--runtime=$(RUNTIME) -test.v)
|
||||
@$(call install_runtime,$(RUNTIME),--network=host)
|
||||
@$(call sudo,test/root:portforward_test,--runtime=$(RUNTIME) -test.v)
|
||||
.PHONY: portforward-test
|
||||
|
||||
# Standard integration targets.
|
||||
INTEGRATION_TARGETS := //test/image:image_test //test/e2e:integration_test
|
||||
|
||||
@@ -324,6 +331,7 @@ fsstress-test: load-basic $(RUNTIME_BIN)
|
||||
@$(call test_runtime,$(RUNTIME),//test/fsstress:fsstress_test)
|
||||
.PHONY: fsstress-test
|
||||
|
||||
|
||||
# Specific containerd version tests.
|
||||
containerd-test-%: load-basic_alpine load-basic_python load-basic_busybox load-basic_symlink-resolv load-basic_httpd load-basic_ubuntu $(RUNTIME_BIN)
|
||||
@$(call install_runtime,$(RUNTIME),) # Clear flags.
|
||||
@@ -335,7 +343,6 @@ else
|
||||
sudo tar -C "$$(dirname $$(which containerd))" -zxvf - containerd-shim-runsc-v1
|
||||
endif
|
||||
@$(call sudo,test/root:root_test,--runtime=$(RUNTIME) -test.v)
|
||||
|
||||
containerd-tests-min: containerd-test-1.4.12
|
||||
|
||||
##
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
FROM redis:5.0.4
|
||||
@@ -45,9 +45,8 @@ import (
|
||||
// client. Container is backed by the offical golang docker API.
|
||||
// See: https://pkg.go.dev/github.com/docker/docker.
|
||||
type Container struct {
|
||||
Name string
|
||||
runtime string
|
||||
|
||||
Name string
|
||||
runtime string
|
||||
logger testutil.Logger
|
||||
client *client.Client
|
||||
id string
|
||||
@@ -90,6 +89,11 @@ type RunOpts struct {
|
||||
// Privileged enables privileged mode.
|
||||
Privileged bool
|
||||
|
||||
// Sets network mode for the container. See container.NetworkMode for types. Several options will
|
||||
// not work w/ gVisor. For example, you can't set the "sandbox" network option for gVisor using
|
||||
// this handle.
|
||||
NetworkMode string
|
||||
|
||||
// CapAdd are the extra set of capabilities to add.
|
||||
CapAdd []string
|
||||
|
||||
@@ -270,6 +274,7 @@ func (c *Container) hostConfig(r RunOpts) *container.HostConfig {
|
||||
CapDrop: r.CapDrop,
|
||||
Privileged: r.Privileged,
|
||||
ReadonlyRootfs: r.ReadOnly,
|
||||
NetworkMode: container.NetworkMode(r.NetworkMode),
|
||||
Resources: container.Resources{
|
||||
Memory: int64(r.Memory), // In bytes.
|
||||
CpusetCpus: r.CpusetCpus,
|
||||
@@ -341,6 +346,25 @@ func (c *Container) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
// RootDirectory returns an educated guess about the container's root directory.
|
||||
func (c *Container) RootDirectory() (string, error) {
|
||||
// The root directory of this container's runtime.
|
||||
rootDir := fmt.Sprintf("/var/run/docker/runtime-%s/moby", c.runtime)
|
||||
_, err := os.Stat(rootDir)
|
||||
if err == nil {
|
||||
return rootDir, nil
|
||||
}
|
||||
// In docker v20+, due to https://github.com/moby/moby/issues/42345 the
|
||||
// rootDir seems to always be the following.
|
||||
const defaultDir = "/var/run/docker/runtime-runc/moby"
|
||||
_, derr := os.Stat(defaultDir)
|
||||
if derr == nil {
|
||||
return defaultDir, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("cannot stat %q: %v or %q: %v", rootDir, err, defaultDir, derr)
|
||||
}
|
||||
|
||||
// SandboxPid returns the container's pid.
|
||||
func (c *Container) SandboxPid(ctx context.Context) (int, error) {
|
||||
resp, err := c.client.ContainerInspect(ctx, c.id)
|
||||
|
||||
@@ -81,12 +81,9 @@ func (p *profile) createProcess(c *Container) error {
|
||||
return fmt.Errorf("failed to get runtime path: %v", err)
|
||||
}
|
||||
|
||||
// The root directory of this container's runtime.
|
||||
rootDir := fmt.Sprintf("/var/run/docker/runtime-%s/moby", c.runtime)
|
||||
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
|
||||
// In docker v20+, due to https://github.com/moby/moby/issues/42345 the
|
||||
// rootDir seems to always be the following.
|
||||
rootDir = "/var/run/docker/runtime-runc/moby"
|
||||
rootDir, err := c.RootDirectory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get root directory: %v", err)
|
||||
}
|
||||
|
||||
// Format is `runsc --root=rootDir debug --profile-*=file --duration=24h containerID`.
|
||||
|
||||
@@ -58,6 +58,9 @@ const (
|
||||
// ContMgrExecuteAsync executes a command in a container.
|
||||
ContMgrExecuteAsync = "containerManager.ExecuteAsync"
|
||||
|
||||
// ContMgrPortForward starts port forwarding with the sandbox.
|
||||
ContMgrPortForward = "containerManager.PortForward"
|
||||
|
||||
// ContMgrProcesses lists processes running in a container.
|
||||
ContMgrProcesses = "containerManager.Processes"
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ type hostInetConn struct {
|
||||
// NewHostInetConn creates a hostInetConn backed by a host socket on the localhost address.
|
||||
func NewHostInetConn(port uint16) (proxyConn, error) {
|
||||
// NOTE: Options must match sandbox seccomp filters. See filter/config.go
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, unix.IPPROTO_TCP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ func Main() {
|
||||
subcommands.Register(new(cmd.List), "")
|
||||
subcommands.Register(new(cmd.PS), "")
|
||||
subcommands.Register(new(cmd.Pause), "")
|
||||
subcommands.Register(new(cmd.PortForward), "")
|
||||
subcommands.Register(new(cmd.Restore), "")
|
||||
subcommands.Register(new(cmd.Resume), "")
|
||||
subcommands.Register(new(cmd.Run), "")
|
||||
|
||||
@@ -33,6 +33,7 @@ go_library(
|
||||
"path.go",
|
||||
"pause.go",
|
||||
"platforms.go",
|
||||
"portforward.go",
|
||||
"ps.go",
|
||||
"read_control.go",
|
||||
"restore.go",
|
||||
@@ -70,6 +71,7 @@ go_library(
|
||||
"//pkg/state/statefile",
|
||||
"//pkg/sync",
|
||||
"//pkg/unet",
|
||||
"//pkg/urpc",
|
||||
"//runsc/boot",
|
||||
"//runsc/cmd/util",
|
||||
"//runsc/config",
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
"gvisor.dev/gvisor/pkg/log"
|
||||
"gvisor.dev/gvisor/pkg/urpc"
|
||||
"gvisor.dev/gvisor/runsc/boot"
|
||||
"gvisor.dev/gvisor/runsc/cmd/util"
|
||||
"gvisor.dev/gvisor/runsc/config"
|
||||
"gvisor.dev/gvisor/runsc/container"
|
||||
"gvisor.dev/gvisor/runsc/flag"
|
||||
)
|
||||
|
||||
// PortForward implements subcommands.Command for the "portforward" command.
|
||||
type PortForward struct {
|
||||
portNum int
|
||||
stream string
|
||||
}
|
||||
|
||||
// Name implements subcommands.Command.Name.
|
||||
func (*PortForward) Name() string {
|
||||
return "port-forward"
|
||||
}
|
||||
|
||||
// Synopsis implements subcommands.Command.Synopsis.
|
||||
func (*PortForward) Synopsis() string {
|
||||
return "port forward to a secure container"
|
||||
}
|
||||
|
||||
// Usage implements subcommands.Command.Usage.
|
||||
func (*PortForward) Usage() string {
|
||||
return `port-forward CONTAINER_ID [LOCAL_PORT:]REMOTE_PORT - port forward to gvisor container.
|
||||
|
||||
Open a local port and forward connections to another port inside the specified
|
||||
container.
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
The following will forward connections on local port 8080 to port 80 in the
|
||||
container named 'nginx':
|
||||
|
||||
# runsc port-forward nginx 8080:80
|
||||
`
|
||||
}
|
||||
|
||||
// SetFlags implements subcommands.Command.SetFlags.
|
||||
func (p *PortForward) SetFlags(f *flag.FlagSet) {}
|
||||
|
||||
// Execute implements subcommands.Command.Execute.
|
||||
func (p *PortForward) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
|
||||
conf := args[0].(*config.Config)
|
||||
// Requires at least the container id and port.
|
||||
if f.NArg() != 2 {
|
||||
f.Usage()
|
||||
return subcommands.ExitUsageError
|
||||
}
|
||||
|
||||
id := f.Arg(0)
|
||||
portStr := f.Arg(1)
|
||||
|
||||
c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, container.LoadOpts{})
|
||||
if err != nil {
|
||||
util.Fatalf("loading container: %v", err)
|
||||
}
|
||||
|
||||
// Allow forwarding to a local port.
|
||||
ports := strings.Split(portStr, ":")
|
||||
if len(ports) != 2 {
|
||||
util.Fatalf("invalid port string %q", portStr)
|
||||
}
|
||||
|
||||
localPort, err := strconv.Atoi(ports[0])
|
||||
if err != nil {
|
||||
util.Fatalf("invalid port string %q: %v", portStr, err)
|
||||
}
|
||||
portNum, err := strconv.Atoi(ports[1])
|
||||
if err != nil {
|
||||
util.Fatalf("invalid port string %q: %v", portStr, err)
|
||||
}
|
||||
if portNum <= 0 || portNum > math.MaxUint16 {
|
||||
util.Fatalf("invalid port %d: %v", portNum, err)
|
||||
}
|
||||
|
||||
// Start port forwarding with the local port.
|
||||
var wg sync.WaitGroup
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
wg.Add(3)
|
||||
go func(localPort, portNum int) {
|
||||
defer cancel()
|
||||
defer wg.Done()
|
||||
// Print message to local user.
|
||||
fmt.Printf("Forwarding local port %d to %d...\n", localPort, portNum)
|
||||
if err := localForward(ctx, c, localPort, uint16(portNum)); err != nil {
|
||||
log.Warningf("port forwarding: %v", err)
|
||||
}
|
||||
}(localPort, portNum)
|
||||
|
||||
// Exit port forwarding if the container exits.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// Cancel port forwarding after Wait returns regardless of return
|
||||
// value as err may indicate sandbox has terminated already.
|
||||
_, _ = c.Wait()
|
||||
fmt.Printf("Container %q stopped. Exiting...\n", c.ID)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Wait for ^C from the user.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sig := waitSignal()
|
||||
fmt.Printf("Got %v, Exiting...\n", sig)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Wait on a WaitGroup for port forwarding to clean up before exiting.
|
||||
wg.Wait()
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// localForward starts port forwarding from the given local port.
|
||||
func localForward(ctx context.Context, c *container.Container, localPort int, containerPort uint16) error {
|
||||
l, err := net.Listen("tcp", ":"+strconv.Itoa(localPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
var localConnChan = make(chan net.Conn, 1)
|
||||
var errChan = make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
localConn, err := l.Accept()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
continue
|
||||
}
|
||||
localConnChan <- localConn
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
// Exit if the context is done.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
log.Warningf("accepting local connection: %v", err)
|
||||
}
|
||||
case localConn := <-localConnChan:
|
||||
// Dispatch a new goroutine to handle the new connection.
|
||||
go func() {
|
||||
defer localConn.Close()
|
||||
fmt.Println("Forwarding new connection...")
|
||||
err := portCopy(ctx, c, localConn, containerPort)
|
||||
if err != nil {
|
||||
log.Warningf("port forwarding: %v", err)
|
||||
}
|
||||
fmt.Println("Finished forwarding connection...")
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// portCopy creates a UDS and begins copying data to and from the local
|
||||
// connection.
|
||||
func portCopy(ctx context.Context, c *container.Container, localConn net.Conn, port uint16) error {
|
||||
// Create a new path address for the UDS.
|
||||
addr, err := tmpUDSAddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the UDS and Listen on it.
|
||||
l, err := net.Listen("unix", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
// Open the UDS as a File so it can be donated to the sentry.
|
||||
streamFile, err := openStream(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening uds stream: %v", err)
|
||||
}
|
||||
defer streamFile.Close()
|
||||
|
||||
// Request port forwarding from the sentry. This request will return
|
||||
// immediately after port forwarding is started and connection state is
|
||||
// handled via the UDS from then on.
|
||||
if err := c.PortForward(&boot.PortForwardOpts{
|
||||
Port: port,
|
||||
FilePayload: urpc.FilePayload{Files: []*os.File{streamFile}},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("PortForward: %v", err)
|
||||
}
|
||||
|
||||
// We have already opened a single connection on the UDS and passed the
|
||||
// client end to the sentry. We accept the connection now in order to get
|
||||
// the other half of the connection.
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
toErrCh := make(chan error)
|
||||
fromErrCh := make(chan error)
|
||||
// Copy data from the local port to the UDS.
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
defer localConn.Close()
|
||||
log.Debugf("Start copying from %q to %q", localConn.LocalAddr().String(), conn.LocalAddr().String())
|
||||
_, err := io.Copy(localConn, conn)
|
||||
log.Debugf("Stopped copying from %q to %q", localConn.LocalAddr().String(), conn.LocalAddr().String())
|
||||
toErrCh <- err
|
||||
close(toErrCh)
|
||||
}()
|
||||
|
||||
// Copy data from the UDS to the local port.
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
defer localConn.Close()
|
||||
log.Debugf("Start copying from %q to %q", conn.LocalAddr().String(), localConn.LocalAddr().String())
|
||||
_, err := io.Copy(conn, localConn)
|
||||
log.Debugf("Stopped copying from %q to %q", conn.LocalAddr().String(), localConn.LocalAddr().String())
|
||||
fromErrCh <- err
|
||||
close(fromErrCh)
|
||||
}()
|
||||
|
||||
errMap := map[string]error{}
|
||||
for {
|
||||
if len(errMap) == 2 {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case e := <-toErrCh:
|
||||
errMap["toChannel"] = e
|
||||
case e := <-fromErrCh:
|
||||
errMap["fromChannel"] = e
|
||||
case <-ctx.Done():
|
||||
log.Debugf("Port forwarding connection canceled for %q: %v", localConn.LocalAddr().String(), ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tmpUDS generates a temporary UDS addr.
|
||||
func tmpUDSAddr() (string, error) {
|
||||
tmpFile, err := ioutil.TempFile("", "runsc-port-forward")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := tmpFile.Name()
|
||||
// Remove the tempfile and just use its name.
|
||||
os.Remove(path)
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// openStream opens a UDS as a socket and returns the file descriptor in an
|
||||
// os.File object.
|
||||
func openStream(name string) (*os.File, error) {
|
||||
// The net package will abstract the fd, so we use raw syscalls.
|
||||
fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We are acting as a client so we will connect to the socket.
|
||||
if err = syscall.Connect(fd, &syscall.SockaddrUnix{Name: name}); err != nil {
|
||||
syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return a File so that we can pass it to the Sentry.
|
||||
return os.NewFile(uintptr(fd), name), nil
|
||||
}
|
||||
|
||||
// waitSignal waits for SIGINT, SIGQUIT, or SIGTERM from the user.
|
||||
func waitSignal() os.Signal {
|
||||
ch := make(chan os.Signal, 2)
|
||||
signal.Notify(
|
||||
ch,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGQUIT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
return <-ch
|
||||
}
|
||||
@@ -574,6 +574,15 @@ func (c *Container) Event() (*boot.EventOut, error) {
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// PortForward starts port forwarding to the container.
|
||||
func (c *Container) PortForward(opts *boot.PortForwardOpts) error {
|
||||
if err := c.requireStatus("port forward", Running); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.ContainerID = c.ID
|
||||
return c.Sandbox.PortForward(opts)
|
||||
}
|
||||
|
||||
// SandboxPid returns the Getpid of the sandbox the container is running in, or -1 if the
|
||||
// container is not running.
|
||||
func (c *Container) SandboxPid() int {
|
||||
|
||||
@@ -567,6 +567,22 @@ func (s *Sandbox) Event(cid string) (*boot.EventOut, error) {
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// PortForward starts port forwarding to the sandbox.
|
||||
func (s *Sandbox) PortForward(opts *boot.PortForwardOpts) error {
|
||||
log.Debugf("Requesting port forward for container %q in sandbox %q: %+v", opts.ContainerID, s.ID, opts)
|
||||
conn, err := s.sandboxConnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.Call(boot.ContMgrPortForward, opts, nil); err != nil {
|
||||
return fmt.Errorf("port forwarding to sandbox: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sandbox) sandboxConnect() (*urpc.Client, error) {
|
||||
log.Debugf("Connecting to sandbox %q", s.ID)
|
||||
conn, err := client.ConnectTo(s.ControlAddress)
|
||||
@@ -1322,7 +1338,7 @@ func (s *Sandbox) waitForStopped() error {
|
||||
return nil
|
||||
}
|
||||
// The sandbox process is a child of the current process,
|
||||
// so we can wait it and collect its zombie.
|
||||
// so we can wait on it to terminate and collect its zombie.
|
||||
if _, err := unix.Wait4(int(pid), &s.status, 0, nil); err != nil {
|
||||
return fmt.Errorf("error waiting the sandbox process: %v", err)
|
||||
}
|
||||
|
||||
@@ -47,3 +47,23 @@ go_test(
|
||||
"@org_golang_x_sys//unix:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "portforward_test",
|
||||
srcs = ["portforward_test.go"],
|
||||
pure = True,
|
||||
tags = [
|
||||
"local",
|
||||
"manual",
|
||||
],
|
||||
visibility = ["//:sandbox"],
|
||||
deps = [
|
||||
"//pkg/cleanup",
|
||||
"//pkg/test/dockerutil",
|
||||
"//runsc/config",
|
||||
"//runsc/flag",
|
||||
"//runsc/specutils",
|
||||
"@com_github_syndtr_gocapability//capability:go_default_library",
|
||||
"@org_golang_x_sync//errgroup:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// 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"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/syndtr/gocapability/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 TestPortForward(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)
|
||||
}
|
||||
|
||||
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 {
|
||||
pf.Wait()
|
||||
if pf.Error() != nil {
|
||||
return fmt.Errorf("portforward command: err: %v out: %s", 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 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
|
||||
}
|
||||
|
||||
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)}
|
||||
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() { 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())
|
||||
}
|
||||
Reference in New Issue
Block a user