github.com/moby/docker@v26.1.3+incompatible/integration-cli/docker_cli_port_test.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"regexp"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/docker/docker/integration-cli/cli"
    13  	"github.com/docker/docker/testutil"
    14  	"gotest.tools/v3/assert"
    15  	is "gotest.tools/v3/assert/cmp"
    16  )
    17  
    18  type DockerCLIPortSuite struct {
    19  	ds *DockerSuite
    20  }
    21  
    22  func (s *DockerCLIPortSuite) TearDownTest(ctx context.Context, c *testing.T) {
    23  	s.ds.TearDownTest(ctx, c)
    24  }
    25  
    26  func (s *DockerCLIPortSuite) OnTimeout(c *testing.T) {
    27  	s.ds.OnTimeout(c)
    28  }
    29  
    30  func (s *DockerCLIPortSuite) TestPortList(c *testing.T) {
    31  	testRequires(c, DaemonIsLinux)
    32  	ctx := testutil.GetContext(c)
    33  
    34  	// one port
    35  	firstID := cli.DockerCmd(c, "run", "-d", "-p", "9876:80", "busybox", "top").Stdout()
    36  	firstID = strings.TrimSpace(firstID)
    37  
    38  	out := cli.DockerCmd(c, "port", firstID, "80").Stdout()
    39  
    40  	assertPortList(c, out, []string{"0.0.0.0:9876", "[::]:9876"})
    41  
    42  	out = cli.DockerCmd(c, "port", firstID).Stdout()
    43  
    44  	assertPortList(c, out, []string{"80/tcp -> 0.0.0.0:9876", "80/tcp -> [::]:9876"})
    45  
    46  	cli.DockerCmd(c, "rm", "-f", firstID)
    47  
    48  	// three port
    49  	id := cli.DockerCmd(c, "run", "-d",
    50  		"-p", "9876:80",
    51  		"-p", "9877:81",
    52  		"-p", "9878:82",
    53  		"busybox", "top",
    54  	).Stdout()
    55  	id = strings.TrimSpace(id)
    56  
    57  	out = cli.DockerCmd(c, "port", id, "80").Stdout()
    58  
    59  	assertPortList(c, out, []string{"0.0.0.0:9876", "[::]:9876"})
    60  
    61  	out = cli.DockerCmd(c, "port", id).Stdout()
    62  
    63  	assertPortList(c, out, []string{
    64  		"80/tcp -> 0.0.0.0:9876",
    65  		"80/tcp -> [::]:9876",
    66  		"81/tcp -> 0.0.0.0:9877",
    67  		"81/tcp -> [::]:9877",
    68  		"82/tcp -> 0.0.0.0:9878",
    69  		"82/tcp -> [::]:9878",
    70  	})
    71  
    72  	cli.DockerCmd(c, "rm", "-f", id)
    73  
    74  	// more and one port mapped to the same container port
    75  	id = cli.DockerCmd(c, "run", "-d",
    76  		"-p", "9876:80",
    77  		"-p", "9999:80",
    78  		"-p", "9877:81",
    79  		"-p", "9878:82",
    80  		"busybox", "top",
    81  	).Stdout()
    82  	id = strings.TrimSpace(id)
    83  
    84  	out = cli.DockerCmd(c, "port", id, "80").Stdout()
    85  
    86  	assertPortList(c, out, []string{"0.0.0.0:9876", "[::]:9876", "0.0.0.0:9999", "[::]:9999"})
    87  
    88  	out = cli.DockerCmd(c, "port", id).Stdout()
    89  
    90  	assertPortList(c, out, []string{
    91  		"80/tcp -> 0.0.0.0:9876",
    92  		"80/tcp -> 0.0.0.0:9999",
    93  		"80/tcp -> [::]:9876",
    94  		"80/tcp -> [::]:9999",
    95  		"81/tcp -> 0.0.0.0:9877",
    96  		"81/tcp -> [::]:9877",
    97  		"82/tcp -> 0.0.0.0:9878",
    98  		"82/tcp -> [::]:9878",
    99  	})
   100  	cli.DockerCmd(c, "rm", "-f", id)
   101  
   102  	testRange := func() {
   103  		// host port ranges used
   104  		IDs := make([]string, 3)
   105  		for i := 0; i < 3; i++ {
   106  			out = cli.DockerCmd(c, "run", "-d", "-p", "9090-9092:80", "busybox", "top").Stdout()
   107  			IDs[i] = strings.TrimSpace(out)
   108  
   109  			out = cli.DockerCmd(c, "port", IDs[i]).Stdout()
   110  
   111  			assertPortList(c, out, []string{
   112  				fmt.Sprintf("80/tcp -> 0.0.0.0:%d", 9090+i),
   113  				fmt.Sprintf("80/tcp -> [::]:%d", 9090+i),
   114  			})
   115  		}
   116  
   117  		// test port range exhaustion
   118  		out, _, err := dockerCmdWithError("run", "-d", "-p", "9090-9092:80", "busybox", "top")
   119  		// Exhausted port range did not return an error
   120  		assert.Assert(c, err != nil, "out: %s", out)
   121  
   122  		for i := 0; i < 3; i++ {
   123  			cli.DockerCmd(c, "rm", "-f", IDs[i])
   124  		}
   125  	}
   126  	testRange()
   127  	// Verify we ran re-use port ranges after they are no longer in use.
   128  	testRange()
   129  
   130  	// test invalid port ranges
   131  	for _, invalidRange := range []string{"9090-9089:80", "9090-:80", "-9090:80"} {
   132  		out, _, err := dockerCmdWithError("run", "-d", "-p", invalidRange, "busybox", "top")
   133  		// Port range should have returned an error
   134  		assert.Assert(c, err != nil, "out: %s", out)
   135  	}
   136  
   137  	// test host range:container range spec.
   138  	id = cli.DockerCmd(c, "run", "-d", "-p", "9800-9803:80-83", "busybox", "top").Stdout()
   139  	id = strings.TrimSpace(id)
   140  
   141  	out = cli.DockerCmd(c, "port", id).Stdout()
   142  
   143  	assertPortList(c, out, []string{
   144  		"80/tcp -> 0.0.0.0:9800",
   145  		"80/tcp -> [::]:9800",
   146  		"81/tcp -> 0.0.0.0:9801",
   147  		"81/tcp -> [::]:9801",
   148  		"82/tcp -> 0.0.0.0:9802",
   149  		"82/tcp -> [::]:9802",
   150  		"83/tcp -> 0.0.0.0:9803",
   151  		"83/tcp -> [::]:9803",
   152  	})
   153  	cli.DockerCmd(c, "rm", "-f", id)
   154  
   155  	// test mixing protocols in same port range
   156  	id = cli.DockerCmd(c, "run", "-d", "-p", "8000-8080:80", "-p", "8000-8080:80/udp", "busybox", "top").Stdout()
   157  	id = strings.TrimSpace(id)
   158  
   159  	out = cli.DockerCmd(c, "port", id).Stdout()
   160  	// Running this test multiple times causes the TCP port to increment.
   161  	err := assertPortRange(ctx, id, []int{8000, 8080}, []int{8000, 8080})
   162  	assert.Check(c, err)
   163  	cli.DockerCmd(c, "rm", "-f", id)
   164  }
   165  
   166  func assertPortList(c *testing.T, out string, expected []string) {
   167  	c.Helper()
   168  	lines := strings.Split(strings.Trim(out, "\n "), "\n")
   169  	assert.Assert(c, is.Len(lines, len(expected)), "exepcted: %s", strings.Join(expected, ", "))
   170  
   171  	sort.Strings(lines)
   172  	sort.Strings(expected)
   173  
   174  	// "docker port" does not yet have a "--format" flag, and older versions
   175  	// of the CLI used an incorrect output format for mappings on IPv6 addresses
   176  	// for example, "80/tcp -> :::80" instead of "80/tcp -> [::]:80".
   177  	oldFormat := func(mapping string) string {
   178  		old := strings.Replace(mapping, "[", "", 1)
   179  		old = strings.Replace(old, "]:", ":", 1)
   180  		return old
   181  	}
   182  
   183  	for i := 0; i < len(expected); i++ {
   184  		if lines[i] == expected[i] {
   185  			continue
   186  		}
   187  		assert.Equal(c, lines[i], oldFormat(expected[i]))
   188  	}
   189  }
   190  
   191  func assertPortRange(ctx context.Context, id string, expectedTCP, expectedUDP []int) error {
   192  	client := testEnv.APIClient()
   193  	inspect, err := client.ContainerInspect(ctx, id)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	var validTCP, validUDP bool
   199  	for portAndProto, binding := range inspect.NetworkSettings.Ports {
   200  		if portAndProto.Proto() == "tcp" && len(expectedTCP) == 0 {
   201  			continue
   202  		}
   203  		if portAndProto.Proto() == "udp" && len(expectedTCP) == 0 {
   204  			continue
   205  		}
   206  
   207  		for _, b := range binding {
   208  			port, err := strconv.Atoi(b.HostPort)
   209  			if err != nil {
   210  				return err
   211  			}
   212  
   213  			if len(expectedTCP) > 0 {
   214  				if port < expectedTCP[0] || port > expectedTCP[1] {
   215  					return fmt.Errorf("tcp port (%d) not in range expected range %d-%d", port, expectedTCP[0], expectedTCP[1])
   216  				}
   217  				validTCP = true
   218  			}
   219  			if len(expectedUDP) > 0 {
   220  				if port < expectedUDP[0] || port > expectedUDP[1] {
   221  					return fmt.Errorf("udp port (%d) not in range expected range %d-%d", port, expectedUDP[0], expectedUDP[1])
   222  				}
   223  				validUDP = true
   224  			}
   225  		}
   226  	}
   227  	if !validTCP {
   228  		return fmt.Errorf("tcp port not found")
   229  	}
   230  	if !validUDP {
   231  		return fmt.Errorf("udp port not found")
   232  	}
   233  	return nil
   234  }
   235  
   236  func stopRemoveContainer(id string, c *testing.T) {
   237  	cli.DockerCmd(c, "rm", "-f", id)
   238  }
   239  
   240  func (s *DockerCLIPortSuite) TestUnpublishedPortsInPsOutput(c *testing.T) {
   241  	testRequires(c, DaemonIsLinux)
   242  	// Run busybox with command line expose (equivalent to EXPOSE in image's Dockerfile) for the following ports
   243  	port1 := 80
   244  	port2 := 443
   245  	expose1 := fmt.Sprintf("--expose=%d", port1)
   246  	expose2 := fmt.Sprintf("--expose=%d", port2)
   247  	cli.DockerCmd(c, "run", "-d", expose1, expose2, "busybox", "sleep", "5")
   248  
   249  	// Check docker ps o/p for last created container reports the unpublished ports
   250  	unpPort1 := fmt.Sprintf("%d/tcp", port1)
   251  	unpPort2 := fmt.Sprintf("%d/tcp", port2)
   252  	out := cli.DockerCmd(c, "ps", "-n=1").Stdout()
   253  	// Missing unpublished ports in docker ps output
   254  	assert.Assert(c, strings.Contains(out, unpPort1))
   255  	// Missing unpublished ports in docker ps output
   256  	assert.Assert(c, strings.Contains(out, unpPort2))
   257  	// Run the container forcing to publish the exposed ports
   258  	cli.DockerCmd(c, "run", "-d", "-P", expose1, expose2, "busybox", "sleep", "5")
   259  
   260  	// Check docker ps o/p for last created container reports the exposed ports in the port bindings
   261  	expBndRegx1 := regexp.MustCompile(`0.0.0.0:\d\d\d\d\d->` + unpPort1)
   262  	expBndRegx2 := regexp.MustCompile(`0.0.0.0:\d\d\d\d\d->` + unpPort2)
   263  	out = cli.DockerCmd(c, "ps", "-n=1").Stdout()
   264  	// Cannot find expected port binding port (0.0.0.0:xxxxx->unpPort1) in docker ps output
   265  	assert.Equal(c, expBndRegx1.MatchString(out), true, fmt.Sprintf("out: %s; unpPort1: %s", out, unpPort1))
   266  	// Cannot find expected port binding port (0.0.0.0:xxxxx->unpPort2) in docker ps output
   267  	assert.Equal(c, expBndRegx2.MatchString(out), true, fmt.Sprintf("out: %s; unpPort2: %s", out, unpPort2))
   268  
   269  	// Run the container specifying explicit port bindings for the exposed ports
   270  	offset := 10000
   271  	pFlag1 := fmt.Sprintf("%d:%d", offset+port1, port1)
   272  	pFlag2 := fmt.Sprintf("%d:%d", offset+port2, port2)
   273  
   274  	id := cli.DockerCmd(c, "run", "-d", "-p", pFlag1, "-p", pFlag2, expose1, expose2, "busybox", "sleep", "5").Stdout()
   275  	id = strings.TrimSpace(id)
   276  
   277  	// Check docker ps o/p for last created container reports the specified port mappings
   278  	expBnd1 := fmt.Sprintf("0.0.0.0:%d->%s", offset+port1, unpPort1)
   279  	expBnd2 := fmt.Sprintf("0.0.0.0:%d->%s", offset+port2, unpPort2)
   280  	out = cli.DockerCmd(c, "ps", "-n=1").Stdout()
   281  	// Cannot find expected port binding (expBnd1) in docker ps output
   282  	assert.Assert(c, strings.Contains(out, expBnd1))
   283  	// Cannot find expected port binding (expBnd2) in docker ps output
   284  	assert.Assert(c, strings.Contains(out, expBnd2))
   285  	// Remove container now otherwise it will interfere with next test
   286  	stopRemoveContainer(id, c)
   287  
   288  	// Run the container with explicit port bindings and no exposed ports
   289  	id = cli.DockerCmd(c, "run", "-d", "-p", pFlag1, "-p", pFlag2, "busybox", "sleep", "5").Stdout()
   290  	id = strings.TrimSpace(id)
   291  
   292  	// Check docker ps o/p for last created container reports the specified port mappings
   293  	out = cli.DockerCmd(c, "ps", "-n=1").Stdout()
   294  	// Cannot find expected port binding (expBnd1) in docker ps output
   295  	assert.Assert(c, strings.Contains(out, expBnd1))
   296  	// Cannot find expected port binding (expBnd2) in docker ps output
   297  	assert.Assert(c, strings.Contains(out, expBnd2))
   298  	// Remove container now otherwise it will interfere with next test
   299  	stopRemoveContainer(id, c)
   300  
   301  	// Run the container with one unpublished exposed port and one explicit port binding
   302  	cli.DockerCmd(c, "run", "-d", expose1, "-p", pFlag2, "busybox", "sleep", "5")
   303  
   304  	// Check docker ps o/p for last created container reports the specified unpublished port and port mapping
   305  	out = cli.DockerCmd(c, "ps", "-n=1").Stdout()
   306  	// Missing unpublished exposed ports (unpPort1) in docker ps output
   307  	assert.Assert(c, strings.Contains(out, unpPort1))
   308  	// Missing port binding (expBnd2) in docker ps output
   309  	assert.Assert(c, strings.Contains(out, expBnd2))
   310  }
   311  
   312  func (s *DockerCLIPortSuite) TestPortHostBinding(c *testing.T) {
   313  	testRequires(c, DaemonIsLinux, NotUserNamespace)
   314  	firstID := cli.DockerCmd(c, "run", "-d", "-p", "9876:80", "busybox", "nc", "-l", "-p", "80").Stdout()
   315  	firstID = strings.TrimSpace(firstID)
   316  
   317  	out := cli.DockerCmd(c, "port", firstID, "80").Stdout()
   318  
   319  	assertPortList(c, out, []string{"0.0.0.0:9876", "[::]:9876"})
   320  
   321  	cli.DockerCmd(c, "run", "--net=host", "busybox", "nc", "localhost", "9876")
   322  
   323  	cli.DockerCmd(c, "rm", "-f", firstID)
   324  
   325  	out, _, err := dockerCmdWithError("run", "--net=host", "busybox", "nc", "localhost", "9876")
   326  	// Port is still bound after the Container is removed
   327  	assert.Assert(c, err != nil, out)
   328  }
   329  
   330  func (s *DockerCLIPortSuite) TestPortExposeHostBinding(c *testing.T) {
   331  	testRequires(c, DaemonIsLinux, NotUserNamespace)
   332  	firstID := cli.DockerCmd(c, "run", "-d", "-P", "--expose", "80", "busybox", "nc", "-l", "-p", "80").Stdout()
   333  	firstID = strings.TrimSpace(firstID)
   334  
   335  	exposedPort := cli.DockerCmd(c, "inspect", "--format", `{{index .NetworkSettings.Ports "80/tcp" 0 "HostPort" }}`, firstID).Stdout()
   336  	exposedPort = strings.TrimSpace(exposedPort)
   337  	cli.DockerCmd(c, "run", "--net=host", "busybox", "nc", "127.0.0.1", exposedPort)
   338  
   339  	cli.DockerCmd(c, "rm", "-f", firstID)
   340  
   341  	out, _, err := dockerCmdWithError("run", "--net=host", "busybox", "nc", "127.0.0.1", exposedPort)
   342  	// Port is still bound after the Container is removed
   343  	assert.Assert(c, err != nil, "out: %s", out)
   344  }
   345  
   346  func (s *DockerCLIPortSuite) TestPortBindingOnSandbox(c *testing.T) {
   347  	testRequires(c, DaemonIsLinux, NotUserNamespace)
   348  	cli.DockerCmd(c, "network", "create", "--internal", "-d", "bridge", "internal-net")
   349  	nr := getNetworkResource(c, "internal-net")
   350  	assert.Equal(c, nr.Internal, true)
   351  
   352  	cli.DockerCmd(c, "run", "--net", "internal-net", "-d", "--name", "c1", "-p", "8080:8080", "busybox", "nc", "-l", "-p", "8080")
   353  	cli.WaitRun(c, "c1")
   354  
   355  	_, _, err := dockerCmdWithError("run", "--net=host", "busybox", "nc", "localhost", "8080")
   356  	assert.Assert(c, err != nil, "Port mapping on internal network is expected to fail")
   357  	// Connect container to another normal bridge network
   358  	cli.DockerCmd(c, "network", "create", "-d", "bridge", "foo-net")
   359  	cli.DockerCmd(c, "network", "connect", "foo-net", "c1")
   360  
   361  	_, _, err = dockerCmdWithError("run", "--net=host", "busybox", "nc", "localhost", "8080")
   362  	assert.Assert(c, err == nil, "Port mapping on the new network is expected to succeed")
   363  }