Browse Source

Merge pull request #10238 from miminar/ephemeral-port-range-allocation

Use system's ephemeral port range for port allocation
Alexander Morozov 10 years ago
parent
commit
02c1dd899a

+ 43 - 9
daemon/networkdriver/portallocator/portallocator.go

@@ -1,10 +1,24 @@
 package portallocator
 
 import (
+	"bufio"
 	"errors"
 	"fmt"
 	"net"
+	"os"
 	"sync"
+
+	log "github.com/Sirupsen/logrus"
+)
+
+const (
+	DefaultPortRangeStart = 49153
+	DefaultPortRangeEnd   = 65535
+)
+
+var (
+	beginPortRange = DefaultPortRangeStart
+	endPortRange   = DefaultPortRangeEnd
 )
 
 type portMap struct {
@@ -15,7 +29,7 @@ type portMap struct {
 func newPortMap() *portMap {
 	return &portMap{
 		p:    map[int]struct{}{},
-		last: EndPortRange,
+		last: endPortRange,
 	}
 }
 
@@ -30,11 +44,6 @@ func newProtoMap() protoMap {
 
 type ipMapping map[string]protoMap
 
-const (
-	BeginPortRange = 49153
-	EndPortRange   = 65535
-)
-
 var (
 	ErrAllPortsAllocated = errors.New("all ports are allocated")
 	ErrUnknownProtocol   = errors.New("unknown protocol")
@@ -59,6 +68,31 @@ func NewErrPortAlreadyAllocated(ip string, port int) ErrPortAlreadyAllocated {
 	}
 }
 
+func init() {
+	const param = "/proc/sys/net/ipv4/ip_local_port_range"
+
+	file, err := os.Open(param)
+	if err != nil {
+		log.Errorf("Failed to read %s kernel parameter: %s", param, err.Error())
+		return
+	}
+	var start, end int
+	n, err := fmt.Fscanf(bufio.NewReader(file), "%d\t%d", &start, &end)
+	if n != 2 || err != nil {
+		if err == nil {
+			err = fmt.Errorf("unexpected count of parsed numbers (%d)", n)
+		}
+		log.Errorf("Failed to parse port range from %s: %v", param, err)
+		return
+	}
+	beginPortRange = start
+	endPortRange = end
+}
+
+func PortRange() (int, int) {
+	return beginPortRange, endPortRange
+}
+
 func (e ErrPortAlreadyAllocated) IP() string {
 	return e.ip
 }
@@ -137,10 +171,10 @@ func ReleaseAll() error {
 
 func (pm *portMap) findPort() (int, error) {
 	port := pm.last
-	for i := 0; i <= EndPortRange-BeginPortRange; i++ {
+	for i := 0; i <= endPortRange-beginPortRange; i++ {
 		port++
-		if port > EndPortRange {
-			port = BeginPortRange
+		if port > endPortRange {
+			port = beginPortRange
 		}
 
 		if _, ok := pm.p[port]; !ok {

+ 15 - 10
daemon/networkdriver/portallocator/portallocator_test.go

@@ -5,6 +5,11 @@ import (
 	"testing"
 )
 
+func init() {
+	beginPortRange = DefaultPortRangeStart
+	endPortRange = DefaultPortRangeEnd
+}
+
 func reset() {
 	ReleaseAll()
 }
@@ -17,7 +22,7 @@ func TestRequestNewPort(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	if expected := BeginPortRange; port != expected {
+	if expected := beginPortRange; port != expected {
 		t.Fatalf("Expected port %d got %d", expected, port)
 	}
 }
@@ -102,13 +107,13 @@ func TestUnknowProtocol(t *testing.T) {
 func TestAllocateAllPorts(t *testing.T) {
 	defer reset()
 
-	for i := 0; i <= EndPortRange-BeginPortRange; i++ {
+	for i := 0; i <= endPortRange-beginPortRange; i++ {
 		port, err := RequestPort(defaultIP, "tcp", 0)
 		if err != nil {
 			t.Fatal(err)
 		}
 
-		if expected := BeginPortRange + i; port != expected {
+		if expected := beginPortRange + i; port != expected {
 			t.Fatalf("Expected port %d got %d", expected, port)
 		}
 	}
@@ -123,7 +128,7 @@ func TestAllocateAllPorts(t *testing.T) {
 	}
 
 	// release a port in the middle and ensure we get another tcp port
-	port := BeginPortRange + 5
+	port := beginPortRange + 5
 	if err := ReleasePort(defaultIP, "tcp", port); err != nil {
 		t.Fatal(err)
 	}
@@ -153,13 +158,13 @@ func BenchmarkAllocatePorts(b *testing.B) {
 	defer reset()
 
 	for i := 0; i < b.N; i++ {
-		for i := 0; i <= EndPortRange-BeginPortRange; i++ {
+		for i := 0; i <= endPortRange-beginPortRange; i++ {
 			port, err := RequestPort(defaultIP, "tcp", 0)
 			if err != nil {
 				b.Fatal(err)
 			}
 
-			if expected := BeginPortRange + i; port != expected {
+			if expected := beginPortRange + i; port != expected {
 				b.Fatalf("Expected port %d got %d", expected, port)
 			}
 		}
@@ -231,15 +236,15 @@ func TestPortAllocation(t *testing.T) {
 func TestNoDuplicateBPR(t *testing.T) {
 	defer reset()
 
-	if port, err := RequestPort(defaultIP, "tcp", BeginPortRange); err != nil {
+	if port, err := RequestPort(defaultIP, "tcp", beginPortRange); err != nil {
 		t.Fatal(err)
-	} else if port != BeginPortRange {
-		t.Fatalf("Expected port %d got %d", BeginPortRange, port)
+	} else if port != beginPortRange {
+		t.Fatalf("Expected port %d got %d", beginPortRange, port)
 	}
 
 	if port, err := RequestPort(defaultIP, "tcp", 0); err != nil {
 		t.Fatal(err)
-	} else if port == BeginPortRange {
+	} else if port == beginPortRange {
 		t.Fatalf("Acquire(0) allocated the same port twice: %d", port)
 	}
 }

+ 4 - 3
daemon/networkdriver/portmapper/mapper_test.go

@@ -129,7 +129,8 @@ func TestMapAllPortsSingleInterface(t *testing.T) {
 	}()
 
 	for i := 0; i < 10; i++ {
-		for i := portallocator.BeginPortRange; i < portallocator.EndPortRange; i++ {
+		start, end := portallocator.PortRange()
+		for i := start; i < end; i++ {
 			if host, err = Map(srcAddr1, dstIp1, 0); err != nil {
 				t.Fatal(err)
 			}
@@ -137,8 +138,8 @@ func TestMapAllPortsSingleInterface(t *testing.T) {
 			hosts = append(hosts, host)
 		}
 
-		if _, err := Map(srcAddr1, dstIp1, portallocator.BeginPortRange); err == nil {
-			t.Fatalf("Port %d should be bound but is not", portallocator.BeginPortRange)
+		if _, err := Map(srcAddr1, dstIp1, start); err == nil {
+			t.Fatalf("Port %d should be bound but is not", start)
 		}
 
 		for _, val := range hosts {

+ 4 - 3
docs/man/docker-run.1.md

@@ -258,9 +258,10 @@ and foreground Docker containers.
    When set to true publish all exposed ports to the host interfaces. The
 default is false. If the operator uses -P (or -p) then Docker will make the
 exposed port accessible on the host and the ports will be available to any
-client that can reach the host. When using -P, Docker will bind the exposed
-ports to a random port on the host between 49153 and 65535. To find the
-mapping between the host ports and the exposed ports, use **docker port**.
+client that can reach the host. When using -P, Docker will bind any exposed
+port to a random port on the host within an *ephemeral port range* defined by
+`/proc/sys/net/ipv4/ip_local_port_range`. To find the mapping between the host
+ports and the exposed ports, use `docker port`.
 
 **-p**, **--publish**=[]
    Publish a container's port, or range of ports, to the host.

+ 12 - 11
docs/sources/articles/networking.md

@@ -385,17 +385,18 @@ to provide special options when invoking `docker run`.  These options
 are covered in more detail in the [Docker User Guide](/userguide/dockerlinks)
 page.  There are two approaches.
 
-First, you can supply `-P` or `--publish-all=true|false` to `docker run`
-which is a blanket operation that identifies every port with an `EXPOSE`
-line in the image's `Dockerfile` and maps it to a host port somewhere in
-the range 49153–65535.  This tends to be a bit inconvenient, since you
-then have to run other `docker` sub-commands to learn which external
-port a given service was mapped to.
-
-More convenient is the `-p SPEC` or `--publish=SPEC` option which lets
-you be explicit about exactly which external port on the Docker server —
-which can be any port at all, not just those in the 49153-65535 block —
-you want mapped to which port in the container.
+First, you can supply `-P` or `--publish-all=true|false` to `docker run` which
+is a blanket operation that identifies every port with an `EXPOSE` line in the
+image's `Dockerfile` or `--expose <port>` commandline flag and maps it to a
+host port somewhere within an *ephemeral port range*. The `docker port` command
+then needs to be used to inspect created mapping. The *ephemeral port range* is
+configured by `/proc/sys/net/ipv4/ip_local_port_range` kernel parameter,
+typically ranging from 32768 to 61000.
+
+Mapping can be specified explicitly using `-p SPEC` or `--publish=SPEC` option.
+It allows you to particularize which port on docker server - which can be any
+port at all, not just one within the *ephemeral port range* — you want mapped
+to which port in the container.
 
 Either way, you should be able to peek at what Docker has accomplished
 in your network stack by examining your NAT tables.

+ 5 - 4
docs/sources/reference/run.md

@@ -651,10 +651,11 @@ developer, the operator has three choices: start the server container
 with `-P` or `-p,` or start the client container with `--link`.
 
 If the operator uses `-P` or `-p` then Docker will make the exposed port
-accessible on the host and the ports will be available to any client
-that can reach the host. When using `-P`, Docker will bind the exposed 
-ports to a random port on the host between 49153 and 65535. To find the
-mapping between the host ports and the exposed ports, use `docker port`.
+accessible on the host and the ports will be available to any client that can
+reach the host. When using `-P`, Docker will bind the exposed port to a random
+port on the host within an *ephemeral port range* defined by
+`/proc/sys/net/ipv4/ip_local_port_range`. To find the mapping between the host
+ports and the exposed ports, use `docker port`.
 
 If the operator uses `--link` when starting the new client container,
 then the client container can access the exposed port via a private

+ 4 - 4
docs/sources/userguide/dockerlinks.md

@@ -25,10 +25,10 @@ container that ran a Python Flask application:
 > Docker can have a variety of network configurations. You can see more
 > information on Docker networking [here](/articles/networking/).
 
-When that container was created, the `-P` flag was used to automatically map any
-network ports inside it to a random high port from the range 49153
-to 65535 on our Docker host.  Next, when `docker ps` was run, you saw that
-port 5000 in the container was bound to port 49155 on the host.
+When that container was created, the `-P` flag was used to automatically map
+any network port inside it to a random high port within an *ephemeral port
+range* on your Docker host. Next, when `docker ps` was run, you saw that port
+5000 in the container was bound to port 49155 on the host.
 
     $ sudo docker ps nostalgic_morse
     CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES

+ 5 - 5
docs/sources/userguide/usingdocker.md

@@ -154,11 +154,11 @@ ports exposed in our image to our host.
 In this case Docker has exposed port 5000 (the default Python Flask
 port) on port 49155.
 
-Network port bindings are very configurable in Docker. In our last
-example the `-P` flag is a shortcut for `-p 5000` that maps port 5000
-inside the container to a high port (from the range 49153 to 65535) on
-the local Docker host. We can also bind Docker containers to specific
-ports using the `-p` flag, for example:
+Network port bindings are very configurable in Docker. In our last example the
+`-P` flag is a shortcut for `-p 5000` that maps port 5000 inside the container
+to a high port (from *ephemeral port range* which typically ranges from 32768
+to 61000) on the local Docker host. We can also bind Docker containers to
+specific ports using the `-p` flag, for example:
 
     $ sudo docker run -d -p 5000:5000 training/webapp python app.py