diff --git a/api/go.mod b/api/go.mod index 55d4bb1f2c..6e9849ac9e 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,7 +3,6 @@ module github.com/moby/moby/api go 1.23.0 require ( - github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/google/go-cmp v0.7.0 github.com/moby/docker-image-spec v1.3.1 diff --git a/api/go.sum b/api/go.sum index 40215a9daf..e2053448ed 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,5 +1,3 @@ -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/api/types/container/nat_aliases.go b/api/types/container/nat_aliases.go deleted file mode 100644 index 470f15655c..0000000000 --- a/api/types/container/nat_aliases.go +++ /dev/null @@ -1,24 +0,0 @@ -package container - -import "github.com/docker/go-connections/nat" - -// PortRangeProto is a string containing port number and protocol in the format "80/tcp", -// or a port range and protocol in the format "80-83/tcp". -// -// It is currently an alias for [nat.Port] but may become a concrete type in a future release. -type PortRangeProto = nat.Port - -// PortSet is a collection of structs indexed by [HostPort]. -// -// It is currently an alias for [nat.PortSet] but may become a concrete type in a future release. -type PortSet = nat.PortSet - -// PortBinding represents a binding between a Host IP address and a [HostPort]. -// -// It is currently an alias for [nat.PortBinding] but may become a concrete type in a future release. -type PortBinding = nat.PortBinding - -// PortMap is a collection of [PortBinding] indexed by [HostPort]. -// -// It is currently an alias for [nat.PortMap] but may become a concrete type in a future release. -type PortMap = nat.PortMap diff --git a/api/types/container/network.go b/api/types/container/network.go new file mode 100644 index 0000000000..b22329033a --- /dev/null +++ b/api/types/container/network.go @@ -0,0 +1,345 @@ +package container + +import ( + "errors" + "fmt" + "iter" + "strconv" + "strings" + "unique" +) + +// NetworkProtocol represents a network protocol for a port. +type NetworkProtocol string + +const ( + TCP NetworkProtocol = "tcp" + UDP NetworkProtocol = "udp" + SCTP NetworkProtocol = "sctp" +) + +// Sentinel port proto value for zero Port and PortRange values. +var protoZero unique.Handle[NetworkProtocol] + +// Port is a type representing a single port number and protocol in the format "/[]". +// +// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value. +type Port struct { + num uint16 + proto unique.Handle[NetworkProtocol] +} + +// ParsePort parses s as a [Port]. +// +// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent. +// If a port number is provided, but no protocol, the default ("tcp") protocol is returned. +func ParsePort(s string) (Port, error) { + if s == "" { + return Port{}, errors.New("invalid port: value is empty") + } + + port, proto, _ := strings.Cut(s, "/") + + portNum, err := parsePortNumber(port) + if err != nil { + return Port{}, fmt.Errorf("invalid port '%s': %w", port, err) + } + + normalizedPortProto := normalizePortProto(proto) + return Port{num: portNum, proto: normalizedPortProto}, nil +} + +// MustParsePort calls [ParsePort](s) and panics on error. +// +// It is intended for use in tests with hard-coded strings. +func MustParsePort(s string) Port { + p, err := ParsePort(s) + if err != nil { + panic(err) + } + return p +} + +// PortFrom returns a [Port] with the given number and protocol. +// +// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false. +func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) { + if proto == "" { + return Port{}, false + } + normalized := normalizePortProto(string(proto)) + return Port{num: num, proto: normalized}, true +} + +// Num returns p's port number. +func (p Port) Num() uint16 { + return p.num +} + +// Proto returns p's network protocol. +func (p Port) Proto() NetworkProtocol { + return p.proto.Value() +} + +// IsZero reports whether p is the zero value. +func (p Port) IsZero() bool { + return p.proto == protoZero +} + +// IsValid reports whether p is an initialized valid port (not the zero value). +func (p Port) IsValid() bool { + return p.proto != protoZero +} + +// String returns a string representation of the port in the format "/". +// If the port is the zero value, it returns "invalid port". +func (p Port) String() string { + switch p.proto { + case protoZero: + return "invalid port" + default: + return string(p.AppendTo(nil)) + } +} + +// AppendText implements [encoding.TextAppender] interface. +// It is the same as [Port.AppendTo] but returns an error to satisfy the interface. +func (p Port) AppendText(b []byte) ([]byte, error) { + return p.AppendTo(b), nil +} + +// AppendTo appends a text encoding of p to b and returns the extended buffer. +func (p Port) AppendTo(b []byte) []byte { + if p.IsZero() { + return b + } + return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value()) +} + +// MarshalText implements [encoding.TextMarshaler] interface. +func (p Port) MarshalText() ([]byte, error) { + return p.AppendText(nil) +} + +// UnmarshalText implements [encoding.TextUnmarshaler] interface. +func (p *Port) UnmarshalText(text []byte) error { + if len(text) == 0 { + *p = Port{} + return nil + } + + port, err := ParsePort(string(text)) + if err != nil { + return err + } + + *p = port + return nil +} + +// Range returns a [PortRange] representing the single port. +func (p Port) Range() PortRange { + return PortRange{start: p.num, end: p.num, proto: p.proto} +} + +// PortSet is a collection of structs indexed by [Port]. +type PortSet = map[Port]struct{} + +// PortBinding represents a binding between a Host IP address and a Host Port. +type PortBinding struct { + // HostIP is the host IP Address + HostIP string `json:"HostIp"` + // HostPort is the host port number + HostPort string `json:"HostPort"` +} + +// PortMap is a collection of [PortBinding] indexed by [Port]. +type PortMap = map[Port][]PortBinding + +// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp". +// +// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value. +type PortRange struct { + start uint16 + end uint16 + proto unique.Handle[NetworkProtocol] +} + +// ParsePortRange parses s as a [PortRange]. +// +// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent. +// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned. +func ParsePortRange(s string) (PortRange, error) { + if s == "" { + return PortRange{}, errors.New("invalid port range: value is empty") + } + + portRange, proto, _ := strings.Cut(s, "/") + + start, end, ok := strings.Cut(portRange, "-") + startVal, err := parsePortNumber(start) + if err != nil { + return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err) + } + + portProto := normalizePortProto(proto) + + if !ok || start == end { + return PortRange{start: startVal, end: startVal, proto: portProto}, nil + } + + endVal, err := parsePortNumber(end) + if err != nil { + return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err) + } + if endVal < startVal { + return PortRange{}, errors.New("invalid port range: " + s) + } + return PortRange{start: startVal, end: endVal, proto: portProto}, nil +} + +// MustParsePortRange calls [ParsePortRange](s) and panics on error. +// It is intended for use in tests with hard-coded strings. +func MustParsePortRange(s string) PortRange { + pr, err := ParsePortRange(s) + if err != nil { + panic(err) + } + return pr +} + +// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol. +// +// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false. +func PortRangeFrom(start, end uint16, proto NetworkProtocol) (pr PortRange, ok bool) { + if end < start || proto == "" { + return PortRange{}, false + } + normalized := normalizePortProto(string(proto)) + return PortRange{start: start, end: end, proto: normalized}, true +} + +// Start returns pr's start port number. +func (pr PortRange) Start() uint16 { + return pr.start +} + +// End returns pr's end port number. +func (pr PortRange) End() uint16 { + return pr.end +} + +// Proto returns pr's network protocol. +func (pr PortRange) Proto() NetworkProtocol { + return pr.proto.Value() +} + +// IsZero reports whether pr is the zero value. +func (pr PortRange) IsZero() bool { + return pr.proto == protoZero +} + +// IsValid reports whether pr is an initialized valid port range (not the zero value). +func (pr PortRange) IsValid() bool { + return pr.proto != protoZero +} + +// String returns a string representation of the port range in the format "-/" or "/" if start == end. +// If the port range is the zero value, it returns "invalid port range". +func (pr PortRange) String() string { + switch pr.proto { + case protoZero: + return "invalid port range" + default: + return string(pr.AppendTo(nil)) + } +} + +// AppendText implements [encoding.TextAppender] interface. +// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface. +func (pr PortRange) AppendText(b []byte) ([]byte, error) { + return pr.AppendTo(b), nil +} + +// AppendTo appends a text encoding of pr to b and returns the extended buffer. +func (pr PortRange) AppendTo(b []byte) []byte { + if pr.IsZero() { + return b + } + if pr.start == pr.end { + return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value()) + } + return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value()) +} + +// MarshalText implements [encoding.TextMarshaler] interface. +func (pr PortRange) MarshalText() ([]byte, error) { + return pr.AppendText(nil) +} + +// UnmarshalText implements [encoding.TextUnmarshaler] interface. +func (pr *PortRange) UnmarshalText(text []byte) error { + if len(text) == 0 { + *pr = PortRange{} + return nil + } + + portRange, err := ParsePortRange(string(text)) + if err != nil { + return err + } + *pr = portRange + return nil +} + +// Range returns pr. +func (pr PortRange) Range() PortRange { + return pr +} + +// All returns an iterator over all the individual ports in the range. +// +// For example: +// +// for port := range pr.All() { +// // ... +// } +func (pr PortRange) All() iter.Seq[Port] { + return func(yield func(Port) bool) { + for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ { + if !yield(Port{num: uint16(i), proto: pr.proto}) { + return + } + } + } +} + +// parsePortNumber parses rawPort into an int, unwrapping strconv errors +// and returning a single "out of range" error for any value outside 0–65535. +func parsePortNumber(rawPort string) (uint16, error) { + if rawPort == "" { + return 0, errors.New("value is empty") + } + port, err := strconv.ParseUint(rawPort, 10, 16) + if err != nil { + var numErr *strconv.NumError + if errors.As(err, &numErr) { + err = numErr.Err + } + return 0, err + } + + return uint16(port), nil +} + +// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent. +// If proto is not specified, it defaults to "tcp". +func normalizePortProto(proto string) unique.Handle[NetworkProtocol] { + if proto == "" { + return unique.Make(TCP) + } + + proto = strings.ToLower(proto) + + return unique.Make(NetworkProtocol(proto)) +} diff --git a/api/types/container/network_test.go b/api/types/container/network_test.go new file mode 100644 index 0000000000..128787146b --- /dev/null +++ b/api/types/container/network_test.go @@ -0,0 +1,600 @@ +package container + +import ( + "encoding/json" + "fmt" + "reflect" + "slices" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +type TestRanger interface { + Range() PortRange +} + +var _ TestRanger = Port{} +var _ TestRanger = PortRange{} + +func TestPort(t *testing.T) { + t.Run("Zero Value", func(t *testing.T) { + var p Port + assert.Check(t, p.IsZero()) + assert.Check(t, !p.IsValid()) + assert.Equal(t, p.String(), "invalid port") + + t.Run("Marshal Unmarshal", func(t *testing.T) { + var p Port + bytes, err := p.MarshalText() + assert.NilError(t, err) + assert.Check(t, len(bytes) == 0) + + err = p.UnmarshalText([]byte("")) + assert.NilError(t, err) + assert.Equal(t, p, Port{}) + }) + + t.Run("JSON Marshal Unmarshal", func(t *testing.T) { + var p Port + bytes, err := json.Marshal(p) + assert.NilError(t, err) + assert.Equal(t, string(bytes), `""`) + + err = json.Unmarshal([]byte(`""`), &p) + assert.NilError(t, err) + assert.Equal(t, p, Port{}) + }) + }) + + t.Run("PortFrom", func(t *testing.T) { + tests := []struct { + num uint16 + proto NetworkProtocol + }{ + {0, TCP}, + {80, TCP}, + {8080, TCP}, + {65535, TCP}, + {80, UDP}, + {8080, SCTP}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) { + p, ok := PortFrom(tc.num, tc.proto) + assert.Check(t, ok) + assert.Equal(t, p.Num(), tc.num) + assert.Equal(t, p.Proto(), tc.proto) + }) + } + + t.Run("Normalize Protocol", func(t *testing.T) { + pr1 := portFrom(1234, "tcp") + pr2 := portFrom(1234, "TCP") + pr3 := portFrom(1234, "tCp") + assert.Equal(t, pr1, pr2) + assert.Equal(t, pr2, pr3) + }) + + negativeTests := []struct { + num uint16 + proto NetworkProtocol + }{ + {0, ""}, + {80, ""}, + } + for _, tc := range negativeTests { + t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) { + p, ok := PortFrom(tc.num, tc.proto) + assert.Check(t, !ok) + assert.Check(t, p.IsZero()) + assert.Check(t, !p.IsValid()) + assert.Equal(t, p.String(), "invalid port") + }) + } + }) + + t.Run("ParsePort", func(t *testing.T) { + tests := []struct { + in string + port Port // output of ParsePort() + str string // output of String(). + portRange PortRange // output of Range() + }{ + // Zero port + { + in: "0/tcp", + port: portFrom(0, TCP), + str: "0/tcp", + portRange: portRangeFrom(0, 0, TCP), + }, + // Max valid port + { + in: "65535/tcp", + port: portFrom(65535, TCP), + str: "65535/tcp", + portRange: portRangeFrom(65535, 65535, TCP), + }, + // Simple valid ports + { + in: "1234/tcp", + port: portFrom(1234, TCP), + str: "1234/tcp", + portRange: portRangeFrom(1234, 1234, TCP), + }, + { + in: "1234/udp", + port: portFrom(1234, UDP), + str: "1234/udp", + portRange: portRangeFrom(1234, 1234, UDP), + }, + { + in: "1234/sctp", + port: portFrom(1234, SCTP), + str: "1234/sctp", + portRange: portRangeFrom(1234, 1234, SCTP), + }, + // Default protocol is tcp + { + in: "1234", + port: portFrom(1234, TCP), + str: "1234/tcp", + portRange: portRangeFrom(1234, 1234, TCP), + }, + // Default protocol is tcp + { + in: "1234/", + port: portFrom(1234, TCP), + str: "1234/tcp", + portRange: portRangeFrom(1234, 1234, TCP), + }, + { + in: "1234/tcp:ipv6only", + port: portFrom(1234, "tcp:ipv6only"), + str: "1234/tcp:ipv6only", + portRange: portRangeFrom(1234, 1234, "tcp:ipv6only"), + }, + } + + for _, tc := range tests { + t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) { + got, err := ParsePort(tc.in) + assert.NilError(t, err) + assert.Equal(t, got, tc.port) + + MustParsePort(tc.in) // should not panic + + assert.Check(t, !got.IsZero()) + assert.Check(t, got.IsValid()) + + // Check that ParsePort is a pure function. + got2, err := ParsePort(tc.in) + assert.NilError(t, err) + assert.Equal(t, got2, got) + + // Check that ParsePort(port.String()) is the identity function. + got3, err := ParsePort(got.String()) + assert.NilError(t, err) + assert.Equal(t, got3, got) + + // Check String() output + s := got.String() + wants := tc.str + if wants == "" { + wants = tc.in + } + assert.Equal(t, s, wants) + + js := `"` + tc.in + `"` + var jsgot Port + err = json.Unmarshal([]byte(js), &jsgot) + assert.NilError(t, err) + assert.Equal(t, jsgot, got) + + jsb, err := json.Marshal(jsgot) + assert.NilError(t, err) + + jswant := `"` + wants + `"` + assert.Equal(t, string(jsb), jswant) + + // Check Range() output + r := got.Range() + assert.Equal(t, r, tc.portRange) + }) + } + + t.Run("Normalize Protocol", func(t *testing.T) { + p1 := MustParsePort("1234/tcp") + p2 := MustParsePort("1234/TCP") + p3 := MustParsePort("1234/tCp") + assert.Equal(t, p1, p2) + assert.Equal(t, p2, p3) + }) + + negativeTests := []string{ + // Empty string + "", + // Whitespace-only string + " ", + // No port number + "/", + // No port number (protocol only) + "/tcp", + // Negative port + "-1", + // Too large port + "65536", + // Non-numeric port + "foo", + // Port range instead of single port + "1234-1240/udp", + // Port range instead of single port without protocol + "1234-1240", + // Garbage port + "asd1234/tcp", + } + + for _, s := range negativeTests { + t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) { + got, err := ParsePort(s) + assert.ErrorContains(t, err, "invalid port") + assert.Check(t, got.IsZero()) + assert.Check(t, !got.IsValid()) + + // Skip JSON unmarshalling test for empty string as that should succeed. + // See test "Zero Value" above. + if s == "" { + return + } + + var jsgot Port + js := []byte(`"` + s + `"`) + err = json.Unmarshal(js, &jsgot) + assert.ErrorContains(t, err, "invalid port") + assert.Equal(t, jsgot, Port{}) + }) + } + }) +} + +func TestPortRange(t *testing.T) { + t.Run("Zero Value", func(t *testing.T) { + var pr PortRange + assert.Check(t, pr.IsZero()) + assert.Check(t, !pr.IsValid()) + assert.Equal(t, pr.String(), "invalid port range") + + t.Run("Marshal Unmarshal", func(t *testing.T) { + var pr PortRange + bytes, err := pr.MarshalText() + assert.NilError(t, err) + assert.Check(t, len(bytes) == 0) + + err = pr.UnmarshalText([]byte("")) + assert.NilError(t, err) + assert.Equal(t, pr, PortRange{}) + }) + + t.Run("JSON Marshal Unmarshal", func(t *testing.T) { + var pr PortRange + bytes, err := json.Marshal(pr) + assert.NilError(t, err) + assert.Equal(t, string(bytes), `""`) + + err = json.Unmarshal([]byte(`""`), &pr) + assert.NilError(t, err) + assert.Equal(t, pr, PortRange{}) + }) + }) + + t.Run("PortRangeFrom", func(t *testing.T) { + tests := []struct { + start uint16 + end uint16 + proto NetworkProtocol + }{ + {0, 0, TCP}, + {0, 1234, TCP}, + {80, 80, TCP}, + {80, 8080, TCP}, + {1234, 65535, TCP}, + {80, 80, UDP}, + {80, 8080, SCTP}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) { + pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto) + assert.Check(t, ok) + assert.Equal(t, pr.Start(), tc.start) + assert.Equal(t, pr.End(), tc.end) + assert.Equal(t, pr.Proto(), tc.proto) + }) + } + + t.Run("Normalize Protocol", func(t *testing.T) { + pr1, _ := PortRangeFrom(1234, 5678, "tcp") + pr2, _ := PortRangeFrom(1234, 5678, "TCP") + pr3, _ := PortRangeFrom(1234, 5678, "tCp") + assert.Equal(t, pr1, pr2) + assert.Equal(t, pr2, pr3) + }) + + negativeTests := []struct { + start uint16 + end uint16 + proto NetworkProtocol + }{ + {1234, 80, TCP}, // end < start + {0, 0, ""}, // empty protocol + } + for _, tc := range negativeTests { + t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) { + pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto) + assert.Check(t, !ok) + assert.Check(t, pr.IsZero()) + assert.Check(t, !pr.IsValid()) + }) + } + }) + + t.Run("ParsePortRange", func(t *testing.T) { + tests := []struct { + in string + portRange PortRange // output of ParsePortRange() and Range() + str string // output of String(). If "", use in. + + }{ + // Zero port + { + in: "0-1234/tcp", + portRange: portRangeFrom(0, 1234, TCP), + str: "0-1234/tcp", + }, + // Max valid port + { + in: "1234-65535/tcp", + portRange: portRangeFrom(1234, 65535, TCP), + str: "1234-65535/tcp", + }, + // Simple valid ports + { + in: "1234-4567/tcp", + portRange: portRangeFrom(1234, 4567, TCP), + str: "1234-4567/tcp", + }, + { + in: "1234-4567/udp", + portRange: portRangeFrom(1234, 4567, UDP), + str: "1234-4567/udp", + }, + // Default protocol is tcp + { + in: "1234-4567", + portRange: portRangeFrom(1234, 4567, TCP), + str: "1234-4567/tcp", + }, + // Default protocol is tcp + { + in: "1234-4567/", + portRange: portRangeFrom(1234, 4567, TCP), + str: "1234-4567/tcp", + }, + { + in: "1234/tcp", + portRange: portRangeFrom(1234, 1234, TCP), + str: "1234/tcp", + }, + { + in: "1234", + portRange: portRangeFrom(1234, 1234, TCP), + str: "1234/tcp", + }, + { + in: "1234-5678/tcp:ipv6only", + portRange: portRangeFrom(1234, 5678, "tcp:ipv6only"), + str: "1234-5678/tcp:ipv6only", + }, + } + + for _, tc := range tests { + t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) { + got, err := ParsePortRange(tc.in) + assert.NilError(t, err) + assert.Equal(t, got, tc.portRange) + assert.Check(t, !got.IsZero()) + assert.Check(t, got.IsValid()) + + MustParsePortRange(tc.in) // should not panic + + // Check that ParsePortRange is a pure function. + got2, err := ParsePortRange(tc.in) + assert.NilError(t, err) + assert.Equal(t, got2, got) + + // Check that ParsePortRange(port.String()) is the identity function. + got3, err := ParsePortRange(got.String()) + assert.NilError(t, err) + assert.Equal(t, got3, got) + + // Check String() output + s := got.String() + wants := tc.str + if wants == "" { + wants = tc.in + } + assert.Equal(t, s, wants) + + js := `"` + tc.in + `"` + var jsgot PortRange + err = json.Unmarshal([]byte(js), &jsgot) + assert.NilError(t, err) + assert.Equal(t, jsgot, got) + + jsb, err := json.Marshal(jsgot) + assert.NilError(t, err) + jswant := `"` + wants + `"` + assert.Equal(t, string(jsb), jswant) + + // Check Range() output + r := got.Range() + assert.Equal(t, r, tc.portRange) + }) + + t.Run("Normalize Protocol", func(t *testing.T) { + pr1 := MustParsePortRange("1234-5678/tcp") + pr2 := MustParsePortRange("1234-5678/TCP") + pr3 := MustParsePortRange("1234-5678/tCp") + assert.Equal(t, pr1, pr2) + assert.Equal(t, pr2, pr3) + }) + + negativeTests := []string{ + // Empty string + "", + // Whitespace-only string + " ", + // No port number + "/", + // No port number (protocol only) + "/tcp", + // Negative start port + "-1-1234", + // Negative end port + "1234--1", + // Too large start port + "65536-65537", + // Too large end port + "1234-65536", + // Non-numeric start port + "foo-1234", + // Non-numeric end port + "1234-bar", + // Start port greater than end port + "1234-1000", + // Garbage port range + "asd1234-5678/tcp", + } + + for _, s := range negativeTests { + t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) { + got, err := ParsePortRange(s) + assert.Check(t, err != nil) + assert.Check(t, got.IsZero()) + assert.Check(t, !got.IsValid()) + + // Skip JSON unmarshalling test for empty string as that should succeed. + // See test "Zero Value" above. + if s == "" { + return + } + + var jsgot PortRange + js := []byte(`"` + s + `"`) + err = json.Unmarshal(js, &jsgot) + assert.Check(t, err != nil) + assert.Equal(t, jsgot, PortRange{}) + }) + } + } + }) + + t.Run("PortRange All()", func(t *testing.T) { + tests := []struct { + in string + want []Port + }{ + { + in: "1000-1000/tcp", + want: []Port{portFrom(1000, TCP)}, + }, + { + in: "1000-1002/tcp", + want: []Port{portFrom(1000, TCP), portFrom(1001, TCP), portFrom(1002, TCP)}, + }, + { + in: "0-0/tcp", + want: []Port{portFrom(0, TCP)}, + }, + { + in: "65535-65535/tcp", + want: []Port{portFrom(65535, TCP)}, + }, + { + in: "65530-65535/tcp", + want: []Port{portFrom(65530, TCP), portFrom(65531, TCP), portFrom(65532, TCP), portFrom(65533, TCP), portFrom(65534, TCP), portFrom(65535, TCP)}, + }, + } + + for _, tc := range tests { + pr := MustParsePortRange(tc.in) + ports := slices.Collect(pr.All()) + if !reflect.DeepEqual(ports, tc.want) { + t.Errorf("PortRange.All() = %#v, want %#v", ports, tc.want) + } + } + + t.Run("All() stop early", func(t *testing.T) { + want := []Port{portFrom(1000, TCP), portFrom(1001, TCP)} + pr := MustParsePortRange("1000-2000/tcp") + var ports []Port + for p := range pr.All() { + ports = append(ports, p) + if len(ports) == 2 { + break + } + } + if !reflect.DeepEqual(ports, want) { + t.Errorf("PortRange.All() = %#v, want %#v", ports, want) + } + }) + }) +} + +func BenchmarkPortRangeAll(b *testing.B) { + b.Run("Single Port", func(b *testing.B) { + pr := MustParsePortRange("1234/tcp") + b.ResetTimer() + for i := 0; i < b.N; i++ { + var sink int64 + for p := range pr.All() { + sink += int64(p.Num()) // prevent compiler optimization + } + if sink < 0 { + b.Fatal("unreachable") + } + } + }) + + b.Run("Range", func(b *testing.B) { + pr := MustParsePortRange("0-65535/tcp") + b.ResetTimer() + for i := 0; i < b.N; i++ { + var sink int64 + for p := range pr.All() { + sink += int64(p.Num()) // prevent compiler optimization + } + if sink < 0 { + b.Fatal("unreachable") + } + } + }) +} + +func portFrom(num uint16, proto NetworkProtocol) Port { + p, ok := PortFrom(num, proto) + if !ok { + panic("invalid port") + } + return p +} + +func portRangeFrom(start, end uint16, proto NetworkProtocol) PortRange { + pr, ok := PortRangeFrom(start, end, proto) + if !ok { + panic("invalid port range") + } + return pr +} diff --git a/daemon/builder/dockerfile/dispatchers.go b/daemon/builder/dockerfile/dispatchers.go index 984a86ab9e..c383aad764 100644 --- a/daemon/builder/dockerfile/dispatchers.go +++ b/daemon/builder/dockerfile/dispatchers.go @@ -11,12 +11,13 @@ import ( "bytes" "context" "fmt" + "net" "runtime" "sort" + "strconv" "strings" "github.com/containerd/platforms" - "github.com/docker/go-connections/nat" "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/moby/buildkit/frontend/dockerfile/shell" @@ -525,7 +526,7 @@ func dispatchExpose(ctx context.Context, d dispatchRequest, c *instructions.Expo } c.Ports = ports - ps, _, err := nat.ParsePortSpecs(ports) + ps, _, err := parsePortSpecs(ports) if err != nil { return err } @@ -540,6 +541,154 @@ func dispatchExpose(ctx context.Context, d dispatchRequest, c *instructions.Expo return d.builder.commit(ctx, d.state, "EXPOSE "+strings.Join(c.Ports, " ")) } +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L122-L144 +// +// parsePortSpecs receives port specs in the format of ip:public:private/proto and parses +// these in to the internal types +func parsePortSpecs(ports []string) (map[container.Port]struct{}, map[container.Port][]container.PortBinding, error) { + var ( + exposedPorts = make(map[container.Port]struct{}, len(ports)) + bindings = make(map[container.Port][]container.PortBinding) + ) + for _, p := range ports { + portMappings, err := parsePortSpec(p) + if err != nil { + return nil, nil, err + } + + for _, pm := range portMappings { + for port, portBindings := range pm { + if _, ok := exposedPorts[port]; !ok { + exposedPorts[port] = struct{}{} + } + bindings[port] = append(bindings[port], portBindings...) + } + } + } + return exposedPorts, bindings, nil +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L172-L237 +// +// parsePortSpec parses a port specification string into a slice of [container.PortMap] +func parsePortSpec(rawPort string) ([]container.PortMap, error) { + ip, hostPort, containerPort := splitParts(rawPort) + proto, containerPort := splitProtoPort(containerPort) + if containerPort == "" { + return nil, fmt.Errorf("no port specified: %s", rawPort) + } + + proto = strings.ToLower(proto) + if err := validateProto(proto); err != nil { + return nil, err + } + + if ip != "" && ip[0] == '[' { + // Strip [] from IPV6 addresses + rawIP, _, err := net.SplitHostPort(ip + ":") + if err != nil { + return nil, fmt.Errorf("invalid IP address %v: %w", ip, err) + } + ip = rawIP + } + if ip != "" && net.ParseIP(ip) == nil { + return nil, errors.New("invalid IP address: " + ip) + } + + pr, err := container.ParsePortRange(containerPort) + if err != nil { + return nil, errors.New("invalid containerPort: " + containerPort) + } + + var ( + startPort = pr.Start() + endPort = pr.End() + ) + + var startHostPort, endHostPort uint16 + if hostPort != "" { + hostPortRange, err := container.ParsePortRange(hostPort) + if err != nil { + return nil, errors.New("invalid hostPort: " + hostPort) + } + startHostPort = hostPortRange.Start() + endHostPort = hostPortRange.End() + if (endPort - startPort) != (endHostPort - startHostPort) { + // Allow host port range iff containerPort is not a range. + // In this case, use the host port range as the dynamic + // host port range to allocate into. + if endPort != startPort { + return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + } + } + } + + count := endPort - startPort + 1 + ports := make([]container.PortMap, 0, count) + + for i := uint16(0); i < count; i++ { + hPort := "" + if hostPort != "" { + hPort = strconv.Itoa(int(startHostPort + i)) + // Set hostPort to a range only if there is a single container port + // and a dynamic host port. + if count == 1 && startHostPort != endHostPort { + hPort += "-" + strconv.Itoa(int(endHostPort)) + } + } + ports = append(ports, container.PortMap{ + container.MustParsePort(fmt.Sprintf("%d/%s", startPort+i, proto)): []container.PortBinding{{HostIP: ip, HostPort: hPort}}, + }) + } + return ports, nil +} + +// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L156-170 +func splitParts(rawport string) (hostIP, hostPort, containerPort string) { + parts := strings.Split(rawport, ":") + + switch len(parts) { + case 1: + return "", "", parts[0] + case 2: + return "", parts[0], parts[1] + case 3: + return parts[0], parts[1], parts[2] + default: + n := len(parts) + return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1] + } +} + +// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L95-L110 +// splitProtoPort splits a port(range) and protocol, formatted as "/[]" +// "/[]". It returns an empty string for both if +// no port(range) is provided. If a port(range) is provided, but no protocol, +// the default ("tcp") protocol is returned. +// +// splitProtoPort does not validate or normalize the returned values. +func splitProtoPort(rawPort string) (proto string, port string) { + port, proto, _ = strings.Cut(rawPort, "/") + if port == "" { + return "", "" + } + if proto == "" { + proto = "tcp" + } + return proto, port +} + +// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L112-L120 +func validateProto(proto string) error { + switch proto { + case "tcp", "udp", "sctp": + // All good + return nil + default: + return errors.New("invalid proto: " + proto) + } +} + // USER foo // // Set the user to 'foo' for future commands and when running the diff --git a/daemon/builder/dockerfile/dispatchers_test.go b/daemon/builder/dockerfile/dispatchers_test.go index da7e964bcb..77c2ea11d0 100644 --- a/daemon/builder/dockerfile/dispatchers_test.go +++ b/daemon/builder/dockerfile/dispatchers_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "reflect" "runtime" "strings" "testing" @@ -335,7 +336,7 @@ func TestExpose(t *testing.T) { assert.Assert(t, sb.state.runConfig.ExposedPorts != nil) assert.Assert(t, is.Len(sb.state.runConfig.ExposedPorts, 1)) - assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, container.PortRangeProto("80/tcp"))) + assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, container.MustParsePort("80/tcp"))) } func TestUser(t *testing.T) { @@ -626,3 +627,648 @@ func TestDispatchUnsupportedOptions(t *testing.T) { } }) } + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L390-L499 +func TestParsePortSpecs(t *testing.T) { + var ( + portMap map[container.Port]struct{} + bindingMap map[container.Port][]container.PortBinding + err error + ) + + tcp1234 := container.MustParsePort("1234/tcp") + udp2345 := container.MustParsePort("2345/udp") + sctp3456 := container.MustParsePort("3456/sctp") + + portMap, bindingMap, err = parsePortSpecs([]string{tcp1234.String(), udp2345.String(), sctp3456.String()}) + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[tcp1234]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[udp2345]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + if _, ok := portMap[sctp3456]; !ok { + t.Fatal("3456/sctp was not parsed properly") + } + + for portSpec, bindings := range bindingMap { + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portSpec) + } + + if bindings[0].HostIP != "" { + t.Fatalf("HostIP should not be set for %s", portSpec) + } + + if bindings[0].HostPort != "" { + t.Fatalf("HostPort should not be set for %s", portSpec) + } + } + + portMap, bindingMap, err = parsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp", "3456:3456/sctp"}) + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[tcp1234]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[udp2345]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + if _, ok := portMap[sctp3456]; !ok { + t.Fatal("3456/sctp was not parsed properly") + } + + for portSpec, bindings := range bindingMap { + _, port := splitProtoPort(portSpec.String()) + + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portSpec) + } + + if bindings[0].HostIP != "" { + t.Fatalf("HostIP should not be set for %s", portSpec) + } + + if bindings[0].HostPort != port { + t.Fatalf("HostPort(%s) should be %s for %s", bindings[0].HostPort, port, portSpec) + } + } + + portMap, bindingMap, err = parsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp", "0.0.0.0:3456:3456/sctp"}) + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[tcp1234]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[udp2345]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + if _, ok := portMap[sctp3456]; !ok { + t.Fatal("3456/sctp was not parsed properly") + } + + for portSpec, bindings := range bindingMap { + _, port := splitProtoPort(portSpec.String()) + + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portSpec) + } + + if bindings[0].HostIP != "0.0.0.0" { + t.Fatalf("HostIP is not 0.0.0.0 for %s", portSpec) + } + + if bindings[0].HostPort != port { + t.Fatalf("HostPort should be %s for %s", port, portSpec) + } + } + + _, _, err = parsePortSpecs([]string{"localhost:1234:1234/tcp"}) + if err == nil { + t.Fatal("Received no error while trying to parse a hostname instead of ip") + } +} + +// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L244-L274 +func TestParsePortSpecEmptyContainerPort(t *testing.T) { + tests := []struct { + name string + spec string + expError string + }{ + { + name: "empty spec", + spec: "", + expError: `no port specified: `, + }, + { + name: "empty container port", + spec: `0.0.0.0:1234-1235:/tcp`, + expError: `no port specified: 0.0.0.0:1234-1235:/tcp`, + }, + { + name: "empty container port and proto", + spec: `0.0.0.0:1234-1235:`, + expError: `no port specified: 0.0.0.0:1234-1235:`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := parsePortSpec(tc.spec) + if err == nil || err.Error() != tc.expError { + t.Fatalf("expected %v, got: %v", tc.expError, err) + } + }) + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L276-L302 +func TestParsePortSpecFull(t *testing.T) { + portMappings, err := parsePortSpec("0.0.0.0:1234-1235:3333-3334/tcp") + if err != nil { + t.Fatalf("expected nil error, got: %v", err) + } + + expected := []container.PortMap{ + { + container.MustParsePort("3333/tcp"): []container.PortBinding{ + { + HostIP: "0.0.0.0", + HostPort: "1234", + }, + }, + }, + { + container.MustParsePort("3334/tcp"): []container.PortBinding{ + { + HostIP: "0.0.0.0", + HostPort: "1235", + }, + }, + }, + } + + if !reflect.DeepEqual(expected, portMappings) { + t.Fatalf("wrong port mappings: got=%v, want=%v", portMappings, expected) + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L304-L388 +func TestPartPortSpecIPV6(t *testing.T) { + type test struct { + name string + spec string + expected []container.PortMap + } + cases := []test{ + { + name: "square angled IPV6 without host port", + spec: "[2001:4860:0:2001::68]::333", + expected: []container.PortMap{ + { + container.MustParsePort("333/tcp"): []container.PortBinding{ + { + HostIP: "2001:4860:0:2001::68", + HostPort: "", + }, + }, + }, + }, + }, + { + name: "square angled IPV6 with host port", + spec: "[::1]:80:80", + expected: []container.PortMap{ + { + container.MustParsePort("80/tcp"): []container.PortBinding{ + { + HostIP: "::1", + HostPort: "80", + }, + }, + }, + }, + }, + { + name: "IPV6 without host port", + spec: "2001:4860:0:2001::68::333", + expected: []container.PortMap{ + { + container.MustParsePort("333/tcp"): []container.PortBinding{ + { + HostIP: "2001:4860:0:2001::68", + HostPort: "", + }, + }, + }, + }, + }, + { + name: "IPV6 with host port", + spec: "::1:80:80", + expected: []container.PortMap{ + { + container.MustParsePort("80/tcp"): []container.PortBinding{ + { + HostIP: "::1", + HostPort: "80", + }, + }, + }, + }, + }, + { + name: ":: IPV6, without host port", + spec: "::::80", + expected: []container.PortMap{ + { + container.MustParsePort("80/tcp"): []container.PortBinding{ + { + HostIP: "::", + HostPort: "", + }, + }, + }, + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + portMappings, err := parsePortSpec(c.spec) + if err != nil { + t.Fatalf("expected nil error, got: %v", err) + } + if !reflect.DeepEqual(c.expected, portMappings) { + t.Fatalf("wrong port mappings: got=%v, want=%v", portMappings, c.expected) + } + }) + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L501-L600 +func TestParsePortSpecsWithRange(t *testing.T) { + var ( + portMap map[container.Port]struct{} + bindingMap map[container.Port][]container.PortBinding + err error + ) + + portMap, bindingMap, err = parsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp", "3456-3458/sctp"}) + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok { + t.Fatal("1234-1236/tcp was not parsed properly") + } + + if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok { + t.Fatal("2345-2347/udp was not parsed properly") + } + + if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok { + t.Fatal("3456-3458/sctp was not parsed properly") + } + + for portSpec, bindings := range bindingMap { + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portSpec) + } + + if bindings[0].HostIP != "" { + t.Fatalf("HostIP should not be set for %s", portSpec) + } + + if bindings[0].HostPort != "" { + t.Fatalf("HostPort should not be set for %s", portSpec) + } + } + + portMap, bindingMap, err = parsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp", "3456-3458:3456-3458/sctp"}) + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok { + t.Fatal("1234-1236 was not parsed properly") + } + + if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok { + t.Fatal("2345-2347 was not parsed properly") + } + + if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok { + t.Fatal("3456-3458 was not parsed properly") + } + + for portSpec, bindings := range bindingMap { + _, port := splitProtoPort(portSpec.String()) + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portSpec) + } + + if bindings[0].HostIP != "" { + t.Fatalf("HostIP should not be set for %s", portSpec) + } + + if bindings[0].HostPort != port { + t.Fatalf("HostPort should be %s for %s", port, portSpec) + } + } + + portMap, bindingMap, err = parsePortSpecs([]string{"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp", "0.0.0.0:3456-3458:3456-3458/sctp"}) + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok { + t.Fatal("1234-1236 was not parsed properly") + } + + if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok { + t.Fatal("2345-2347 was not parsed properly") + } + + if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok { + t.Fatal("3456-3458 was not parsed properly") + } + + for portSpec, bindings := range bindingMap { + _, port := splitProtoPort(portSpec.String()) + if len(bindings) != 1 || bindings[0].HostIP != "0.0.0.0" || bindings[0].HostPort != port { + t.Fatalf("Expect single binding to port %s but found %s", port, bindings) + } + } + + _, _, err = parsePortSpecs([]string{"localhost:1234-1236:1234-1236/tcp"}) + + if err == nil { + t.Fatal("Received no error while trying to parse a hostname instead of ip") + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L602-L642 +func TestParseNetworkOptsPrivateOnly(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::80"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Errorf("Expected 1 got %d", len(ports)) + } + if len(bindings) != 1 { + t.Errorf("Expected 1 got %d", len(bindings)) + } + for k := range ports { + if k.Proto() != "tcp" { + t.Errorf("Expected tcp got %s", k.Proto()) + } + if k.Num() != 80 { + t.Errorf("Expected 80 got %d", k.Num()) + } + b, exists := bindings[k] + if !exists { + t.Error("Binding does not exist") + } + if len(b) != 1 { + t.Errorf("Expected 1 got %d", len(b)) + } + s := b[0] + if s.HostPort != "" { + t.Errorf("Expected \"\" got %s", s.HostPort) + } + if s.HostIP != "192.168.1.100" { + t.Errorf("Expected 192.168.1.100 got %s", s.HostIP) + } + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L644-L684 +func TestParseNetworkOptsPublic(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100:8080:80"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Errorf("Expected 1 got %d", len(ports)) + } + if len(bindings) != 1 { + t.Errorf("Expected 1 got %d", len(bindings)) + } + for k := range ports { + if k.Proto() != "tcp" { + t.Errorf("Expected tcp got %s", k.Proto()) + } + if k.Num() != 80 { + t.Errorf("Expected 80 got %d", k.Num()) + } + b, exists := bindings[k] + if !exists { + t.Error("Binding does not exist") + } + if len(b) != 1 { + t.Errorf("Expected 1 got %d", len(b)) + } + s := b[0] + if s.HostPort != "8080" { + t.Errorf("Expected 8080 got %s", s.HostPort) + } + if s.HostIP != "192.168.1.100" { + t.Errorf("Expected 192.168.1.100 got %s", s.HostIP) + } + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L686-L701 +func TestParseNetworkOptsPublicNoPort(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100"}) + + if err == nil { + t.Error("Expected error Invalid containerPort") + } + if ports != nil { + t.Errorf("Expected nil got %s", ports) + } + if bindings != nil { + t.Errorf("Expected nil got %s", bindings) + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L703-L717 +func TestParseNetworkOptsNegativePorts(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100:-1:-1"}) + + if err == nil { + t.Error("Expected error Invalid containerPort") + } + if len(ports) != 0 { + t.Errorf("Expected 0 got %d: %#v", len(ports), ports) + } + if len(bindings) != 0 { + t.Errorf("Expected 0 got %d: %#v", len(bindings), bindings) + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L719-L759 +func TestParseNetworkOptsUdp(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::6000/udp"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Errorf("Expected 1 got %d: %#v", len(ports), ports) + } + if len(bindings) != 1 { + t.Errorf("Expected 1 got %d", len(bindings)) + } + for k := range ports { + if k.Proto() != "udp" { + t.Errorf("Expected udp got %s", k.Proto()) + } + if k.Num() != 6000 { + t.Errorf("Expected 6000 got %d", k.Num()) + } + b, exists := bindings[k] + if !exists { + t.Error("Binding does not exist") + } + if len(b) != 1 { + t.Errorf("Expected 1 got %d", len(b)) + } + s := b[0] + if s.HostPort != "" { + t.Errorf("Expected \"\" got %s", s.HostPort) + } + if s.HostIP != "192.168.1.100" { + t.Errorf("Expected 192.168.1.100 got %s", s.HostIP) + } + } +} + +// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L761-L801 +func TestParseNetworkOptsSctp(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::6000/sctp"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Errorf("Expected 1 got %d: %#v", len(ports), ports) + } + if len(bindings) != 1 { + t.Errorf("Expected 1 got %d: %#v", len(bindings), bindings) + } + for k := range ports { + if k.Proto() != "sctp" { + t.Errorf("Expected sctp got %s", k.Proto()) + } + if k.Num() != 6000 { + t.Errorf("Expected 6000 got %d", k.Num()) + } + b, exists := bindings[k] + if !exists { + t.Error("Binding does not exist") + } + if len(b) != 1 { + t.Errorf("Expected 1 got %d", len(b)) + } + s := b[0] + if s.HostPort != "" { + t.Errorf("Expected \"\" got %s", s.HostPort) + } + if s.HostIP != "192.168.1.100" { + t.Errorf("Expected 192.168.1.100 got %s", s.HostIP) + } + } +} + +// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L146-L242 +func TestSplitProtoPort(t *testing.T) { + tests := []struct { + doc string + input string + expPort string + expProto string + }{ + { + doc: "empty value", + }, + { + doc: "zero value", + input: "0", + expPort: "0", + expProto: "tcp", + }, + { + doc: "empty port", + input: "/udp", + expPort: "", + expProto: "", + }, + { + doc: "single port", + input: "1234", + expPort: "1234", + expProto: "tcp", + }, + { + doc: "single port with empty protocol", + input: "1234/", + expPort: "1234", + expProto: "tcp", + }, + { + doc: "single port with protocol", + input: "1234/udp", + expPort: "1234", + expProto: "udp", + }, + { + doc: "port range", + input: "80-8080", + expPort: "80-8080", + expProto: "tcp", + }, + { + doc: "port range with empty protocol", + input: "80-8080/", + expPort: "80-8080", + expProto: "tcp", + }, + { + doc: "port range with protocol", + input: "80-8080/udp", + expPort: "80-8080", + expProto: "udp", + }, + // SplitProtoPort currently does not validate or normalize, so these are expected returns + { + doc: "negative value", + input: "-1", + expPort: "-1", + expProto: "tcp", + }, + { + doc: "uppercase protocol", + input: "1234/UDP", + expPort: "1234", + expProto: "UDP", + }, + { + doc: "any value", + input: "any port value", + expPort: "any port value", + expProto: "tcp", + }, + { + doc: "any value with protocol", + input: "any port value/any proto value", + expPort: "any port value", + expProto: "any proto value", + }, + } + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + proto, port := splitProtoPort(tc.input) + if proto != tc.expProto { + t.Errorf("expected proto %s, got %s", tc.expProto, proto) + } + if port != tc.expPort { + t.Errorf("expected port %s, got %s", tc.expPort, port) + } + }) + } +} diff --git a/daemon/builder/dockerfile/internals_test.go b/daemon/builder/dockerfile/internals_test.go index b7516137da..3ac42069ae 100644 --- a/daemon/builder/dockerfile/internals_test.go +++ b/daemon/builder/dockerfile/internals_test.go @@ -137,8 +137,8 @@ func fullMutableRunConfig() *container.Config { Cmd: []string{"command", "arg1"}, Env: []string{"env1=foo", "env2=bar"}, ExposedPorts: container.PortSet{ - "1000/tcp": {}, - "1001/tcp": {}, + container.MustParsePort("1000/tcp"): {}, + container.MustParsePort("1001/tcp"): {}, }, Volumes: map[string]struct{}{ "one": {}, @@ -161,7 +161,7 @@ func TestDeepCopyRunConfig(t *testing.T) { ctrCfg.Cmd[1] = "arg2" ctrCfg.Env[1] = "env2=new" - ctrCfg.ExposedPorts["10002"] = struct{}{} + ctrCfg.ExposedPorts[container.MustParsePort("10002")] = struct{}{} ctrCfg.Volumes["three"] = struct{}{} ctrCfg.Entrypoint[1] = "arg2" ctrCfg.OnBuild[0] = "start" diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 1ff0f675a1..bc851f7a1b 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "math" "net" "strconv" "strings" @@ -150,7 +151,15 @@ func (c *containerConfig) portBindings() container.PortMap { continue } - port := container.PortRangeProto(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String()))) + if portConfig.TargetPort > math.MaxUint16 { + continue + } + + port, ok := container.PortFrom(uint16(portConfig.TargetPort), container.NetworkProtocol(portConfig.Protocol.String())) + if !ok { + continue + } + binding := []container.PortBinding{ {}, } @@ -176,8 +185,8 @@ func (c *containerConfig) init() *bool { return &init } -func (c *containerConfig) exposedPorts() map[container.PortRangeProto]struct{} { - exposedPorts := make(map[container.PortRangeProto]struct{}) +func (c *containerConfig) exposedPorts() map[container.Port]struct{} { + exposedPorts := make(map[container.Port]struct{}) if c.task.Endpoint == nil { return exposedPorts } @@ -187,7 +196,15 @@ func (c *containerConfig) exposedPorts() map[container.PortRangeProto]struct{} { continue } - port := container.PortRangeProto(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String()))) + if portConfig.TargetPort > math.MaxUint16 { + continue + } + + port, ok := container.PortFrom(uint16(portConfig.TargetPort), container.NetworkProtocol(portConfig.Protocol.String())) + if !ok { + continue + } + exposedPorts[port] = struct{}{} } diff --git a/daemon/cluster/executor/container/controller.go b/daemon/cluster/executor/container/controller.go index 7906a8525b..f724d3eeee 100644 --- a/daemon/cluster/executor/container/controller.go +++ b/daemon/cluster/executor/container/controller.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "strconv" - "strings" "time" cerrdefs "github.com/containerd/errdefs" @@ -643,27 +642,17 @@ func parsePortStatus(ctnr container.InspectResponse) (*api.PortStatus, error) { func parsePortMap(portMap container.PortMap) ([]*api.PortConfig, error) { exposedPorts := make([]*api.PortConfig, 0, len(portMap)) - for portProtocol, mapping := range portMap { - p, proto, ok := strings.Cut(string(portProtocol), "/") - if !ok { - return nil, fmt.Errorf("invalid port mapping: %s", portProtocol) - } - - port, err := strconv.ParseUint(p, 10, 16) - if err != nil { - return nil, err - } - + for port, mapping := range portMap { var protocol api.PortConfig_Protocol - switch strings.ToLower(proto) { - case "tcp": + switch port.Proto() { + case container.TCP: protocol = api.ProtocolTCP - case "udp": + case container.UDP: protocol = api.ProtocolUDP - case "sctp": + case container.SCTP: protocol = api.ProtocolSCTP default: - return nil, fmt.Errorf("invalid protocol: %s", proto) + return nil, fmt.Errorf("invalid protocol: %s", port.Proto()) } for _, binding := range mapping { @@ -677,7 +666,7 @@ func parsePortMap(portMap container.PortMap) ([]*api.PortConfig, error) { exposedPorts = append(exposedPorts, &api.PortConfig{ PublishMode: api.PublishModeHost, Protocol: protocol, - TargetPort: uint32(port), + TargetPort: uint32(port.Num()), PublishedPort: uint32(hostPort), }) } diff --git a/daemon/container.go b/daemon/container.go index 70fa622a57..b7c32fd3fc 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -12,7 +12,6 @@ import ( "time" "github.com/containerd/log" - "github.com/docker/go-connections/nat" containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/mount" networktypes "github.com/moby/moby/api/types/network" @@ -375,13 +374,17 @@ func validateHealthCheck(healthConfig *containertypes.HealthConfig) error { func validatePortBindings(ports containertypes.PortMap) error { for port := range ports { - _, portStr := nat.SplitProtoPort(string(port)) - if _, err := nat.ParsePort(portStr); err != nil { - return errors.Errorf("invalid port specification: %q", portStr) + if !port.IsValid() { + return errors.Errorf("invalid port specification: %q", port.String()) } + for _, pb := range ports[port] { - _, err := nat.NewPort(nat.SplitProtoPort(pb.HostPort)) - if err != nil { + if pb.HostPort == "" { + // Empty HostPort means to map to an ephemeral port. + continue + } + + if _, err := containertypes.ParsePortRange(pb.HostPort); err != nil { return errors.Errorf("invalid port specification: %q", pb.HostPort) } } diff --git a/daemon/container/view.go b/daemon/container/view.go index bcb792a559..35c48feaaf 100644 --- a/daemon/container/view.go +++ b/daemon/container/view.go @@ -10,7 +10,6 @@ import ( "time" "github.com/containerd/log" - "github.com/docker/go-connections/nat" memdb "github.com/hashicorp/go-memdb" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/network" @@ -391,29 +390,24 @@ func (v *View) transform(ctr *Container) *Snapshot { } } for p, bindings := range ctr.NetworkSettings.Ports { - proto, port := nat.SplitProtoPort(string(p)) - p, err := nat.ParsePort(port) - if err != nil { - log.G(context.TODO()).WithError(err).Warn("invalid port map") - continue - } if len(bindings) == 0 { snapshot.Ports = append(snapshot.Ports, container.PortSummary{ - PrivatePort: uint16(p), - Type: proto, + PrivatePort: p.Num(), + Type: string(p.Proto()), }) continue } for _, binding := range bindings { - h, err := nat.ParsePort(binding.HostPort) + // TODO(thaJeztah): if this is always a port/proto (no range), we can simplify this to [container.ParsePort]. + h, err := container.ParsePortRange(binding.HostPort) if err != nil { log.G(context.TODO()).WithError(err).Warn("invalid host port map") continue } snapshot.Ports = append(snapshot.Ports, container.PortSummary{ - PrivatePort: uint16(p), - PublicPort: uint16(h), - Type: proto, + PrivatePort: p.Num(), + PublicPort: h.Start(), + Type: string(p.Proto()), IP: binding.HostIP, }) } diff --git a/daemon/container_operations.go b/daemon/container_operations.go index 0a9f89b7ee..1b22ff9eb0 100644 --- a/daemon/container_operations.go +++ b/daemon/container_operations.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "maps" "net" "net/netip" "os" @@ -15,7 +14,6 @@ import ( cerrdefs "github.com/containerd/errdefs" "github.com/containerd/log" - "github.com/docker/go-connections/nat" containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/events" networktypes "github.com/moby/moby/api/types/network" @@ -112,51 +110,56 @@ func buildSandboxOptions(cfg *config.Config, ctr *container.Container) ([]libnet } } - // Create a deep copy (as [nat.SortPortMap] mutates the map). - // Not using a maps.Clone here, as that won't dereference the - // slice (PortMap is a map[Port][]PortBinding). - bindings := make(containertypes.PortMap) + portBindings := make(containertypes.PortMap, len(ctr.HostConfig.PortBindings)) for p, b := range ctr.HostConfig.PortBindings { - bindings[p] = slices.Clone(b) + portBindings[p] = slices.Clone(b) } - ports := slices.Collect(maps.Keys(ctr.Config.ExposedPorts)) - nat.SortPortMap(ports, bindings) + for p := range ctr.Config.ExposedPorts { + if _, ok := portBindings[p]; !ok { + // Create nil entries for exposed but un-mapped ports. + portBindings[p] = nil + } + } var ( publishedPorts []types.PortBinding exposedPorts []types.TransportPort ) - for _, port := range ports { - portProto := types.ParseProtocol(port.Proto()) - portNum := uint16(port.Int()) + for port, bindings := range portBindings { + protocol := types.ParseProtocol(string(port.Proto())) exposedPorts = append(exposedPorts, types.TransportPort{ - Proto: portProto, - Port: portNum, + Proto: protocol, + Port: port.Num(), }) - for _, binding := range bindings[port] { - newP, err := nat.NewPort(nat.SplitProtoPort(binding.HostPort)) - var portStart, portEnd int - if err == nil { - portStart, portEnd, err = newP.Range() - } - if err != nil { - return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding.HostPort, err) + for _, binding := range bindings { + var ( + portRange containertypes.PortRange + err error + ) + + // Empty HostPort means to map to an ephemeral port. + if binding.HostPort != "" { + portRange, err = containertypes.ParsePortRange(binding.HostPort) + if err != nil { + return nil, fmt.Errorf("error parsing HostPort value(%s):%v", binding.HostPort, err) + } } + publishedPorts = append(publishedPorts, types.PortBinding{ - Proto: portProto, - Port: portNum, + Proto: protocol, + Port: port.Num(), HostIP: net.ParseIP(binding.HostIP), - HostPort: uint16(portStart), - HostPortEnd: uint16(portEnd), + HostPort: portRange.Start(), + HostPortEnd: portRange.End(), }) } - if ctr.HostConfig.PublishAllPorts && len(bindings[port]) == 0 { + if ctr.HostConfig.PublishAllPorts && len(bindings) == 0 { publishedPorts = append(publishedPorts, types.PortBinding{ - Proto: portProto, - Port: portNum, + Proto: protocol, + Port: port.Num(), }) } } diff --git a/daemon/container_unix_test.go b/daemon/container_unix_test.go index 6ea76ec29f..2fa48b7bba 100644 --- a/daemon/container_unix_test.go +++ b/daemon/container_unix_test.go @@ -19,7 +19,7 @@ func TestContainerWarningHostAndPublishPorts(t *testing.T) { }{ {ports: containertypes.PortMap{}}, {ports: containertypes.PortMap{ - "8080": []containertypes.PortBinding{{HostPort: "8989"}}, + containertypes.MustParsePort("8080"): []containertypes.PortBinding{{HostPort: "8989"}}, }, warnings: []string{"Published ports are discarded when using host network mode"}}, } muteLogs(t) diff --git a/daemon/containerd/image_import_test.go b/daemon/containerd/image_import_test.go index 5210173c37..de917c3d47 100644 --- a/daemon/containerd/image_import_test.go +++ b/daemon/containerd/image_import_test.go @@ -12,7 +12,7 @@ import ( func TestContainerConfigToDockerImageConfig(t *testing.T) { ociCFG := containerConfigToDockerOCIImageConfig(&container.Config{ ExposedPorts: container.PortSet{ - "80/tcp": struct{}{}, + container.MustParsePort("80/tcp"): struct{}{}, }, }) diff --git a/daemon/containerd/imagespec.go b/daemon/containerd/imagespec.go index 6ec79d8011..0d578c2ce8 100644 --- a/daemon/containerd/imagespec.go +++ b/daemon/containerd/imagespec.go @@ -80,7 +80,7 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock if len(cfg.ExposedPorts) > 0 { ociCfg.ExposedPorts = map[string]struct{}{} for k := range cfg.ExposedPorts { - ociCfg.ExposedPorts[string(k)] = struct{}{} + ociCfg.ExposedPorts[k.String()] = struct{}{} } } ext.Healthcheck = cfg.Healthcheck @@ -97,7 +97,9 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock func dockerOCIImageConfigToContainerConfig(cfg imagespec.DockerOCIImageConfig) *container.Config { exposedPorts := make(container.PortSet, len(cfg.ExposedPorts)) for k := range cfg.ExposedPorts { - exposedPorts[container.PortRangeProto(k)] = struct{}{} + if p, err := container.ParsePort(k); err == nil { + exposedPorts[p] = struct{}{} + } } return &container.Config{ diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go index 8c32cc91fd..dffc715056 100644 --- a/daemon/daemon_test.go +++ b/daemon/daemon_test.go @@ -221,8 +221,8 @@ func TestContainerInitDNS(t *testing.T) { func TestMerge(t *testing.T) { configImage := &containertypes.Config{ ExposedPorts: containertypes.PortSet{ - "1111/tcp": struct{}{}, - "2222/tcp": struct{}{}, + containertypes.MustParsePort("1111/tcp"): struct{}{}, + containertypes.MustParsePort("2222/tcp"): struct{}{}, }, Env: []string{"VAR1=1", "VAR2=2"}, Volumes: map[string]struct{}{ @@ -233,8 +233,8 @@ func TestMerge(t *testing.T) { configUser := &containertypes.Config{ ExposedPorts: containertypes.PortSet{ - "2222/tcp": struct{}{}, - "3333/tcp": struct{}{}, + containertypes.MustParsePort("2222/tcp"): struct{}{}, + containertypes.MustParsePort("3333/tcp"): struct{}{}, }, Env: []string{"VAR2=3", "VAR3=3"}, Volumes: map[string]struct{}{ @@ -250,7 +250,7 @@ func TestMerge(t *testing.T) { t.Fatalf("Expected 3 ExposedPorts, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) } for portSpecs := range configUser.ExposedPorts { - if portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + if portSpecs.Num() != 1111 && portSpecs.Num() != 2222 && portSpecs.Num() != 3333 { t.Fatalf("Expected 1111 or 2222 or 3333, found %s", portSpecs) } } @@ -273,7 +273,9 @@ func TestMerge(t *testing.T) { } configImage2 := &containertypes.Config{ - ExposedPorts: map[containertypes.PortRangeProto]struct{}{"0/tcp": {}}, + ExposedPorts: map[containertypes.Port]struct{}{ + containertypes.MustParsePort("0/tcp"): {}, + }, } if err := merge(configUser, configImage2); err != nil { @@ -284,7 +286,7 @@ func TestMerge(t *testing.T) { t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) } for portSpecs := range configUser.ExposedPorts { - if portSpecs.Port() != "0" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + if portSpecs.Num() != 0 && portSpecs.Num() != 1111 && portSpecs.Num() != 2222 && portSpecs.Num() != 3333 { t.Fatalf("Expected %q or %q or %q or %q, found %s", 0, 1111, 2222, 3333, portSpecs) } } diff --git a/daemon/images/imagespec.go b/daemon/images/imagespec.go index 50dac756b4..a05d66a752 100644 --- a/daemon/images/imagespec.go +++ b/daemon/images/imagespec.go @@ -26,7 +26,7 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock if len(cfg.ExposedPorts) > 0 { ociCfg.ExposedPorts = map[string]struct{}{} for k, v := range cfg.ExposedPorts { - ociCfg.ExposedPorts[string(k)] = v + ociCfg.ExposedPorts[k.String()] = v } } ext.Healthcheck = cfg.Healthcheck diff --git a/daemon/internal/image/cache/compare_test.go b/daemon/internal/image/cache/compare_test.go index b1743797f4..ab458b042c 100644 --- a/daemon/internal/image/cache/compare_test.go +++ b/daemon/internal/image/cache/compare_test.go @@ -12,17 +12,17 @@ import ( func TestCompare(t *testing.T) { ports1 := container.PortSet{ - "1111/tcp": struct{}{}, - "2222/tcp": struct{}{}, + container.MustParsePort("1111/tcp"): struct{}{}, + container.MustParsePort("2222/tcp"): struct{}{}, } ports2 := container.PortSet{ - "3333/tcp": struct{}{}, - "4444/tcp": struct{}{}, + container.MustParsePort("3333/tcp"): struct{}{}, + container.MustParsePort("4444/tcp"): struct{}{}, } ports3 := container.PortSet{ - "1111/tcp": struct{}{}, - "2222/tcp": struct{}{}, - "5555/tcp": struct{}{}, + container.MustParsePort("1111/tcp"): struct{}{}, + container.MustParsePort("2222/tcp"): struct{}{}, + container.MustParsePort("5555/tcp"): struct{}{}, } volumes1 := map[string]struct{}{ "/test1": {}, diff --git a/daemon/links/links.go b/daemon/links/links.go index 6afa0e38ee..a03c90f313 100644 --- a/daemon/links/links.go +++ b/daemon/links/links.go @@ -1,11 +1,13 @@ package links import ( + "cmp" "fmt" + "maps" "path" + "slices" "strings" - "github.com/docker/go-connections/nat" "github.com/moby/moby/api/types/container" ) @@ -20,21 +22,18 @@ type Link struct { // Child environments variables ChildEnvironment []string // Child exposed ports - Ports []container.PortRangeProto // TODO(thaJeztah): can we use []string here, or do we need the features of nat.Port? + Ports []container.Port } // EnvVars generates environment variables for the linked container // for the Link with the given options. -func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[container.PortRangeProto]struct{}) []string { +func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[container.Port]struct{}) []string { return NewLink(parentIP, childIP, name, env, exposedPorts).ToEnv() } // NewLink initializes a new Link struct with the provided options. -func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[container.PortRangeProto]struct{}) *Link { - ports := make([]container.PortRangeProto, 0, len(exposedPorts)) - for p := range exposedPorts { - ports = append(ports, p) - } +func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[container.Port]struct{}) *Link { + ports := slices.Collect(maps.Keys(exposedPorts)) return &Link{ Name: name, @@ -53,27 +52,26 @@ func (l *Link) ToEnv() []string { alias := strings.ReplaceAll(strings.ToUpper(n), "-", "_") // sort the ports so that we can bulk the continuous ports together - nat.Sort(l.Ports, withTCPPriority) + slices.SortFunc(l.Ports, withTCPPriority) - var pStart, pEnd container.PortRangeProto env := make([]string, 0, 1+len(l.Ports)*4) + var pStart, pEnd container.Port + for i, p := range l.Ports { if i == 0 { pStart, pEnd = p, p - env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port())) + env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%d", alias, p.Proto(), l.ChildIP, p.Num())) } - // These env-vars are produced for every port, regardless if they're - // part of a port-range. - prefix := fmt.Sprintf("%s_PORT_%s_%s", alias, p.Port(), strings.ToUpper(p.Proto())) - env = append(env, fmt.Sprintf("%s=%s://%s:%s", prefix, p.Proto(), l.ChildIP, p.Port())) + // These env-vars are produced for every port, regardless if they're part of a port-range. + prefix := fmt.Sprintf("%s_PORT_%d_%s", alias, p.Num(), strings.ToUpper(string(p.Proto()))) + env = append(env, fmt.Sprintf("%s=%s://%s:%d", prefix, p.Proto(), l.ChildIP, p.Num())) env = append(env, fmt.Sprintf("%s_ADDR=%s", prefix, l.ChildIP)) - env = append(env, fmt.Sprintf("%s_PORT=%s", prefix, p.Port())) + env = append(env, fmt.Sprintf("%s_PORT=%d", prefix, p.Num())) env = append(env, fmt.Sprintf("%s_PROTO=%s", prefix, p.Proto())) - // Detect whether this port is part of a range (consecutive port number - // and same protocol). - if p.Int() == pEnd.Int()+1 && strings.EqualFold(p.Proto(), pStart.Proto()) { + // Detect whether this port is part of a range (consecutive port number and same protocol). + if p.Num() == pEnd.Num()+1 && p.Proto() == pEnd.Proto() { pEnd = p if i < len(l.Ports)-1 { continue @@ -81,11 +79,11 @@ func (l *Link) ToEnv() []string { } if pEnd != pStart { - prefix = fmt.Sprintf("%s_PORT_%s_%s", alias, pStart.Port(), strings.ToUpper(pStart.Proto())) - env = append(env, fmt.Sprintf("%s_START=%s://%s:%s", prefix, pStart.Proto(), l.ChildIP, pStart.Port())) - env = append(env, fmt.Sprintf("%s_PORT_START=%s", prefix, pStart.Port())) - env = append(env, fmt.Sprintf("%s_END=%s://%s:%s", prefix, pEnd.Proto(), l.ChildIP, pEnd.Port())) - env = append(env, fmt.Sprintf("%s_PORT_END=%s", prefix, pEnd.Port())) + prefix = fmt.Sprintf("%s_PORT_%d_%s", alias, pStart.Num(), strings.ToUpper(string(pStart.Proto()))) + env = append(env, fmt.Sprintf("%s_START=%s://%s:%d", prefix, pStart.Proto(), l.ChildIP, pStart.Num())) + env = append(env, fmt.Sprintf("%s_PORT_START=%d", prefix, pStart.Num())) + env = append(env, fmt.Sprintf("%s_END=%s://%s:%d", prefix, pEnd.Proto(), l.ChildIP, pEnd.Num())) + env = append(env, fmt.Sprintf("%s_PORT_END=%d", prefix, pEnd.Num())) } // Reset for next range (if any) @@ -113,17 +111,15 @@ func (l *Link) ToEnv() []string { // withTCPPriority prioritizes ports using TCP over other protocols before // comparing port-number and protocol. -func withTCPPriority(ip, jp container.PortRangeProto) bool { - if strings.EqualFold(ip.Proto(), jp.Proto()) { - return ip.Int() < jp.Int() +func withTCPPriority(ip, jp container.Port) int { + if ip.Proto() == jp.Proto() { + return cmp.Compare(ip.Num(), jp.Num()) } - - if strings.EqualFold(ip.Proto(), "tcp") { - return true + if ip.Proto() == container.TCP { + return -1 } - if strings.EqualFold(jp.Proto(), "tcp") { - return false + if jp.Proto() == container.TCP { + return 1 } - - return strings.ToLower(ip.Proto()) < strings.ToLower(jp.Proto()) + return cmp.Compare(ip.Proto(), jp.Proto()) } diff --git a/daemon/links/links_test.go b/daemon/links/links_test.go index e96cd3500d..68356f4b3e 100644 --- a/daemon/links/links_test.go +++ b/daemon/links/links_test.go @@ -1,17 +1,18 @@ package links import ( + "slices" "sort" "testing" - "github.com/docker/go-connections/nat" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/api/types/container" "gotest.tools/v3/assert" ) func TestLinkNaming(t *testing.T) { actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, container.PortSet{ - "6379/tcp": struct{}{}, + container.MustParsePort("6379/tcp"): struct{}{}, }) expectedEnv := []string{ @@ -28,23 +29,25 @@ func TestLinkNaming(t *testing.T) { } func TestLinkNew(t *testing.T) { + tcp6379 := container.MustParsePort("6379/tcp") link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", nil, container.PortSet{ - "6379/tcp": struct{}{}, + tcp6379: struct{}{}, }) expected := &Link{ Name: "/db/docker", ParentIP: "172.0.17.3", ChildIP: "172.0.17.2", - Ports: []container.PortRangeProto{"6379/tcp"}, + Ports: []container.Port{tcp6379}, } - assert.DeepEqual(t, expected, link) + assert.DeepEqual(t, expected, link, cmpopts.EquateComparable(container.Port{})) } func TestLinkEnv(t *testing.T) { + tcp6379 := container.MustParsePort("6379/tcp") actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{ - "6379/tcp": struct{}{}, + tcp6379: struct{}{}, }) expectedEnv := []string{ @@ -64,39 +67,39 @@ func TestLinkEnv(t *testing.T) { // TestSortPorts verifies that ports are sorted with TCP taking priority, // and ports with the same protocol to be sorted by port. func TestSortPorts(t *testing.T) { - ports := []container.PortRangeProto{ - "6379/tcp", - "6376/udp", - "6380/tcp", - "6376/sctp", - "6381/tcp", - "6381/udp", - "6375/udp", - "6375/sctp", + ports := []container.Port{ + container.MustParsePort("6379/tcp"), + container.MustParsePort("6376/udp"), + container.MustParsePort("6380/tcp"), + container.MustParsePort("6376/sctp"), + container.MustParsePort("6381/tcp"), + container.MustParsePort("6381/udp"), + container.MustParsePort("6375/udp"), + container.MustParsePort("6375/sctp"), } - expected := []container.PortRangeProto{ - "6379/tcp", - "6380/tcp", - "6381/tcp", - "6375/sctp", - "6376/sctp", - "6375/udp", - "6376/udp", - "6381/udp", + expected := []container.Port{ + container.MustParsePort("6379/tcp"), + container.MustParsePort("6380/tcp"), + container.MustParsePort("6381/tcp"), + container.MustParsePort("6375/sctp"), + container.MustParsePort("6376/sctp"), + container.MustParsePort("6375/udp"), + container.MustParsePort("6376/udp"), + container.MustParsePort("6381/udp"), } - nat.Sort(ports, withTCPPriority) - assert.DeepEqual(t, expected, ports) + slices.SortFunc(ports, withTCPPriority) + assert.DeepEqual(t, expected, ports, cmpopts.EquateComparable(container.Port{})) } func TestLinkMultipleEnv(t *testing.T) { actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{ - "6300/udp": struct{}{}, - "6379/tcp": struct{}{}, - "6380/tcp": struct{}{}, - "6381/tcp": struct{}{}, - "6382/udp": struct{}{}, + container.MustParsePort("6300/udp"): struct{}{}, + container.MustParsePort("6379/tcp"): struct{}{}, + container.MustParsePort("6380/tcp"): struct{}{}, + container.MustParsePort("6381/tcp"): struct{}{}, + container.MustParsePort("6382/udp"): struct{}{}, }) expectedEnv := []string{ @@ -143,11 +146,11 @@ func BenchmarkLinkMultipleEnv(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { _ = EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{ - "6300/udp": struct{}{}, - "6379/tcp": struct{}{}, - "6380/tcp": struct{}{}, - "6381/tcp": struct{}{}, - "6382/udp": struct{}{}, + container.MustParsePort("6300/udp"): struct{}{}, + container.MustParsePort("6379/tcp"): struct{}{}, + container.MustParsePort("6380/tcp"): struct{}{}, + container.MustParsePort("6381/tcp"): struct{}{}, + container.MustParsePort("6382/udp"): struct{}{}, }) } } diff --git a/daemon/list.go b/daemon/list.go index e50564255a..7129a978ed 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -11,7 +11,6 @@ import ( cerrdefs "github.com/containerd/errdefs" "github.com/containerd/log" - "github.com/docker/go-connections/nat" containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/filters" "github.com/moby/moby/v2/daemon/container" @@ -404,13 +403,12 @@ func portOp(key string, filter map[string]bool) func(value string) error { return fmt.Errorf("filter for '%s' should not contain ':': %s", key, value) } // support two formats, original format /[] or /[] - proto, portRange := nat.SplitProtoPort(value) - start, end, err := nat.ParsePortRange(portRange) + portRange, err := containertypes.ParsePortRange(value) if err != nil { return fmt.Errorf("error while looking up for %s %s: %s", key, value, err) } - for portNum := start; portNum <= end; portNum++ { - filter[fmt.Sprintf("%d/%s", portNum, proto)] = true + for p := range portRange.All() { + filter[p.String()] = true } return nil } diff --git a/daemon/network.go b/daemon/network.go index 018351c8aa..10745ceb46 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -4,17 +4,14 @@ import ( "context" "errors" "fmt" - "maps" "net" "net/netip" - "slices" "sort" "strconv" "strings" "sync" "github.com/containerd/log" - "github.com/docker/go-connections/nat" containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/events" networktypes "github.com/moby/moby/api/types/network" @@ -960,51 +957,44 @@ func buildPortsRelatedCreateEndpointOptions(c *container.Container, n *libnetwor return nil, nil } - // Create a deep copy (as [nat.SortPortMap] mutates the map). - // Not using a maps.Clone here, as that won't dereference the - // slice (PortMap is a map[Port][]PortBinding). - bindings := make(containertypes.PortMap) - for p, b := range c.HostConfig.PortBindings { - bindings[p] = slices.Clone(b) - } - - ports := slices.Collect(maps.Keys(bindings)) - nat.SortPortMap(ports, bindings) - var ( exposedPorts []lntypes.TransportPort publishedPorts []lntypes.PortBinding ) - for _, port := range ports { - portProto := lntypes.ParseProtocol(port.Proto()) - portNum := uint16(port.Int()) + for p, bindings := range c.HostConfig.PortBindings { + protocol := lntypes.ParseProtocol(string(p.Proto())) exposedPorts = append(exposedPorts, lntypes.TransportPort{ - Proto: portProto, - Port: portNum, + Proto: protocol, + Port: p.Num(), }) - for _, binding := range bindings[port] { - newP, err := nat.NewPort(nat.SplitProtoPort(binding.HostPort)) - var portStart, portEnd int - if err == nil { - portStart, portEnd, err = newP.Range() - } - if err != nil { - return nil, fmt.Errorf("error parsing HostPort value (%s): %w", binding.HostPort, err) + for _, binding := range bindings { + var ( + portRange containertypes.PortRange + err error + ) + + // Empty HostPort means to map to an ephemeral port. + if binding.HostPort != "" { + portRange, err = containertypes.ParsePortRange(binding.HostPort) + if err != nil { + return nil, fmt.Errorf("error parsing HostPort value(%s):%v", binding.HostPort, err) + } } + publishedPorts = append(publishedPorts, lntypes.PortBinding{ - Proto: portProto, - Port: portNum, + Proto: protocol, + Port: p.Num(), HostIP: net.ParseIP(binding.HostIP), - HostPort: uint16(portStart), - HostPortEnd: uint16(portEnd), + HostPort: portRange.Start(), + HostPortEnd: portRange.End(), }) } - if c.HostConfig.PublishAllPorts && len(bindings[port]) == 0 { + if c.HostConfig.PublishAllPorts && len(bindings) == 0 { publishedPorts = append(publishedPorts, lntypes.PortBinding{ - Proto: portProto, - Port: portNum, + Proto: protocol, + Port: p.Num(), }) } } @@ -1038,9 +1028,9 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint) if expData, ok := driverInfo[netlabel.ExposedPorts]; ok { if exposedPorts, ok := expData.([]lntypes.TransportPort); ok { for _, tp := range exposedPorts { - natPort, err := nat.NewPort(tp.Proto.String(), strconv.Itoa(int(tp.Port))) - if err != nil { - log.G(context.TODO()).Errorf("invalid exposed port %s: %v", tp.String(), err) + natPort, ok := containertypes.PortFrom(tp.Port, containertypes.NetworkProtocol(tp.Proto.String())) + if !ok { + log.G(context.TODO()).Errorf("Invalid exposed port: %s", tp.String()) continue } if _, ok := pm[natPort]; !ok { @@ -1057,12 +1047,13 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint) if portMapping, ok := mapData.([]lntypes.PortBinding); ok { for _, pp := range portMapping { - // Use an empty string for the host port if there's no port assigned. - natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port))) - if err != nil { - log.G(context.TODO()).Errorf("invalid port binding %s: %v", pp, err) + // Use an empty string for the host natPort if there's no natPort assigned. + natPort, ok := containertypes.PortFrom(pp.Port, containertypes.NetworkProtocol(pp.Proto.String())) + if !ok { + log.G(context.TODO()).Errorf("Invalid port binding: %s", pp.String()) continue } + var hp string if pp.HostPort > 0 { hp = strconv.Itoa(int(pp.HostPort)) diff --git a/daemon/server/router/container/container_routes.go b/daemon/server/router/container/container_routes.go index 35ecc41d8c..fa4466a2aa 100644 --- a/daemon/server/router/container/container_routes.go +++ b/daemon/server/router/container/container_routes.go @@ -12,7 +12,6 @@ import ( "github.com/containerd/log" "github.com/containerd/platforms" - "github.com/docker/go-connections/nat" "github.com/moby/moby/api/types" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/filters" @@ -891,7 +890,7 @@ func handleSysctlBC( func handlePortBindingsBC(hostConfig *container.HostConfig, version string) string { var emptyPBs []string - for portProto, bindings := range hostConfig.PortBindings { + for port, bindings := range hostConfig.PortBindings { if len(bindings) > 0 { continue } @@ -902,15 +901,15 @@ func handlePortBindingsBC(hostConfig *container.HostConfig, version string) stri // on-disk state for containers created by older versions of the // Engine. Drop the PortBindings entry to ensure that no backfilling // will happen when restarting the daemon. - delete(hostConfig.PortBindings, portProto) + delete(hostConfig.PortBindings, port) continue } if versions.Equal(version, "1.52") { - emptyPBs = append(emptyPBs, string(portProto)) + emptyPBs = append(emptyPBs, port.String()) } - hostConfig.PortBindings[portProto] = []nat.PortBinding{{}} + hostConfig.PortBindings[port] = []container.PortBinding{{}} } if len(emptyPBs) > 0 { diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 3af87a271e..6cddb65e27 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -505,7 +505,7 @@ func (s *DockerAPISuite) TestContainerAPIBadPort(c *testing.T) { hostConfig := container.HostConfig{ PortBindings: container.PortMap{ - "8080/tcp": []container.PortBinding{ + container.MustParsePort("8080/tcp"): []container.PortBinding{ { HostIP: "", HostPort: "aa80", diff --git a/integration-cli/docker_cli_create_test.go b/integration-cli/docker_cli_create_test.go index d686b2df26..4eae2630e0 100644 --- a/integration-cli/docker_cli_create_test.go +++ b/integration-cli/docker_cli_create_test.go @@ -92,7 +92,7 @@ func (s *DockerCLICreateSuite) TestCreateWithPortRange(c *testing.T) { var containers []struct { HostConfig *struct { - PortBindings map[containertypes.PortRangeProto][]containertypes.PortBinding + PortBindings map[containertypes.Port][]containertypes.PortBinding } } err := json.Unmarshal([]byte(out), &containers) @@ -105,8 +105,8 @@ func (s *DockerCLICreateSuite) TestCreateWithPortRange(c *testing.T) { assert.Equal(c, len(cont.HostConfig.PortBindings), 4, fmt.Sprintf("Expected 4 ports bindings, got %d", len(cont.HostConfig.PortBindings))) for k, v := range cont.HostConfig.PortBindings { - assert.Equal(c, len(v), 1, fmt.Sprintf("Expected 1 ports binding, for the port %s but found %s", k, v)) - assert.Equal(c, k.Port(), v[0].HostPort, fmt.Sprintf("Expected host port %s to match published port %s", k.Port(), v[0].HostPort)) + assert.Equal(c, len(v), 1, fmt.Sprintf("Expected 1 ports binding, for the port %s but found %s", k, v)) + assert.Equal(c, fmt.Sprintf("%d", k.Num()), v[0].HostPort, fmt.Sprintf("Expected host port %d to match published port %s", k.Num(), v[0].HostPort)) } } @@ -118,7 +118,7 @@ func (s *DockerCLICreateSuite) TestCreateWithLargePortRange(c *testing.T) { var containers []struct { HostConfig *struct { - PortBindings map[containertypes.PortRangeProto][]containertypes.PortBinding + PortBindings map[containertypes.Port][]containertypes.PortBinding } } @@ -132,7 +132,7 @@ func (s *DockerCLICreateSuite) TestCreateWithLargePortRange(c *testing.T) { for k, v := range cont.HostConfig.PortBindings { assert.Equal(c, len(v), 1) - assert.Equal(c, k.Port(), v[0].HostPort, fmt.Sprintf("Expected host port %s to match published port %s", k.Port(), v[0].HostPort)) + assert.Equal(c, fmt.Sprintf("%d", k.Num()), v[0].HostPort, fmt.Sprintf("Expected host port %d to match published port %s", k.Num(), v[0].HostPort)) } } diff --git a/integration-cli/docker_cli_port_test.go b/integration-cli/docker_cli_port_test.go index 5ff8b9696b..37b63798ed 100644 --- a/integration-cli/docker_cli_port_test.go +++ b/integration-cli/docker_cli_port_test.go @@ -197,11 +197,11 @@ func assertPortRange(ctx context.Context, id string, expectedTCP, expectedUDP [] } var validTCP, validUDP bool - for portAndProto, binding := range inspect.NetworkSettings.Ports { - if portAndProto.Proto() == "tcp" && len(expectedTCP) == 0 { + for port, binding := range inspect.NetworkSettings.Ports { + if port.Proto() == "tcp" && len(expectedTCP) == 0 { continue } - if portAndProto.Proto() == "udp" && len(expectedTCP) == 0 { + if port.Proto() == "udp" && len(expectedTCP) == 0 { continue } diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 02c87c5199..b1c68bc4d8 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2173,9 +2173,8 @@ func (s *DockerCLIRunSuite) TestRunAllowPortRangeThroughExpose(c *testing.T) { c.Fatal(err) } for port, binding := range ports { - portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) - if portnum < 3000 || portnum > 3003 { - c.Fatalf("Port %d is out of range ", portnum) + if port.Num() < 3000 || port.Num() > 3003 { + c.Fatalf("Port %d is out of range", port.Num()) } if len(binding) == 0 || binding[0].HostPort == "" { c.Fatalf("Port is not mapped for the port %s", port) @@ -2510,12 +2509,11 @@ func (s *DockerCLIRunSuite) TestRunAllowPortRangeThroughPublish(c *testing.T) { err := json.Unmarshal([]byte(portStr), &ports) assert.NilError(c, err, "failed to unmarshal: %v", portStr) for port, binding := range ports { - portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) - if portnum < 3000 || portnum > 3003 { - c.Fatalf("Port %d is out of range ", portnum) + if port.Num() < 3000 || port.Num() > 3003 { + c.Fatalf("Port %d is out of range", port.Num()) } if len(binding) == 0 || binding[0].HostPort == "" { - c.Fatal("Port is not mapped for the port "+port, id) + c.Fatalf("Port is not mapped for the port %s", port) } } } diff --git a/integration/container/daemon_test.go b/integration/container/daemon_test.go index f4482a0bd9..fbf0fee983 100644 --- a/integration/container/daemon_test.go +++ b/integration/container/daemon_test.go @@ -71,12 +71,13 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) { defer d.Stop(t) apiClient := d.NewClientT(t) + mappedPort := containertypes.MustParsePort("80/tcp") // The intention of this container is to ignore stop signals. // Sadly this means the test will take longer, but at least this test can be parallelized. cid := container.Run(ctx, t, apiClient, container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{}}}), + container.WithPortMap(containertypes.PortMap{mappedPort: {{}}}), container.WithCmd("/bin/sh", "-c", "while true; do echo hello; sleep 1; done")) defer func() { err := apiClient.ContainerRemove(ctx, cid, client.ContainerRemoveOptions{Force: true}) @@ -87,7 +88,7 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) { assert.NilError(t, err) assert.Assert(t, inspect.NetworkSettings.SandboxID != "") assert.Assert(t, inspect.NetworkSettings.SandboxKey != "") - assert.Assert(t, inspect.NetworkSettings.Ports["80/tcp"] != nil) + assert.Assert(t, inspect.NetworkSettings.Ports[mappedPort] != nil) assert.NilError(t, d.Kill()) d.Start(t) @@ -96,5 +97,5 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) { assert.NilError(t, err) assert.Assert(t, inspect.NetworkSettings.SandboxID == "") assert.Assert(t, inspect.NetworkSettings.SandboxKey == "") - assert.Assert(t, is.Nil(inspect.NetworkSettings.Ports["80/tcp"])) + assert.Assert(t, is.Nil(inspect.NetworkSettings.Ports[mappedPort])) } diff --git a/integration/container/nat_test.go b/integration/container/nat_test.go index 347fe46298..6e2eb2df9f 100644 --- a/integration/container/nat_test.go +++ b/integration/container/nat_test.go @@ -113,7 +113,7 @@ func TestNetworkLoopbackNat(t *testing.T) { assert.Check(t, is.Equal(msg, strings.TrimSpace(b.String()))) } -func startServerContainer(ctx context.Context, t *testing.T, msg string, port int) string { +func startServerContainer(ctx context.Context, t *testing.T, msg string, port uint16) string { t.Helper() apiClient := testEnv.APIClient() @@ -123,7 +123,7 @@ func startServerContainer(ctx context.Context, t *testing.T, msg string, port in container.WithExposedPorts(fmt.Sprintf("%d/tcp", port)), func(c *container.TestContainerConfig) { c.HostConfig.PortBindings = containertypes.PortMap{ - containertypes.PortRangeProto(fmt.Sprintf("%d/tcp", port)): []containertypes.PortBinding{ + containertypes.MustParsePort(fmt.Sprintf("%d/tcp", port)): []containertypes.PortBinding{ { HostPort: fmt.Sprintf("%d", port), }, diff --git a/integration/internal/container/ops.go b/integration/internal/container/ops.go index d85d71efdc..e407df9d75 100644 --- a/integration/internal/container/ops.go +++ b/integration/internal/container/ops.go @@ -73,9 +73,10 @@ func WithSysctls(sysctls map[string]string) func(*TestContainerConfig) { // WithExposedPorts sets the exposed ports of the container func WithExposedPorts(ports ...string) func(*TestContainerConfig) { return func(c *TestContainerConfig) { - c.Config.ExposedPorts = map[container.PortRangeProto]struct{}{} + c.Config.ExposedPorts = map[container.Port]struct{}{} for _, port := range ports { - c.Config.ExposedPorts[container.PortRangeProto(port)] = struct{}{} + p, _ := container.ParsePort(port) + c.Config.ExposedPorts[p] = struct{}{} } } } diff --git a/integration/network/bridge/bridge_linux_test.go b/integration/network/bridge/bridge_linux_test.go index 45be6bec9a..ffca2c5de1 100644 --- a/integration/network/bridge/bridge_linux_test.go +++ b/integration/network/bridge/bridge_linux_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/docker/go-connections/nat" containertypes "github.com/moby/moby/api/types/container" networktypes "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/versions" @@ -518,18 +517,19 @@ func TestEndpointWithCustomIfname(t *testing.T) { func TestPublishedPortAlreadyInUse(t *testing.T) { ctx := setupTest(t) apiClient := testEnv.APIClient() + mappedPort := containertypes.MustParsePort("80/tcp") ctr1 := ctr.Run(ctx, t, apiClient, ctr.WithCmd("top"), ctr.WithExposedPorts("80/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}})) + ctr.WithPortMap(containertypes.PortMap{mappedPort: {{HostPort: "8000"}}})) defer ctr.Remove(ctx, t, apiClient, ctr1, client.ContainerRemoveOptions{Force: true}) ctr2 := ctr.Create(ctx, t, apiClient, ctr.WithCmd("top"), ctr.WithRestartPolicy(containertypes.RestartPolicyAlways), ctr.WithExposedPorts("80/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}})) + ctr.WithPortMap(containertypes.PortMap{mappedPort: {{HostPort: "8000"}}})) defer ctr.Remove(ctx, t, apiClient, ctr2, client.ContainerRemoveOptions{Force: true}) err := apiClient.ContainerStart(ctx, ctr2, client.ContainerStartOptions{}) @@ -564,18 +564,18 @@ func TestAllPortMappingsAreReturned(t *testing.T) { ctrID := ctr.Run(ctx, t, apiClient, ctr.WithExposedPorts("80/tcp", "81/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}}), + ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}), ctr.WithEndpointSettings("testnetv4", &networktypes.EndpointSettings{}), ctr.WithEndpointSettings("testnetv6", &networktypes.EndpointSettings{})) defer ctr.Remove(ctx, t, apiClient, ctrID, client.ContainerRemoveOptions{Force: true}) inspect := ctr.Inspect(ctx, t, apiClient, ctrID) assert.DeepEqual(t, inspect.NetworkSettings.Ports, containertypes.PortMap{ - "80/tcp": []containertypes.PortBinding{ + containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{ {HostIP: "0.0.0.0", HostPort: "8000"}, {HostIP: "::", HostPort: "8000"}, }, - "81/tcp": nil, + containertypes.MustParsePort("81/tcp"): nil, }) } @@ -603,7 +603,7 @@ func TestFirewalldReloadNoZombies(t *testing.T) { cid := ctr.Run(ctx, t, c, ctr.WithExposedPorts("80/tcp", "81/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}})) + ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}})) defer func() { if !removed { ctr.Remove(ctx, t, c, cid, client.ContainerRemoveOptions{Force: true}) @@ -793,7 +793,7 @@ func TestPortMappingRestore(t *testing.T) { const svrName = "svr" cid := ctr.Run(ctx, t, c, ctr.WithExposedPorts("80/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": {}}), + ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}), ctr.WithName(svrName), ctr.WithRestartPolicy(containertypes.RestartPolicyUnlessStopped), ctr.WithCmd("httpd", "-f"), @@ -804,9 +804,9 @@ func TestPortMappingRestore(t *testing.T) { t.Helper() insp := ctr.Inspect(ctx, t, c, cid) assert.Check(t, is.Equal(insp.State.Running, true)) - if assert.Check(t, is.Contains(insp.NetworkSettings.Ports, containertypes.PortRangeProto("80/tcp"))) && - assert.Check(t, is.Len(insp.NetworkSettings.Ports["80/tcp"], 2)) { - hostPort := insp.NetworkSettings.Ports["80/tcp"][0].HostPort + if assert.Check(t, is.Contains(insp.NetworkSettings.Ports, containertypes.MustParsePort("80/tcp"))) && + assert.Check(t, is.Len(insp.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")], 2)) { + hostPort := insp.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort res := ctr.RunAttach(ctx, t, c, ctr.WithExtraHost("thehost:host-gateway"), ctr.WithCmd("wget", "-T3", "http://"+net.JoinHostPort("thehost", hostPort)), @@ -951,7 +951,7 @@ func TestEmptyPortBindingsBC(t *testing.T) { d.StartWithBusybox(ctx, t) defer d.Stop(t) - createInspect := func(t *testing.T, version string, pbs []nat.PortBinding) (containertypes.PortMap, []string) { + createInspect := func(t *testing.T, version string, pbs []containertypes.PortBinding) (containertypes.PortMap, []string) { apiClient := d.NewClientT(t, client.WithVersion(version)) defer apiClient.Close() @@ -966,7 +966,7 @@ func TestEmptyPortBindingsBC(t *testing.T) { // Create a container with an empty list of port bindings for container port 80/tcp. config := ctr.NewTestConfig(ctr.WithCmd("top"), ctr.WithExposedPorts("80/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": pbs})) + ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): pbs})) c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name) assert.NilError(t, err) defer apiClient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true}) @@ -979,25 +979,25 @@ func TestEmptyPortBindingsBC(t *testing.T) { } t.Run("backfilling on old client version", func(t *testing.T) { - expMappings := containertypes.PortMap{"80/tcp": { + expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): { {}, // An empty PortBinding is backfilled }} expWarnings := make([]string, 0) - mappings, warnings := createInspect(t, "1.51", []nat.PortBinding{}) + mappings, warnings := createInspect(t, "1.51", []containertypes.PortBinding{}) assert.DeepEqual(t, expMappings, mappings) assert.DeepEqual(t, expWarnings, warnings) }) t.Run("backfilling on API 1.52, with a warning", func(t *testing.T) { - expMappings := containertypes.PortMap{"80/tcp": { + expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): { {}, // An empty PortBinding is backfilled }} expWarnings := []string{ "Following container port(s) have an empty list of port-bindings: 80/tcp. Starting with API 1.53, such bindings will be discarded.", } - mappings, warnings := createInspect(t, "1.52", []nat.PortBinding{}) + mappings, warnings := createInspect(t, "1.52", []containertypes.PortBinding{}) assert.DeepEqual(t, expMappings, mappings) assert.DeepEqual(t, expWarnings, warnings) }) @@ -1006,19 +1006,19 @@ func TestEmptyPortBindingsBC(t *testing.T) { expMappings := containertypes.PortMap{} expWarnings := make([]string, 0) - mappings, warnings := createInspect(t, "1.53", []nat.PortBinding{}) + mappings, warnings := createInspect(t, "1.53", []containertypes.PortBinding{}) assert.DeepEqual(t, expMappings, mappings) assert.DeepEqual(t, expWarnings, warnings) }) for _, apiVersion := range []string{"1.51", "1.52", "1.53"} { t.Run("no backfilling on API "+apiVersion+" with non-empty bindings", func(t *testing.T) { - expMappings := containertypes.PortMap{"80/tcp": { + expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): { {HostPort: "8080"}, }} expWarnings := make([]string, 0) - mappings, warnings := createInspect(t, apiVersion, []nat.PortBinding{{HostPort: "8080"}}) + mappings, warnings := createInspect(t, apiVersion, []containertypes.PortBinding{{HostPort: "8080"}}) assert.DeepEqual(t, expMappings, mappings) assert.DeepEqual(t, expWarnings, warnings) }) @@ -1043,7 +1043,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) { cid := ctr.Create(ctx, t, c, ctr.WithExposedPorts("80/tcp"), - ctr.WithPortMap(containertypes.PortMap{"80/tcp": {}})) + ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}})) defer c.ContainerRemove(ctx, cid, client.ContainerRemoveOptions{Force: true}) // Stop the daemon to safely tamper with the on-disk state. @@ -1052,7 +1052,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) { d.TamperWithContainerConfig(t, cid, func(container *container.Container) { // Simulate a container created with an older version of the Engine // by setting an empty list of port bindings. - container.HostConfig.PortBindings = containertypes.PortMap{"80/tcp": {}} + container.HostConfig.PortBindings = containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}} }) // Restart the daemon — it should backfill the empty port binding slice. @@ -1060,7 +1060,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) { inspect := ctr.Inspect(ctx, t, c, cid) - expMappings := containertypes.PortMap{"80/tcp": { + expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): { {}, // An empty PortBinding is backfilled }} assert.DeepEqual(t, expMappings, inspect.HostConfig.PortBindings) diff --git a/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go b/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go index d654fca6eb..3aeff2645d 100644 --- a/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go +++ b/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go @@ -82,7 +82,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -95,7 +95,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -108,7 +108,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -142,7 +142,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -155,7 +155,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -167,7 +167,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -179,7 +179,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostIP: "127.0.0.1", HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: "127.0.0.1", HostPort: "8080"}}}, }, }, }}, @@ -327,7 +327,7 @@ func createBridgeNetworks(ctx context.Context, t *testing.T, d *daemon.Daemon, s for _, ctr := range nw.containers { var exposedPorts []string for ep := range ctr.portMappings { - exposedPorts = append(exposedPorts, ep.Port()+"/"+ep.Proto()) + exposedPorts = append(exposedPorts, ep.String()) } id := container.Run(ctx, t, c, container.WithNetworkMode(nw.name), @@ -356,7 +356,7 @@ func createServices(ctx context.Context, t *testing.T, d *daemon.Daemon, section portConfig = append(portConfig, swarmtypes.PortConfig{ Protocol: swarmtypes.PortConfigProtocol(ctrPP.Proto()), PublishedPort: uint32(hp), - TargetPort: uint32(ctrPP.Int()), + TargetPort: uint32(ctrPP.Num()), }) } } diff --git a/integration/network/bridge/nftablesdoc/nftablesdoc_linux_test.go b/integration/network/bridge/nftablesdoc/nftablesdoc_linux_test.go index e9890e92e5..fc249aa8d3 100644 --- a/integration/network/bridge/nftablesdoc/nftablesdoc_linux_test.go +++ b/integration/network/bridge/nftablesdoc/nftablesdoc_linux_test.go @@ -79,7 +79,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -92,7 +92,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -105,7 +105,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -139,7 +139,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -152,7 +152,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}, }, }, }}, @@ -178,7 +178,7 @@ var index = []section{ containers: []ctrDesc{ { name: "c1", - portMappings: containertypes.PortMap{"80/tcp": {{HostIP: "127.0.0.1", HostPort: "8080"}}}, + portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: "127.0.0.1", HostPort: "8080"}}}, }, }, }}, @@ -300,7 +300,7 @@ func createBridgeNetworks(ctx context.Context, t *testing.T, d *daemon.Daemon, s for _, ctr := range nw.containers { var exposedPorts []string for ep := range ctr.portMappings { - exposedPorts = append(exposedPorts, ep.Port()+"/"+ep.Proto()) + exposedPorts = append(exposedPorts, ep.String()) } id := container.Run(ctx, t, c, container.WithNetworkMode(nw.name), diff --git a/integration/networking/bridge_linux_test.go b/integration/networking/bridge_linux_test.go index 0418c75767..2764115e72 100644 --- a/integration/networking/bridge_linux_test.go +++ b/integration/networking/bridge_linux_test.go @@ -388,7 +388,7 @@ func TestBridgeINCRouted(t *testing.T) { container.WithNetworkMode(netName), container.WithName("ctr-"+gwMode), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}), ) t.Cleanup(func() { c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true}) @@ -565,7 +565,7 @@ func TestAccessToPublishedPort(t *testing.T) { container.WithNetworkMode(serverNetName), container.WithName("ctr-server"), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {containertypes.PortBinding{HostPort: "8080"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {containertypes.PortBinding{HostPort: "8080"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true}) @@ -688,7 +688,7 @@ func TestInterNetworkDirectRouting(t *testing.T) { container.WithNetworkMode(serverNetName), container.WithName("ctr-pub"), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {containertypes.PortBinding{HostPort: "8080"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {containertypes.PortBinding{HostPort: "8080"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, ctrPubId, client.ContainerRemoveOptions{Force: true}) @@ -1063,7 +1063,7 @@ func TestDisableIPv6OnInterface(t *testing.T) { container.WithName(ctrName), container.WithNetworkMode(tc.netName), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}), container.WithEndpointSettings(tc.netName, &networktypes.EndpointSettings{ DriverOpts: map[string]string{ netlabel.EndpointSysctls: "net.ipv6.conf.IFNAME.disable_ipv6=1", @@ -1504,7 +1504,7 @@ func TestGatewaySelection(t *testing.T) { container.WithName(ctrName), container.WithNetworkMode(netName4), container.WithExposedPorts("80"), - container.WithPortMap(containertypes.PortMap{"80": {{HostPort: "8080"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostPort: "8080"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true}) @@ -1956,7 +1956,7 @@ func TestDropInForwardChain(t *testing.T) { ctrId := container.Run(ctx, t, c, container.WithNetworkMode(netName46), container.WithExposedPorts("80"), - container.WithPortMap(containertypes.PortMap{"80": {{HostPort: hostPort}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostPort: hostPort}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true}) diff --git a/integration/networking/nat_windows_test.go b/integration/networking/nat_windows_test.go index b1d51c5833..2668132a50 100644 --- a/integration/networking/nat_windows_test.go +++ b/integration/networking/nat_windows_test.go @@ -97,13 +97,13 @@ func TestFlakyPortMappedHairpinWindows(t *testing.T) { serverId := container.Run(ctx, t, c, container.WithNetworkMode(serverNetName), container.WithExposedPorts("80"), - container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "0.0.0.0"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: "0.0.0.0"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true}) inspect := container.Inspect(ctx, t, c, serverId) - hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort + hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort attachCtx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() diff --git a/integration/networking/port_mapping_linux_test.go b/integration/networking/port_mapping_linux_test.go index a32fb892c1..d59405029a 100644 --- a/integration/networking/port_mapping_linux_test.go +++ b/integration/networking/port_mapping_linux_test.go @@ -73,7 +73,7 @@ func TestDisableNAT(t *testing.T) { { name: "defaults", expPortMap: containertypes.PortMap{ - "80/tcp": []containertypes.PortBinding{ + containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{ {HostIP: "0.0.0.0", HostPort: "8080"}, {HostIP: "::", HostPort: "8080"}, }, @@ -84,7 +84,7 @@ func TestDisableNAT(t *testing.T) { gwMode4: "nat", gwMode6: "routed", expPortMap: containertypes.PortMap{ - "80/tcp": []containertypes.PortBinding{ + containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{ {HostIP: "0.0.0.0", HostPort: "8080"}, {HostIP: "::", HostPort: ""}, }, @@ -95,7 +95,7 @@ func TestDisableNAT(t *testing.T) { gwMode4: "routed", gwMode6: "nat", expPortMap: containertypes.PortMap{ - "80/tcp": []containertypes.PortBinding{ + containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{ {HostIP: "::", HostPort: "8080"}, {HostIP: "0.0.0.0", HostPort: ""}, }, @@ -124,7 +124,7 @@ func TestDisableNAT(t *testing.T) { id := container.Run(ctx, t, c, container.WithNetworkMode(netName), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}), ) defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true}) @@ -162,13 +162,13 @@ func TestPortMappedHairpinTCP(t *testing.T) { serverId := container.Run(ctx, t, c, container.WithNetworkMode(serverNetName), container.WithExposedPorts("80"), - container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "0.0.0.0"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: "0.0.0.0"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true}) inspect := container.Inspect(ctx, t, c, serverId) - hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort + hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort clientCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() @@ -209,13 +209,13 @@ func TestPortMappedHairpinUDP(t *testing.T) { serverId := container.Run(ctx, t, c, container.WithNetworkMode(serverNetName), container.WithExposedPorts("54/udp"), - container.WithPortMap(containertypes.PortMap{"54/udp": {{HostIP: "0.0.0.0"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("54/udp"): {{HostIP: "0.0.0.0"}}}), container.WithCmd("/bin/sh", "-c", "echo 'foobar.internal 192.168.155.23' | dnsd -c - -p 54"), ) defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true}) inspect := container.Inspect(ctx, t, c, serverId) - hostPort := inspect.NetworkSettings.Ports["54/udp"][0].HostPort + hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("54/udp")][0].HostPort // nslookup gets an answer quickly from the dns server, but then tries to // query another DNS server (for some unknown reasons) and times out. Hence, @@ -251,13 +251,13 @@ func TestProxy4To6(t *testing.T) { serverId := container.Run(ctx, t, c, container.WithNetworkMode(netName), container.WithExposedPorts("80"), - container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "::1"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: "::1"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true}) inspect := container.Inspect(ctx, t, c, serverId) - hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort + hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort var resp *http.Response addr := "http://[::1]:" + hostPort @@ -375,7 +375,7 @@ func TestAccessPublishedPortFromHost(t *testing.T) { serverID := container.Run(ctx, t, c, container.WithName(sanitizeCtrName(t.Name()+"-server")), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: hostPort}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}), container.WithCmd("httpd", "-f"), container.WithNetworkMode(bridgeName)) defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true}) @@ -455,7 +455,7 @@ func TestAccessPublishedPortFromRemoteHost(t *testing.T) { serverID := container.Run(ctx, t, c, container.WithName(sanitizeCtrName(t.Name()+"-server")), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: hostPort}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}), container.WithCmd("httpd", "-f"), container.WithNetworkMode(bridgeName)) defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true}) @@ -553,13 +553,13 @@ func TestAccessPublishedPortFromCtr(t *testing.T) { serverId := container.Run(ctx, t, c, container.WithNetworkMode(netName), container.WithExposedPorts("80"), - container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "0.0.0.0"}}}), + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: "0.0.0.0"}}}), container.WithCmd("httpd", "-f"), ) defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true}) inspect := container.Inspect(ctx, t, c, serverId) - hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort + hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort clientCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() @@ -603,7 +603,9 @@ func TestRestartUserlandProxyUnder2MSL(t *testing.T) { ctrOpts := []func(*container.TestContainerConfig){ container.WithName(ctrName), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "1780"}}}), + container.WithPortMap(containertypes.PortMap{ + containertypes.MustParsePort("80/tcp"): {{HostPort: "1780"}}, + }), container.WithCmd("httpd", "-f"), container.WithNetworkMode(netName), } @@ -700,7 +702,9 @@ func TestDirectRoutingOpenPorts(t *testing.T) { container.WithNetworkMode(netName), container.WithName("ctr-"+gwMode), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {}}), + container.WithPortMap(containertypes.PortMap{ + containertypes.MustParsePort("80/tcp"): {}, + }), ) t.Cleanup(func() { c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true}) @@ -979,7 +983,9 @@ func TestRoutedNonGateway(t *testing.T) { ctrId := container.Run(ctx, t, c, container.WithCmd("httpd", "-f"), container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}), + container.WithPortMap(containertypes.PortMap{ + containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}, + }), container.WithNetworkMode(natNetName), container.WithNetworkMode(routedNetName), container.WithEndpointSettings(natNetName, &networktypes.EndpointSettings{GwPriority: 1}), @@ -1133,7 +1139,9 @@ func TestAccessPublishedPortFromAnotherNetwork(t *testing.T) { container.WithName("server"), container.WithCmd("nc", "-lp", "5000"), container.WithExposedPorts("5000/tcp"), - container.WithPortMap(containertypes.PortMap{"5000/tcp": {{HostPort: "5000"}}}), + container.WithPortMap(containertypes.PortMap{ + containertypes.MustParsePort("5000/tcp"): {{HostPort: "5000"}}, + }), container.WithNetworkMode(servnet)) defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true}) @@ -1329,7 +1337,9 @@ func testDirectRemoteAccessOnExposedPort(t *testing.T, ctx context.Context, d *d container.WithName(sanitizeCtrName(t.Name()+"-server")), container.WithCmd("nc", "-lup", "5000"), container.WithExposedPorts("5000/udp"), - container.WithPortMap(containertypes.PortMap{"5000/udp": {{HostPort: hostPort}}}), + container.WithPortMap(containertypes.PortMap{ + containertypes.MustParsePort("5000/udp"): {{HostPort: hostPort}}, + }), container.WithNetworkMode(bridgeName), container.WithEndpointSettings(bridgeName, &networktypes.EndpointSettings{ IPAddress: ctrIP.String(), @@ -1415,7 +1425,9 @@ func TestAccessPortPublishedOnLoopbackAddress(t *testing.T) { container.WithCmd("nc", "-lup", "5000"), container.WithExposedPorts("5000/udp"), // This port is mapped on 127.0.0.2, so it should not be remotely accessible. - container.WithPortMap(containertypes.PortMap{"5000/udp": {{HostIP: loIP, HostPort: hostPort}}}), + container.WithPortMap(containertypes.PortMap{ + containertypes.MustParsePort("5000/udp"): {{HostIP: loIP, HostPort: hostPort}}, + }), container.WithNetworkMode(bridgeName)) defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true}) @@ -1526,7 +1538,7 @@ func TestSkipRawRules(t *testing.T) { ctrId := container.Run(ctx, t, c, container.WithExposedPorts("80/tcp"), - container.WithPortMap(containertypes.PortMap{"80/tcp": { + container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): { {HostIP: "127.0.0.1", HostPort: "8080"}, {HostPort: "8081"}, }}), @@ -1559,9 +1571,9 @@ func TestMixAnyWithSpecificHostAddrs(t *testing.T) { ctrId := container.Run(ctx, t, c, container.WithExposedPorts("80/"+proto, "81/"+proto, "82/"+proto), container.WithPortMap(containertypes.PortMap{ - containertypes.PortRangeProto("81/" + proto): {{}}, - containertypes.PortRangeProto("82/" + proto): {{}}, - containertypes.PortRangeProto("80/" + proto): {{HostIP: "127.0.0.1"}}, + containertypes.MustParsePort("81/" + proto): {{}}, + containertypes.MustParsePort("82/" + proto): {{}}, + containertypes.MustParsePort("80/" + proto): {{HostIP: "127.0.0.1"}}, }), ) defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true}) diff --git a/internal/testutil/fakestorage/storage.go b/internal/testutil/fakestorage/storage.go index fe6dc88b8d..525c25daab 100644 --- a/internal/testutil/fakestorage/storage.go +++ b/internal/testutil/fakestorage/storage.go @@ -164,7 +164,7 @@ COPY . /static`); err != nil { // Find out the system assigned port i, err := c.ContainerInspect(context.Background(), b.ID) assert.NilError(t, err) - ports, exists := i.NetworkSettings.Ports["80/tcp"] + ports, exists := i.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")] assert.Assert(t, exists, "unable to find port 80/tcp for %s", ctrName) if len(ports) == 0 { t.Fatalf("no ports mapped for 80/tcp for %s: %#v", ctrName, i.NetworkSettings.Ports) diff --git a/vendor/github.com/docker/go-connections/nat/nat.go b/vendor/github.com/docker/go-connections/nat/nat.go deleted file mode 100644 index 1ffe0355dc..0000000000 --- a/vendor/github.com/docker/go-connections/nat/nat.go +++ /dev/null @@ -1,237 +0,0 @@ -// Package nat is a convenience package for manipulation of strings describing network ports. -package nat - -import ( - "errors" - "fmt" - "net" - "strconv" - "strings" -) - -// PortBinding represents a binding between a Host IP address and a Host Port -type PortBinding struct { - // HostIP is the host IP Address - HostIP string `json:"HostIp"` - // HostPort is the host port number - HostPort string -} - -// PortMap is a collection of PortBinding indexed by Port -type PortMap map[Port][]PortBinding - -// PortSet is a collection of structs indexed by Port -type PortSet map[Port]struct{} - -// Port is a string containing port number and protocol in the format "80/tcp" -type Port string - -// NewPort creates a new instance of a Port given a protocol and port number or port range -func NewPort(proto, port string) (Port, error) { - // Check for parsing issues on "port" now so we can avoid having - // to check it later on. - - portStartInt, portEndInt, err := ParsePortRangeToInt(port) - if err != nil { - return "", err - } - - if portStartInt == portEndInt { - return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil - } - return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil -} - -// ParsePort parses the port number string and returns an int -func ParsePort(rawPort string) (int, error) { - if rawPort == "" { - return 0, nil - } - port, err := strconv.ParseUint(rawPort, 10, 16) - if err != nil { - return 0, fmt.Errorf("invalid port '%s': %w", rawPort, errors.Unwrap(err)) - } - return int(port), nil -} - -// ParsePortRangeToInt parses the port range string and returns start/end ints -func ParsePortRangeToInt(rawPort string) (int, int, error) { - if rawPort == "" { - return 0, 0, nil - } - start, end, err := ParsePortRange(rawPort) - if err != nil { - return 0, 0, err - } - return int(start), int(end), nil -} - -// Proto returns the protocol of a Port -func (p Port) Proto() string { - proto, _ := SplitProtoPort(string(p)) - return proto -} - -// Port returns the port number of a Port -func (p Port) Port() string { - _, port := SplitProtoPort(string(p)) - return port -} - -// Int returns the port number of a Port as an int -func (p Port) Int() int { - portStr := p.Port() - // We don't need to check for an error because we're going to - // assume that any error would have been found, and reported, in NewPort() - port, _ := ParsePort(portStr) - return port -} - -// Range returns the start/end port numbers of a Port range as ints -func (p Port) Range() (int, int, error) { - return ParsePortRangeToInt(p.Port()) -} - -// SplitProtoPort splits a port(range) and protocol, formatted as "/[]" -// "/[]". It returns an empty string for both if -// no port(range) is provided. If a port(range) is provided, but no protocol, -// the default ("tcp") protocol is returned. -// -// SplitProtoPort does not validate or normalize the returned values. -func SplitProtoPort(rawPort string) (proto string, port string) { - port, proto, _ = strings.Cut(rawPort, "/") - if port == "" { - return "", "" - } - if proto == "" { - proto = "tcp" - } - return proto, port -} - -func validateProto(proto string) error { - switch proto { - case "tcp", "udp", "sctp": - // All good - return nil - default: - return errors.New("invalid proto: " + proto) - } -} - -// ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses -// these in to the internal types -func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) { - var ( - exposedPorts = make(map[Port]struct{}, len(ports)) - bindings = make(map[Port][]PortBinding) - ) - for _, p := range ports { - portMappings, err := ParsePortSpec(p) - if err != nil { - return nil, nil, err - } - - for _, pm := range portMappings { - port := pm.Port - if _, ok := exposedPorts[port]; !ok { - exposedPorts[port] = struct{}{} - } - bindings[port] = append(bindings[port], pm.Binding) - } - } - return exposedPorts, bindings, nil -} - -// PortMapping is a data object mapping a Port to a PortBinding -type PortMapping struct { - Port Port - Binding PortBinding -} - -func (p *PortMapping) String() string { - return net.JoinHostPort(p.Binding.HostIP, p.Binding.HostPort+":"+string(p.Port)) -} - -func splitParts(rawport string) (hostIP, hostPort, containerPort string) { - parts := strings.Split(rawport, ":") - - switch len(parts) { - case 1: - return "", "", parts[0] - case 2: - return "", parts[0], parts[1] - case 3: - return parts[0], parts[1], parts[2] - default: - n := len(parts) - return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1] - } -} - -// ParsePortSpec parses a port specification string into a slice of PortMappings -func ParsePortSpec(rawPort string) ([]PortMapping, error) { - ip, hostPort, containerPort := splitParts(rawPort) - proto, containerPort := SplitProtoPort(containerPort) - proto = strings.ToLower(proto) - if err := validateProto(proto); err != nil { - return nil, err - } - - if ip != "" && ip[0] == '[' { - // Strip [] from IPV6 addresses - rawIP, _, err := net.SplitHostPort(ip + ":") - if err != nil { - return nil, fmt.Errorf("invalid IP address %v: %w", ip, err) - } - ip = rawIP - } - if ip != "" && net.ParseIP(ip) == nil { - return nil, errors.New("invalid IP address: " + ip) - } - if containerPort == "" { - return nil, fmt.Errorf("no port specified: %s", rawPort) - } - - startPort, endPort, err := ParsePortRange(containerPort) - if err != nil { - return nil, errors.New("invalid containerPort: " + containerPort) - } - - var startHostPort, endHostPort uint64 - if hostPort != "" { - startHostPort, endHostPort, err = ParsePortRange(hostPort) - if err != nil { - return nil, errors.New("invalid hostPort: " + hostPort) - } - if (endPort - startPort) != (endHostPort - startHostPort) { - // Allow host port range iff containerPort is not a range. - // In this case, use the host port range as the dynamic - // host port range to allocate into. - if endPort != startPort { - return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) - } - } - } - - count := endPort - startPort + 1 - ports := make([]PortMapping, 0, count) - - for i := uint64(0); i < count; i++ { - cPort := Port(strconv.FormatUint(startPort+i, 10) + "/" + proto) - hPort := "" - if hostPort != "" { - hPort = strconv.FormatUint(startHostPort+i, 10) - // Set hostPort to a range only if there is a single container port - // and a dynamic host port. - if count == 1 && startHostPort != endHostPort { - hPort += "-" + strconv.FormatUint(endHostPort, 10) - } - } - ports = append(ports, PortMapping{ - Port: cPort, - Binding: PortBinding{HostIP: ip, HostPort: hPort}, - }) - } - return ports, nil -} diff --git a/vendor/github.com/docker/go-connections/nat/parse.go b/vendor/github.com/docker/go-connections/nat/parse.go deleted file mode 100644 index 64affa2a90..0000000000 --- a/vendor/github.com/docker/go-connections/nat/parse.go +++ /dev/null @@ -1,33 +0,0 @@ -package nat - -import ( - "errors" - "strconv" - "strings" -) - -// ParsePortRange parses and validates the specified string as a port-range (8000-9000) -func ParsePortRange(ports string) (uint64, uint64, error) { - if ports == "" { - return 0, 0, errors.New("empty string specified for ports") - } - if !strings.Contains(ports, "-") { - start, err := strconv.ParseUint(ports, 10, 16) - end := start - return start, end, err - } - - parts := strings.Split(ports, "-") - start, err := strconv.ParseUint(parts[0], 10, 16) - if err != nil { - return 0, 0, err - } - end, err := strconv.ParseUint(parts[1], 10, 16) - if err != nil { - return 0, 0, err - } - if end < start { - return 0, 0, errors.New("invalid range specified for port: " + ports) - } - return start, end, nil -} diff --git a/vendor/github.com/docker/go-connections/nat/sort.go b/vendor/github.com/docker/go-connections/nat/sort.go deleted file mode 100644 index b6eed145e1..0000000000 --- a/vendor/github.com/docker/go-connections/nat/sort.go +++ /dev/null @@ -1,96 +0,0 @@ -package nat - -import ( - "sort" - "strings" -) - -type portSorter struct { - ports []Port - by func(i, j Port) bool -} - -func (s *portSorter) Len() int { - return len(s.ports) -} - -func (s *portSorter) Swap(i, j int) { - s.ports[i], s.ports[j] = s.ports[j], s.ports[i] -} - -func (s *portSorter) Less(i, j int) bool { - ip := s.ports[i] - jp := s.ports[j] - - return s.by(ip, jp) -} - -// Sort sorts a list of ports using the provided predicate -// This function should compare `i` and `j`, returning true if `i` is -// considered to be less than `j` -func Sort(ports []Port, predicate func(i, j Port) bool) { - s := &portSorter{ports, predicate} - sort.Sort(s) -} - -type portMapEntry struct { - port Port - binding PortBinding -} - -type portMapSorter []portMapEntry - -func (s portMapSorter) Len() int { return len(s) } -func (s portMapSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less sorts the port so that the order is: -// 1. port with larger specified bindings -// 2. larger port -// 3. port with tcp protocol -func (s portMapSorter) Less(i, j int) bool { - pi, pj := s[i].port, s[j].port - hpi, hpj := toInt(s[i].binding.HostPort), toInt(s[j].binding.HostPort) - return hpi > hpj || pi.Int() > pj.Int() || (pi.Int() == pj.Int() && strings.ToLower(pi.Proto()) == "tcp") -} - -// SortPortMap sorts the list of ports and their respected mapping. The ports -// will explicit HostPort will be placed first. -func SortPortMap(ports []Port, bindings PortMap) { - s := portMapSorter{} - for _, p := range ports { - if binding, ok := bindings[p]; ok && len(binding) > 0 { - for _, b := range binding { - s = append(s, portMapEntry{port: p, binding: b}) - } - bindings[p] = []PortBinding{} - } else { - s = append(s, portMapEntry{port: p}) - } - } - - sort.Sort(s) - var ( - i int - pm = make(map[Port]struct{}) - ) - // reorder ports - for _, entry := range s { - if _, ok := pm[entry.port]; !ok { - ports[i] = entry.port - pm[entry.port] = struct{}{} - i++ - } - // reorder bindings for this port - if _, ok := bindings[entry.port]; ok { - bindings[entry.port] = append(bindings[entry.port], entry.binding) - } - } -} - -func toInt(s string) uint64 { - i, _, err := ParsePortRange(s) - if err != nil { - i = 0 - } - return i -} diff --git a/vendor/github.com/moby/moby/api/types/container/nat_aliases.go b/vendor/github.com/moby/moby/api/types/container/nat_aliases.go deleted file mode 100644 index 470f15655c..0000000000 --- a/vendor/github.com/moby/moby/api/types/container/nat_aliases.go +++ /dev/null @@ -1,24 +0,0 @@ -package container - -import "github.com/docker/go-connections/nat" - -// PortRangeProto is a string containing port number and protocol in the format "80/tcp", -// or a port range and protocol in the format "80-83/tcp". -// -// It is currently an alias for [nat.Port] but may become a concrete type in a future release. -type PortRangeProto = nat.Port - -// PortSet is a collection of structs indexed by [HostPort]. -// -// It is currently an alias for [nat.PortSet] but may become a concrete type in a future release. -type PortSet = nat.PortSet - -// PortBinding represents a binding between a Host IP address and a [HostPort]. -// -// It is currently an alias for [nat.PortBinding] but may become a concrete type in a future release. -type PortBinding = nat.PortBinding - -// PortMap is a collection of [PortBinding] indexed by [HostPort]. -// -// It is currently an alias for [nat.PortMap] but may become a concrete type in a future release. -type PortMap = nat.PortMap diff --git a/vendor/github.com/moby/moby/api/types/container/network.go b/vendor/github.com/moby/moby/api/types/container/network.go new file mode 100644 index 0000000000..b22329033a --- /dev/null +++ b/vendor/github.com/moby/moby/api/types/container/network.go @@ -0,0 +1,345 @@ +package container + +import ( + "errors" + "fmt" + "iter" + "strconv" + "strings" + "unique" +) + +// NetworkProtocol represents a network protocol for a port. +type NetworkProtocol string + +const ( + TCP NetworkProtocol = "tcp" + UDP NetworkProtocol = "udp" + SCTP NetworkProtocol = "sctp" +) + +// Sentinel port proto value for zero Port and PortRange values. +var protoZero unique.Handle[NetworkProtocol] + +// Port is a type representing a single port number and protocol in the format "/[]". +// +// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value. +type Port struct { + num uint16 + proto unique.Handle[NetworkProtocol] +} + +// ParsePort parses s as a [Port]. +// +// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent. +// If a port number is provided, but no protocol, the default ("tcp") protocol is returned. +func ParsePort(s string) (Port, error) { + if s == "" { + return Port{}, errors.New("invalid port: value is empty") + } + + port, proto, _ := strings.Cut(s, "/") + + portNum, err := parsePortNumber(port) + if err != nil { + return Port{}, fmt.Errorf("invalid port '%s': %w", port, err) + } + + normalizedPortProto := normalizePortProto(proto) + return Port{num: portNum, proto: normalizedPortProto}, nil +} + +// MustParsePort calls [ParsePort](s) and panics on error. +// +// It is intended for use in tests with hard-coded strings. +func MustParsePort(s string) Port { + p, err := ParsePort(s) + if err != nil { + panic(err) + } + return p +} + +// PortFrom returns a [Port] with the given number and protocol. +// +// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false. +func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) { + if proto == "" { + return Port{}, false + } + normalized := normalizePortProto(string(proto)) + return Port{num: num, proto: normalized}, true +} + +// Num returns p's port number. +func (p Port) Num() uint16 { + return p.num +} + +// Proto returns p's network protocol. +func (p Port) Proto() NetworkProtocol { + return p.proto.Value() +} + +// IsZero reports whether p is the zero value. +func (p Port) IsZero() bool { + return p.proto == protoZero +} + +// IsValid reports whether p is an initialized valid port (not the zero value). +func (p Port) IsValid() bool { + return p.proto != protoZero +} + +// String returns a string representation of the port in the format "/". +// If the port is the zero value, it returns "invalid port". +func (p Port) String() string { + switch p.proto { + case protoZero: + return "invalid port" + default: + return string(p.AppendTo(nil)) + } +} + +// AppendText implements [encoding.TextAppender] interface. +// It is the same as [Port.AppendTo] but returns an error to satisfy the interface. +func (p Port) AppendText(b []byte) ([]byte, error) { + return p.AppendTo(b), nil +} + +// AppendTo appends a text encoding of p to b and returns the extended buffer. +func (p Port) AppendTo(b []byte) []byte { + if p.IsZero() { + return b + } + return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value()) +} + +// MarshalText implements [encoding.TextMarshaler] interface. +func (p Port) MarshalText() ([]byte, error) { + return p.AppendText(nil) +} + +// UnmarshalText implements [encoding.TextUnmarshaler] interface. +func (p *Port) UnmarshalText(text []byte) error { + if len(text) == 0 { + *p = Port{} + return nil + } + + port, err := ParsePort(string(text)) + if err != nil { + return err + } + + *p = port + return nil +} + +// Range returns a [PortRange] representing the single port. +func (p Port) Range() PortRange { + return PortRange{start: p.num, end: p.num, proto: p.proto} +} + +// PortSet is a collection of structs indexed by [Port]. +type PortSet = map[Port]struct{} + +// PortBinding represents a binding between a Host IP address and a Host Port. +type PortBinding struct { + // HostIP is the host IP Address + HostIP string `json:"HostIp"` + // HostPort is the host port number + HostPort string `json:"HostPort"` +} + +// PortMap is a collection of [PortBinding] indexed by [Port]. +type PortMap = map[Port][]PortBinding + +// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp". +// +// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value. +type PortRange struct { + start uint16 + end uint16 + proto unique.Handle[NetworkProtocol] +} + +// ParsePortRange parses s as a [PortRange]. +// +// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent. +// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned. +func ParsePortRange(s string) (PortRange, error) { + if s == "" { + return PortRange{}, errors.New("invalid port range: value is empty") + } + + portRange, proto, _ := strings.Cut(s, "/") + + start, end, ok := strings.Cut(portRange, "-") + startVal, err := parsePortNumber(start) + if err != nil { + return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err) + } + + portProto := normalizePortProto(proto) + + if !ok || start == end { + return PortRange{start: startVal, end: startVal, proto: portProto}, nil + } + + endVal, err := parsePortNumber(end) + if err != nil { + return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err) + } + if endVal < startVal { + return PortRange{}, errors.New("invalid port range: " + s) + } + return PortRange{start: startVal, end: endVal, proto: portProto}, nil +} + +// MustParsePortRange calls [ParsePortRange](s) and panics on error. +// It is intended for use in tests with hard-coded strings. +func MustParsePortRange(s string) PortRange { + pr, err := ParsePortRange(s) + if err != nil { + panic(err) + } + return pr +} + +// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol. +// +// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false. +func PortRangeFrom(start, end uint16, proto NetworkProtocol) (pr PortRange, ok bool) { + if end < start || proto == "" { + return PortRange{}, false + } + normalized := normalizePortProto(string(proto)) + return PortRange{start: start, end: end, proto: normalized}, true +} + +// Start returns pr's start port number. +func (pr PortRange) Start() uint16 { + return pr.start +} + +// End returns pr's end port number. +func (pr PortRange) End() uint16 { + return pr.end +} + +// Proto returns pr's network protocol. +func (pr PortRange) Proto() NetworkProtocol { + return pr.proto.Value() +} + +// IsZero reports whether pr is the zero value. +func (pr PortRange) IsZero() bool { + return pr.proto == protoZero +} + +// IsValid reports whether pr is an initialized valid port range (not the zero value). +func (pr PortRange) IsValid() bool { + return pr.proto != protoZero +} + +// String returns a string representation of the port range in the format "-/" or "/" if start == end. +// If the port range is the zero value, it returns "invalid port range". +func (pr PortRange) String() string { + switch pr.proto { + case protoZero: + return "invalid port range" + default: + return string(pr.AppendTo(nil)) + } +} + +// AppendText implements [encoding.TextAppender] interface. +// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface. +func (pr PortRange) AppendText(b []byte) ([]byte, error) { + return pr.AppendTo(b), nil +} + +// AppendTo appends a text encoding of pr to b and returns the extended buffer. +func (pr PortRange) AppendTo(b []byte) []byte { + if pr.IsZero() { + return b + } + if pr.start == pr.end { + return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value()) + } + return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value()) +} + +// MarshalText implements [encoding.TextMarshaler] interface. +func (pr PortRange) MarshalText() ([]byte, error) { + return pr.AppendText(nil) +} + +// UnmarshalText implements [encoding.TextUnmarshaler] interface. +func (pr *PortRange) UnmarshalText(text []byte) error { + if len(text) == 0 { + *pr = PortRange{} + return nil + } + + portRange, err := ParsePortRange(string(text)) + if err != nil { + return err + } + *pr = portRange + return nil +} + +// Range returns pr. +func (pr PortRange) Range() PortRange { + return pr +} + +// All returns an iterator over all the individual ports in the range. +// +// For example: +// +// for port := range pr.All() { +// // ... +// } +func (pr PortRange) All() iter.Seq[Port] { + return func(yield func(Port) bool) { + for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ { + if !yield(Port{num: uint16(i), proto: pr.proto}) { + return + } + } + } +} + +// parsePortNumber parses rawPort into an int, unwrapping strconv errors +// and returning a single "out of range" error for any value outside 0–65535. +func parsePortNumber(rawPort string) (uint16, error) { + if rawPort == "" { + return 0, errors.New("value is empty") + } + port, err := strconv.ParseUint(rawPort, 10, 16) + if err != nil { + var numErr *strconv.NumError + if errors.As(err, &numErr) { + err = numErr.Err + } + return 0, err + } + + return uint16(port), nil +} + +// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent. +// If proto is not specified, it defaults to "tcp". +func normalizePortProto(proto string) unique.Handle[NetworkProtocol] { + if proto == "" { + return unique.Make(TCP) + } + + proto = strings.ToLower(proto) + + return unique.Make(NetworkProtocol(proto)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 247a778f97..30969a745a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -506,7 +506,6 @@ github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache/memory # github.com/docker/go-connections v0.6.0 ## explicit; go 1.18 -github.com/docker/go-connections/nat github.com/docker/go-connections/sockets github.com/docker/go-connections/tlsconfig # github.com/docker/go-events v0.0.0-20250808211157-605354379745