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>
This commit is contained in:
Michal Minar 2015-01-21 13:40:59 +01:00
parent 1ff904e045
commit 0eb3544c43
8 changed files with 90 additions and 48 deletions

View File

@ -3,8 +3,22 @@ package portallocator
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"strings"
"sync" "sync"
log "github.com/Sirupsen/logrus"
)
const (
DefaultPortRangeStart = 49153
DefaultPortRangeEnd = 65535
)
var (
beginPortRange = DefaultPortRangeStart
endPortRange = DefaultPortRangeEnd
) )
type portMap struct { type portMap struct {
@ -15,7 +29,7 @@ type portMap struct {
func newPortMap() *portMap { func newPortMap() *portMap {
return &portMap{ return &portMap{
p: map[int]struct{}{}, p: map[int]struct{}{},
last: EndPortRange, last: endPortRange,
} }
} }
@ -30,11 +44,6 @@ func newProtoMap() protoMap {
type ipMapping map[string]protoMap type ipMapping map[string]protoMap
const (
BeginPortRange = 49153
EndPortRange = 65535
)
var ( var (
ErrAllPortsAllocated = errors.New("all ports are allocated") ErrAllPortsAllocated = errors.New("all ports are allocated")
ErrUnknownProtocol = errors.New("unknown protocol") ErrUnknownProtocol = errors.New("unknown protocol")
@ -59,6 +68,29 @@ func NewErrPortAlreadyAllocated(ip string, port int) ErrPortAlreadyAllocated {
} }
} }
func init() {
const param = "/proc/sys/net/ipv4/ip_local_port_range"
if line, err := ioutil.ReadFile(param); err != nil {
log.Errorf("Failed to read %s kernel parameter: %s", param, err.Error())
} else {
var start, end int
if n, err := fmt.Fscanf(strings.NewReader(string(line)), "%d\t%d", &start, &end); 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)
} else {
beginPortRange = start
endPortRange = end
}
}
}
func GetPortRange() (int, int) {
return beginPortRange, endPortRange
}
func (e ErrPortAlreadyAllocated) IP() string { func (e ErrPortAlreadyAllocated) IP() string {
return e.ip return e.ip
} }
@ -137,10 +169,11 @@ func ReleaseAll() error {
func (pm *portMap) findPort() (int, error) { func (pm *portMap) findPort() (int, error) {
port := pm.last port := pm.last
for i := 0; i <= EndPortRange-BeginPortRange; i++ { start, end := GetPortRange()
for i := 0; i <= end-start; i++ {
port++ port++
if port > EndPortRange { if port > end {
port = BeginPortRange port = start
} }
if _, ok := pm.p[port]; !ok { if _, ok := pm.p[port]; !ok {

View File

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

View File

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

View File

@ -244,9 +244,10 @@ and foreground Docker containers.
When set to true publish all exposed ports to the host interfaces. The 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 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 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 client that can reach the host. When using -P, Docker will bind any exposed
ports to a random port on the host between 49153 and 65535. To find the port to a random port on the host within an *ephemeral port range* defined by
mapping between the host ports and the exposed ports, use **docker port**. `/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**=[] **-p**, **--publish**=[]
Publish a container's port, or range of ports, to the host. Publish a container's port, or range of ports, to the host.

View File

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

View File

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

View File

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

View File

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