Browse code

Use system's ephemeral port range for port allocation

Read `/proc/sys/net/ipv4/ip_local_port_range` kernel parameter to obtain
ephemeral port range that now sets the boundaries of port allocator
which finds free host ports for those exported by containers.

Signed-off-by: Michal Minar <miminar@redhat.com>

Michal Minar authored on 2015/01/21 21:40:59
Showing 8 changed files
... ...
@@ -3,8 +3,22 @@ package portallocator
3 3
 import (
4 4
 	"errors"
5 5
 	"fmt"
6
+	"io/ioutil"
6 7
 	"net"
8
+	"strings"
7 9
 	"sync"
10
+
11
+	log "github.com/Sirupsen/logrus"
12
+)
13
+
14
+const (
15
+	DefaultPortRangeStart = 49153
16
+	DefaultPortRangeEnd   = 65535
17
+)
18
+
19
+var (
20
+	beginPortRange = DefaultPortRangeStart
21
+	endPortRange   = DefaultPortRangeEnd
8 22
 )
9 23
 
10 24
 type portMap struct {
... ...
@@ -15,7 +29,7 @@ type portMap struct {
15 15
 func newPortMap() *portMap {
16 16
 	return &portMap{
17 17
 		p:    map[int]struct{}{},
18
-		last: EndPortRange,
18
+		last: endPortRange,
19 19
 	}
20 20
 }
21 21
 
... ...
@@ -30,11 +44,6 @@ func newProtoMap() protoMap {
30 30
 
31 31
 type ipMapping map[string]protoMap
32 32
 
33
-const (
34
-	BeginPortRange = 49153
35
-	EndPortRange   = 65535
36
-)
37
-
38 33
 var (
39 34
 	ErrAllPortsAllocated = errors.New("all ports are allocated")
40 35
 	ErrUnknownProtocol   = errors.New("unknown protocol")
... ...
@@ -59,6 +68,29 @@ func NewErrPortAlreadyAllocated(ip string, port int) ErrPortAlreadyAllocated {
59 59
 	}
60 60
 }
61 61
 
62
+func init() {
63
+	const param = "/proc/sys/net/ipv4/ip_local_port_range"
64
+
65
+	if line, err := ioutil.ReadFile(param); err != nil {
66
+		log.Errorf("Failed to read %s kernel parameter: %s", param, err.Error())
67
+	} else {
68
+		var start, end int
69
+		if n, err := fmt.Fscanf(strings.NewReader(string(line)), "%d\t%d", &start, &end); n != 2 || err != nil {
70
+			if err == nil {
71
+				err = fmt.Errorf("unexpected count of parsed numbers (%d)", n)
72
+			}
73
+			log.Errorf("Failed to parse port range from %s: %v", param, err)
74
+		} else {
75
+			beginPortRange = start
76
+			endPortRange = end
77
+		}
78
+	}
79
+}
80
+
81
+func GetPortRange() (int, int) {
82
+	return beginPortRange, endPortRange
83
+}
84
+
62 85
 func (e ErrPortAlreadyAllocated) IP() string {
63 86
 	return e.ip
64 87
 }
... ...
@@ -137,10 +169,11 @@ func ReleaseAll() error {
137 137
 
138 138
 func (pm *portMap) findPort() (int, error) {
139 139
 	port := pm.last
140
-	for i := 0; i <= EndPortRange-BeginPortRange; i++ {
140
+	start, end := GetPortRange()
141
+	for i := 0; i <= end-start; i++ {
141 142
 		port++
142
-		if port > EndPortRange {
143
-			port = BeginPortRange
143
+		if port > end {
144
+			port = start
144 145
 		}
145 146
 
146 147
 		if _, ok := pm.p[port]; !ok {
... ...
@@ -5,6 +5,11 @@ import (
5 5
 	"testing"
6 6
 )
7 7
 
8
+func init() {
9
+	beginPortRange = DefaultPortRangeStart
10
+	endPortRange = DefaultPortRangeStart + 500
11
+}
12
+
8 13
 func reset() {
9 14
 	ReleaseAll()
10 15
 }
... ...
@@ -17,7 +22,7 @@ func TestRequestNewPort(t *testing.T) {
17 17
 		t.Fatal(err)
18 18
 	}
19 19
 
20
-	if expected := BeginPortRange; port != expected {
20
+	if expected := beginPortRange; port != expected {
21 21
 		t.Fatalf("Expected port %d got %d", expected, port)
22 22
 	}
23 23
 }
... ...
@@ -102,13 +107,13 @@ func TestUnknowProtocol(t *testing.T) {
102 102
 func TestAllocateAllPorts(t *testing.T) {
103 103
 	defer reset()
104 104
 
105
-	for i := 0; i <= EndPortRange-BeginPortRange; i++ {
105
+	for i := 0; i <= endPortRange-beginPortRange; i++ {
106 106
 		port, err := RequestPort(defaultIP, "tcp", 0)
107 107
 		if err != nil {
108 108
 			t.Fatal(err)
109 109
 		}
110 110
 
111
-		if expected := BeginPortRange + i; port != expected {
111
+		if expected := beginPortRange + i; port != expected {
112 112
 			t.Fatalf("Expected port %d got %d", expected, port)
113 113
 		}
114 114
 	}
... ...
@@ -123,7 +128,7 @@ func TestAllocateAllPorts(t *testing.T) {
123 123
 	}
124 124
 
125 125
 	// release a port in the middle and ensure we get another tcp port
126
-	port := BeginPortRange + 5
126
+	port := beginPortRange + 5
127 127
 	if err := ReleasePort(defaultIP, "tcp", port); err != nil {
128 128
 		t.Fatal(err)
129 129
 	}
... ...
@@ -153,13 +158,13 @@ func BenchmarkAllocatePorts(b *testing.B) {
153 153
 	defer reset()
154 154
 
155 155
 	for i := 0; i < b.N; i++ {
156
-		for i := 0; i <= EndPortRange-BeginPortRange; i++ {
156
+		for i := 0; i <= endPortRange-beginPortRange; i++ {
157 157
 			port, err := RequestPort(defaultIP, "tcp", 0)
158 158
 			if err != nil {
159 159
 				b.Fatal(err)
160 160
 			}
161 161
 
162
-			if expected := BeginPortRange + i; port != expected {
162
+			if expected := beginPortRange + i; port != expected {
163 163
 				b.Fatalf("Expected port %d got %d", expected, port)
164 164
 			}
165 165
 		}
... ...
@@ -231,15 +236,15 @@ func TestPortAllocation(t *testing.T) {
231 231
 func TestNoDuplicateBPR(t *testing.T) {
232 232
 	defer reset()
233 233
 
234
-	if port, err := RequestPort(defaultIP, "tcp", BeginPortRange); err != nil {
234
+	if port, err := RequestPort(defaultIP, "tcp", beginPortRange); err != nil {
235 235
 		t.Fatal(err)
236
-	} else if port != BeginPortRange {
237
-		t.Fatalf("Expected port %d got %d", BeginPortRange, port)
236
+	} else if port != beginPortRange {
237
+		t.Fatalf("Expected port %d got %d", beginPortRange, port)
238 238
 	}
239 239
 
240 240
 	if port, err := RequestPort(defaultIP, "tcp", 0); err != nil {
241 241
 		t.Fatal(err)
242
-	} else if port == BeginPortRange {
242
+	} else if port == beginPortRange {
243 243
 		t.Fatalf("Acquire(0) allocated the same port twice: %d", port)
244 244
 	}
245 245
 }
... ...
@@ -129,7 +129,8 @@ func TestMapAllPortsSingleInterface(t *testing.T) {
129 129
 	}()
130 130
 
131 131
 	for i := 0; i < 10; i++ {
132
-		for i := portallocator.BeginPortRange; i < portallocator.EndPortRange; i++ {
132
+		start, end := portallocator.GetPortRange()
133
+		for i := start; i < end; i++ {
133 134
 			if host, err = Map(srcAddr1, dstIp1, 0); err != nil {
134 135
 				t.Fatal(err)
135 136
 			}
... ...
@@ -137,8 +138,8 @@ func TestMapAllPortsSingleInterface(t *testing.T) {
137 137
 			hosts = append(hosts, host)
138 138
 		}
139 139
 
140
-		if _, err := Map(srcAddr1, dstIp1, portallocator.BeginPortRange); err == nil {
141
-			t.Fatalf("Port %d should be bound but is not", portallocator.BeginPortRange)
140
+		if _, err := Map(srcAddr1, dstIp1, start); err == nil {
141
+			t.Fatalf("Port %d should be bound but is not", start)
142 142
 		}
143 143
 
144 144
 		for _, val := range hosts {
... ...
@@ -244,9 +244,10 @@ and foreground Docker containers.
244 244
    When set to true publish all exposed ports to the host interfaces. The
245 245
 default is false. If the operator uses -P (or -p) then Docker will make the
246 246
 exposed port accessible on the host and the ports will be available to any
247
-client that can reach the host. When using -P, Docker will bind the exposed
248
-ports to a random port on the host between 49153 and 65535. To find the
249
-mapping between the host ports and the exposed ports, use **docker port**.
247
+client that can reach the host. When using -P, Docker will bind any exposed
248
+port to a random port on the host within an *ephemeral port range* defined by
249
+`/proc/sys/net/ipv4/ip_local_port_range`. To find the mapping between the host
250
+ports and the exposed ports, use `docker port`.
250 251
 
251 252
 **-p**, **--publish**=[]
252 253
    Publish a container's port, or range of ports, to the host.
... ...
@@ -370,17 +370,18 @@ to provide special options when invoking `docker run`.  These options
370 370
 are covered in more detail in the [Docker User Guide](/userguide/dockerlinks)
371 371
 page.  There are two approaches.
372 372
 
373
-First, you can supply `-P` or `--publish-all=true|false` to `docker run`
374
-which is a blanket operation that identifies every port with an `EXPOSE`
375
-line in the image's `Dockerfile` and maps it to a host port somewhere in
376
-the range 49153–65535.  This tends to be a bit inconvenient, since you
377
-then have to run other `docker` sub-commands to learn which external
378
-port a given service was mapped to.
379
-
380
-More convenient is the `-p SPEC` or `--publish=SPEC` option which lets
381
-you be explicit about exactly which external port on the Docker server —
382
-which can be any port at all, not just those in the 49153-65535 block —
383
-you want mapped to which port in the container.
373
+First, you can supply `-P` or `--publish-all=true|false` to `docker run` which
374
+is a blanket operation that identifies every port with an `EXPOSE` line in the
375
+image's `Dockerfile` or `--expose <port>` commandline flag and maps it to a
376
+host port somewhere within an *ephemeral port range*. The `docker port` command
377
+then needs to be used to inspect created mapping. The *ephemeral port range* is
378
+configured by `/proc/sys/net/ipv4/ip_local_port_range` kernel parameter,
379
+typically ranging from 32768 to 61000.
380
+
381
+Mapping can be specified explicitly using `-p SPEC` or `--publish=SPEC` option.
382
+It allows you to particularize which port on docker server - which can be any
383
+port at all, not just one within the *ephemeral port range* — you want mapped
384
+to which port in the container.
384 385
 
385 386
 Either way, you should be able to peek at what Docker has accomplished
386 387
 in your network stack by examining your NAT tables.
... ...
@@ -635,10 +635,11 @@ developer, the operator has three choices: start the server container
635 635
 with `-P` or `-p,` or start the client container with `--link`.
636 636
 
637 637
 If the operator uses `-P` or `-p` then Docker will make the exposed port
638
-accessible on the host and the ports will be available to any client
639
-that can reach the host. When using `-P`, Docker will bind the exposed 
640
-ports to a random port on the host between 49153 and 65535. To find the
641
-mapping between the host ports and the exposed ports, use `docker port`.
638
+accessible on the host and the ports will be available to any client that can
639
+reach the host. When using `-P`, Docker will bind the exposed port to a random
640
+port on the host within an *ephemeral port range* defined by
641
+`/proc/sys/net/ipv4/ip_local_port_range`. To find the mapping between the host
642
+ports and the exposed ports, use `docker port`.
642 643
 
643 644
 If the operator uses `--link` when starting the new client container,
644 645
 then the client container can access the exposed port via a private
... ...
@@ -25,10 +25,10 @@ container that ran a Python Flask application:
25 25
 > Docker can have a variety of network configurations. You can see more
26 26
 > information on Docker networking [here](/articles/networking/).
27 27
 
28
-When that container was created, the `-P` flag was used to automatically map any
29
-network ports inside it to a random high port from the range 49153
30
-to 65535 on our Docker host.  Next, when `docker ps` was run, you saw that
31
-port 5000 in the container was bound to port 49155 on the host.
28
+When that container was created, the `-P` flag was used to automatically map
29
+any network port inside it to a random high port within an *ephemeral port
30
+range* on your Docker host. Next, when `docker ps` was run, you saw that port
31
+5000 in the container was bound to port 49155 on the host.
32 32
 
33 33
     $ sudo docker ps nostalgic_morse
34 34
     CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES
... ...
@@ -154,11 +154,11 @@ ports exposed in our image to our host.
154 154
 In this case Docker has exposed port 5000 (the default Python Flask
155 155
 port) on port 49155.
156 156
 
157
-Network port bindings are very configurable in Docker. In our last
158
-example the `-P` flag is a shortcut for `-p 5000` that maps port 5000
159
-inside the container to a high port (from the range 49153 to 65535) on
160
-the local Docker host. We can also bind Docker containers to specific
161
-ports using the `-p` flag, for example:
157
+Network port bindings are very configurable in Docker. In our last example the
158
+`-P` flag is a shortcut for `-p 5000` that maps port 5000 inside the container
159
+to a high port (from *ephemeral port range* which typically ranges from 32768
160
+to 61000) on the local Docker host. We can also bind Docker containers to
161
+specific ports using the `-p` flag, for example:
162 162
 
163 163
     $ sudo docker run -d -p 5000:5000 training/webapp python app.py
164 164