mirror of
https://github.com/moby/moby.git
synced 2026-06-30 19:58:03 +00:00
Merge pull request #52818 from mat007/portallocator-reserved-ports
daemon/libnetwork/portallocator: skip kernel-reserved ports
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
@@ -41,6 +42,7 @@ type (
|
||||
ipMap ipMapping
|
||||
begin int
|
||||
end int
|
||||
reserved map[uint16]struct{}
|
||||
}
|
||||
portRange struct {
|
||||
begin int
|
||||
@@ -80,6 +82,7 @@ func newInstance() *PortAllocator {
|
||||
defaultIP: net.IPv4zero,
|
||||
begin: begin,
|
||||
end: end,
|
||||
reserved: reservedPorts(begin, end),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +95,15 @@ func dynamicPortRange() (start, end int) {
|
||||
return begin, end
|
||||
}
|
||||
|
||||
func reservedPorts(begin, end int) map[uint16]struct{} {
|
||||
reserved, err := getReservedPorts(begin, end)
|
||||
if err != nil {
|
||||
log.G(context.TODO()).WithError(err).Info("ignoring system reserved ports")
|
||||
return nil
|
||||
}
|
||||
return reserved
|
||||
}
|
||||
|
||||
func makeIpMapping(begin, end int) ipMapping {
|
||||
return ipMapping{
|
||||
netip.IPv4Unspecified(): makeProtoMap(begin, end),
|
||||
@@ -224,6 +236,12 @@ func (p *PortAllocator) RequestPortsInRange(ips []net.IP, proto string, portStar
|
||||
pRanges[addr] = pMap.portMap.getPortRange(portStart, portEnd)
|
||||
}
|
||||
|
||||
// Requests for the default ephemeral range are automatic assignments, so
|
||||
// the ports the kernel excludes from its own automatic assignments must
|
||||
// be skipped. Requests for a specific range are honored as-is, like
|
||||
// requests for a specific port.
|
||||
skipReserved := portStart == 0 && portEnd == 0
|
||||
|
||||
// Arbitrarily starting after the last port allocated for the first address, search
|
||||
// for a port that's available in all ranges.
|
||||
firstAddr, _ := netip.AddrFromSlice(ips[0])
|
||||
@@ -235,6 +253,12 @@ func (p *PortAllocator) RequestPortsInRange(ips []net.IP, proto string, portStar
|
||||
port = firstRange.begin
|
||||
}
|
||||
|
||||
if skipReserved && port >= 0 && port <= math.MaxUint16 {
|
||||
if _, ok := p.reserved[uint16(port)]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
portAlreadyAllocated := func() bool {
|
||||
for _, pMap := range ipToPortMapRef {
|
||||
if _, ok := pMap.portMap.p[port]; ok {
|
||||
|
||||
@@ -39,3 +39,7 @@ func getDynamicPortRange() (start int, end int, _ error) {
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func getReservedPorts(_, _ int) (map[uint16]struct{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getDynamicPortRange() (start int, end int, _ error) {
|
||||
@@ -23,3 +25,53 @@ func getDynamicPortRange() (start int, end int, _ error) {
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
// getReservedPorts returns the ports the kernel excludes from automatic
|
||||
// assignment, read from /proc/sys/net/ipv4/ip_local_reserved_ports
|
||||
// (e.g. "8080,9148" or "30000-32767"). Reserved ports outside the
|
||||
// begin-end allocation range are dropped, the allocator never picks
|
||||
// them anyway.
|
||||
func getReservedPorts(begin, end int) (map[uint16]struct{}, error) {
|
||||
const reservedPortsKernelParam = "/proc/sys/net/ipv4/ip_local_reserved_ports"
|
||||
data, err := os.ReadFile(reservedPortsKernelParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reserved, err := parseReservedPorts(string(data), begin, end)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("port allocator - failed to parse system reserved ports from %s: %v", reservedPortsKernelParam, err)
|
||||
}
|
||||
return reserved, nil
|
||||
}
|
||||
|
||||
// parseReservedPorts parses a port list in the kernel format, a
|
||||
// comma-separated list of single ports ("1080") and port ranges
|
||||
// ("30000-32767"), keeping only ports within begin-end.
|
||||
func parseReservedPorts(list string, begin, end int) (map[uint16]struct{}, error) {
|
||||
list = strings.TrimSpace(list)
|
||||
if list == "" {
|
||||
return nil, nil
|
||||
}
|
||||
reserved := map[uint16]struct{}{}
|
||||
for entry := range strings.SplitSeq(list, ",") {
|
||||
first, last, isRange := strings.Cut(entry, "-")
|
||||
entryBegin, err := strconv.ParseUint(first, 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port %q: %v", entry, err)
|
||||
}
|
||||
entryEnd := entryBegin
|
||||
if isRange {
|
||||
entryEnd, err = strconv.ParseUint(last, 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port range %q: %v", entry, err)
|
||||
}
|
||||
if entryEnd < entryBegin {
|
||||
return nil, fmt.Errorf("invalid port range %q", entry)
|
||||
}
|
||||
}
|
||||
for port := max(int(entryBegin), begin); port <= min(int(entryEnd), end); port++ {
|
||||
reserved[uint16(port)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return reserved, nil
|
||||
}
|
||||
|
||||
100
daemon/libnetwork/portallocator/portallocator_linux_test.go
Normal file
100
daemon/libnetwork/portallocator/portallocator_linux_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package portallocator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestParseReservedPorts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
list string
|
||||
begin, end int
|
||||
expected map[uint16]struct{}
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
list: "\n",
|
||||
},
|
||||
{
|
||||
name: "single port",
|
||||
list: "8080\n",
|
||||
expected: map[uint16]struct{}{8080: {}},
|
||||
},
|
||||
{
|
||||
name: "multiple ports",
|
||||
list: "8080,9148",
|
||||
expected: map[uint16]struct{}{8080: {}, 9148: {}},
|
||||
},
|
||||
{
|
||||
name: "port range",
|
||||
list: "30000-30002",
|
||||
expected: map[uint16]struct{}{30000: {}, 30001: {}, 30002: {}},
|
||||
},
|
||||
{
|
||||
name: "mixed",
|
||||
list: "8080,30000-30001,9148",
|
||||
expected: map[uint16]struct{}{8080: {}, 30000: {}, 30001: {}, 9148: {}},
|
||||
},
|
||||
{
|
||||
name: "port outside allocation range",
|
||||
list: "8080,30000",
|
||||
begin: 20000,
|
||||
end: 40000,
|
||||
// 8080 is below the allocation range, the allocator never picks it.
|
||||
expected: map[uint16]struct{}{30000: {}},
|
||||
},
|
||||
{
|
||||
name: "range clamped to allocation range",
|
||||
list: "29998-30001",
|
||||
begin: 30000,
|
||||
end: 40000,
|
||||
expected: map[uint16]struct{}{30000: {}, 30001: {}},
|
||||
},
|
||||
{
|
||||
name: "range fully outside allocation range",
|
||||
list: "8080-8082",
|
||||
begin: 30000,
|
||||
end: 40000,
|
||||
expected: map[uint16]struct{}{},
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
list: "8080,abc",
|
||||
expectedErr: `invalid port "abc"`,
|
||||
},
|
||||
{
|
||||
name: "invalid range",
|
||||
list: "30002-30000",
|
||||
expectedErr: `invalid port range "30002-30000"`,
|
||||
},
|
||||
{
|
||||
name: "incomplete range",
|
||||
list: "30000-",
|
||||
expectedErr: `invalid port range "30000-"`,
|
||||
},
|
||||
{
|
||||
name: "out of range port",
|
||||
list: "65536",
|
||||
expectedErr: `invalid port "65536"`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
begin, end := tc.begin, tc.end
|
||||
if begin == 0 && end == 0 {
|
||||
begin, end = 1, 65535
|
||||
}
|
||||
reserved, err := parseReservedPorts(tc.list, begin, end)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
|
||||
return
|
||||
}
|
||||
assert.Check(t, err)
|
||||
assert.Check(t, is.DeepEqual(reserved, tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -356,3 +356,43 @@ func TestMixUnspecAndSpecificAddrs(t *testing.T) {
|
||||
assert.Check(t, err)
|
||||
assert.Check(t, is.Equal(port, p.begin+1))
|
||||
}
|
||||
|
||||
func TestRequestNewPortSkipsReservedPorts(t *testing.T) {
|
||||
p := newInstance()
|
||||
p.reserved = map[uint16]struct{}{uint16(p.begin): {}, uint16(p.begin + 1): {}}
|
||||
|
||||
port, err := p.RequestPort(net.IPv4zero, "tcp", 0)
|
||||
assert.Check(t, err)
|
||||
assert.Check(t, is.Equal(port, p.begin+2))
|
||||
}
|
||||
|
||||
func TestRequestSpecificReservedPort(t *testing.T) {
|
||||
p := newInstance()
|
||||
p.reserved = map[uint16]struct{}{10000: {}}
|
||||
|
||||
// Explicit port allocation is unchanged by reserved ports.
|
||||
port, err := p.RequestPort(net.IPv4zero, "tcp", 10000)
|
||||
assert.Check(t, err)
|
||||
assert.Check(t, is.Equal(port, 10000))
|
||||
}
|
||||
|
||||
func TestRequestPortInRangeWithReservedPort(t *testing.T) {
|
||||
p := newInstance()
|
||||
p.reserved = map[uint16]struct{}{20000: {}}
|
||||
|
||||
// Explicit range allocation is unchanged by reserved ports.
|
||||
port, err := p.RequestPortInRange(net.IPv4zero, "tcp", 20000, 20004)
|
||||
assert.Check(t, err)
|
||||
assert.Check(t, is.Equal(port, 20000))
|
||||
}
|
||||
|
||||
func TestRequestNewPortAllReserved(t *testing.T) {
|
||||
p := newInstance()
|
||||
p.reserved = map[uint16]struct{}{}
|
||||
for i := p.begin; i <= p.end; i++ {
|
||||
p.reserved[uint16(i)] = struct{}{}
|
||||
}
|
||||
|
||||
_, err := p.RequestPort(net.IPv4zero, "tcp", 0)
|
||||
assert.Check(t, is.ErrorIs(err, errAllPortsAllocated))
|
||||
}
|
||||
|
||||
@@ -10,3 +10,7 @@ const (
|
||||
func getDynamicPortRange() (start int, end int, _ error) {
|
||||
return defaultPortRangeStart, defaultPortRangeEnd, nil
|
||||
}
|
||||
|
||||
func getReservedPorts(_, _ int) (map[uint16]struct{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user