Browse code

api: move container port type to network package

Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>

Austin Vazquez authored on 2025/10/03 22:48:08
Showing 46 changed files
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"time"
5 5
 
6 6
 	dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
7
+	"github.com/moby/moby/api/types/network"
7 8
 )
8 9
 
9 10
 // MinimumDuration puts a minimum on user configured duration.
... ...
@@ -28,7 +29,7 @@ type Config struct {
28 28
 	AttachStdin     bool                // Attach the standard input, makes possible user interaction
29 29
 	AttachStdout    bool                // Attach the standard output
30 30
 	AttachStderr    bool                // Attach the standard error
31
-	ExposedPorts    PortSet             `json:",omitempty"` // List of exposed ports
31
+	ExposedPorts    network.PortSet     `json:",omitempty"` // List of exposed ports
32 32
 	Tty             bool                // Attach standard streams to a tty, including stdin if it is not closed.
33 33
 	OpenStdin       bool                // Open stdin
34 34
 	StdinOnce       bool                // If true, close stdin after the 1 attached client disconnects.
... ...
@@ -421,7 +421,7 @@ type HostConfig struct {
421 421
 	ContainerIDFile string            // File (path) where the containerId is written
422 422
 	LogConfig       LogConfig         // Configuration of the logs for this container
423 423
 	NetworkMode     NetworkMode       // Network mode to use for the container
424
-	PortBindings    PortMap           // Port mapping between the exposed port (container) and the host
424
+	PortBindings    network.PortMap   // Port mapping between the exposed port (container) and the host
425 425
 	RestartPolicy   RestartPolicy     // Restart policy to be used for the container
426 426
 	AutoRemove      bool              // Automatically remove container when it exits
427 427
 	VolumeDriver    string            // Name of the volume driver used to mount volumes
428 428
deleted file mode 100644
... ...
@@ -1,346 +0,0 @@
1
-package container
2
-
3
-import (
4
-	"errors"
5
-	"fmt"
6
-	"iter"
7
-	"net/netip"
8
-	"strconv"
9
-	"strings"
10
-	"unique"
11
-)
12
-
13
-// NetworkProtocol represents a network protocol for a port.
14
-type NetworkProtocol string
15
-
16
-const (
17
-	TCP  NetworkProtocol = "tcp"
18
-	UDP  NetworkProtocol = "udp"
19
-	SCTP NetworkProtocol = "sctp"
20
-)
21
-
22
-// Sentinel port proto value for zero Port and PortRange values.
23
-var protoZero unique.Handle[NetworkProtocol]
24
-
25
-// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
26
-//
27
-// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value.
28
-type Port struct {
29
-	num   uint16
30
-	proto unique.Handle[NetworkProtocol]
31
-}
32
-
33
-// ParsePort parses s as a [Port].
34
-//
35
-// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent.
36
-// If a port number is provided, but no protocol, the default ("tcp") protocol is returned.
37
-func ParsePort(s string) (Port, error) {
38
-	if s == "" {
39
-		return Port{}, errors.New("invalid port: value is empty")
40
-	}
41
-
42
-	port, proto, _ := strings.Cut(s, "/")
43
-
44
-	portNum, err := parsePortNumber(port)
45
-	if err != nil {
46
-		return Port{}, fmt.Errorf("invalid port '%s': %w", port, err)
47
-	}
48
-
49
-	normalizedPortProto := normalizePortProto(proto)
50
-	return Port{num: portNum, proto: normalizedPortProto}, nil
51
-}
52
-
53
-// MustParsePort calls [ParsePort](s) and panics on error.
54
-//
55
-// It is intended for use in tests with hard-coded strings.
56
-func MustParsePort(s string) Port {
57
-	p, err := ParsePort(s)
58
-	if err != nil {
59
-		panic(err)
60
-	}
61
-	return p
62
-}
63
-
64
-// PortFrom returns a [Port] with the given number and protocol.
65
-//
66
-// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
67
-func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) {
68
-	if proto == "" {
69
-		return Port{}, false
70
-	}
71
-	normalized := normalizePortProto(string(proto))
72
-	return Port{num: num, proto: normalized}, true
73
-}
74
-
75
-// Num returns p's port number.
76
-func (p Port) Num() uint16 {
77
-	return p.num
78
-}
79
-
80
-// Proto returns p's network protocol.
81
-func (p Port) Proto() NetworkProtocol {
82
-	return p.proto.Value()
83
-}
84
-
85
-// IsZero reports whether p is the zero value.
86
-func (p Port) IsZero() bool {
87
-	return p.proto == protoZero
88
-}
89
-
90
-// IsValid reports whether p is an initialized valid port (not the zero value).
91
-func (p Port) IsValid() bool {
92
-	return p.proto != protoZero
93
-}
94
-
95
-// String returns a string representation of the port in the format "<portnum>/<proto>".
96
-// If the port is the zero value, it returns "invalid port".
97
-func (p Port) String() string {
98
-	switch p.proto {
99
-	case protoZero:
100
-		return "invalid port"
101
-	default:
102
-		return string(p.AppendTo(nil))
103
-	}
104
-}
105
-
106
-// AppendText implements [encoding.TextAppender] interface.
107
-// It is the same as [Port.AppendTo] but returns an error to satisfy the interface.
108
-func (p Port) AppendText(b []byte) ([]byte, error) {
109
-	return p.AppendTo(b), nil
110
-}
111
-
112
-// AppendTo appends a text encoding of p to b and returns the extended buffer.
113
-func (p Port) AppendTo(b []byte) []byte {
114
-	if p.IsZero() {
115
-		return b
116
-	}
117
-	return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value())
118
-}
119
-
120
-// MarshalText implements [encoding.TextMarshaler] interface.
121
-func (p Port) MarshalText() ([]byte, error) {
122
-	return p.AppendText(nil)
123
-}
124
-
125
-// UnmarshalText implements [encoding.TextUnmarshaler] interface.
126
-func (p *Port) UnmarshalText(text []byte) error {
127
-	if len(text) == 0 {
128
-		*p = Port{}
129
-		return nil
130
-	}
131
-
132
-	port, err := ParsePort(string(text))
133
-	if err != nil {
134
-		return err
135
-	}
136
-
137
-	*p = port
138
-	return nil
139
-}
140
-
141
-// Range returns a [PortRange] representing the single port.
142
-func (p Port) Range() PortRange {
143
-	return PortRange{start: p.num, end: p.num, proto: p.proto}
144
-}
145
-
146
-// PortSet is a collection of structs indexed by [Port].
147
-type PortSet = map[Port]struct{}
148
-
149
-// PortBinding represents a binding between a Host IP address and a Host Port.
150
-type PortBinding struct {
151
-	// HostIP is the host IP Address
152
-	HostIP netip.Addr `json:"HostIp"`
153
-	// HostPort is the host port number
154
-	HostPort string `json:"HostPort"`
155
-}
156
-
157
-// PortMap is a collection of [PortBinding] indexed by [Port].
158
-type PortMap = map[Port][]PortBinding
159
-
160
-// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp".
161
-//
162
-// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value.
163
-type PortRange struct {
164
-	start uint16
165
-	end   uint16
166
-	proto unique.Handle[NetworkProtocol]
167
-}
168
-
169
-// ParsePortRange parses s as a [PortRange].
170
-//
171
-// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent.
172
-// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned.
173
-func ParsePortRange(s string) (PortRange, error) {
174
-	if s == "" {
175
-		return PortRange{}, errors.New("invalid port range: value is empty")
176
-	}
177
-
178
-	portRange, proto, _ := strings.Cut(s, "/")
179
-
180
-	start, end, ok := strings.Cut(portRange, "-")
181
-	startVal, err := parsePortNumber(start)
182
-	if err != nil {
183
-		return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err)
184
-	}
185
-
186
-	portProto := normalizePortProto(proto)
187
-
188
-	if !ok || start == end {
189
-		return PortRange{start: startVal, end: startVal, proto: portProto}, nil
190
-	}
191
-
192
-	endVal, err := parsePortNumber(end)
193
-	if err != nil {
194
-		return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err)
195
-	}
196
-	if endVal < startVal {
197
-		return PortRange{}, errors.New("invalid port range: " + s)
198
-	}
199
-	return PortRange{start: startVal, end: endVal, proto: portProto}, nil
200
-}
201
-
202
-// MustParsePortRange calls [ParsePortRange](s) and panics on error.
203
-// It is intended for use in tests with hard-coded strings.
204
-func MustParsePortRange(s string) PortRange {
205
-	pr, err := ParsePortRange(s)
206
-	if err != nil {
207
-		panic(err)
208
-	}
209
-	return pr
210
-}
211
-
212
-// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
213
-//
214
-// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
215
-func PortRangeFrom(start, end uint16, proto NetworkProtocol) (pr PortRange, ok bool) {
216
-	if end < start || proto == "" {
217
-		return PortRange{}, false
218
-	}
219
-	normalized := normalizePortProto(string(proto))
220
-	return PortRange{start: start, end: end, proto: normalized}, true
221
-}
222
-
223
-// Start returns pr's start port number.
224
-func (pr PortRange) Start() uint16 {
225
-	return pr.start
226
-}
227
-
228
-// End returns pr's end port number.
229
-func (pr PortRange) End() uint16 {
230
-	return pr.end
231
-}
232
-
233
-// Proto returns pr's network protocol.
234
-func (pr PortRange) Proto() NetworkProtocol {
235
-	return pr.proto.Value()
236
-}
237
-
238
-// IsZero reports whether pr is the zero value.
239
-func (pr PortRange) IsZero() bool {
240
-	return pr.proto == protoZero
241
-}
242
-
243
-// IsValid reports whether pr is an initialized valid port range (not the zero value).
244
-func (pr PortRange) IsValid() bool {
245
-	return pr.proto != protoZero
246
-}
247
-
248
-// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
249
-// If the port range is the zero value, it returns "invalid port range".
250
-func (pr PortRange) String() string {
251
-	switch pr.proto {
252
-	case protoZero:
253
-		return "invalid port range"
254
-	default:
255
-		return string(pr.AppendTo(nil))
256
-	}
257
-}
258
-
259
-// AppendText implements [encoding.TextAppender] interface.
260
-// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface.
261
-func (pr PortRange) AppendText(b []byte) ([]byte, error) {
262
-	return pr.AppendTo(b), nil
263
-}
264
-
265
-// AppendTo appends a text encoding of pr to b and returns the extended buffer.
266
-func (pr PortRange) AppendTo(b []byte) []byte {
267
-	if pr.IsZero() {
268
-		return b
269
-	}
270
-	if pr.start == pr.end {
271
-		return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value())
272
-	}
273
-	return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value())
274
-}
275
-
276
-// MarshalText implements [encoding.TextMarshaler] interface.
277
-func (pr PortRange) MarshalText() ([]byte, error) {
278
-	return pr.AppendText(nil)
279
-}
280
-
281
-// UnmarshalText implements [encoding.TextUnmarshaler] interface.
282
-func (pr *PortRange) UnmarshalText(text []byte) error {
283
-	if len(text) == 0 {
284
-		*pr = PortRange{}
285
-		return nil
286
-	}
287
-
288
-	portRange, err := ParsePortRange(string(text))
289
-	if err != nil {
290
-		return err
291
-	}
292
-	*pr = portRange
293
-	return nil
294
-}
295
-
296
-// Range returns pr.
297
-func (pr PortRange) Range() PortRange {
298
-	return pr
299
-}
300
-
301
-// All returns an iterator over all the individual ports in the range.
302
-//
303
-// For example:
304
-//
305
-//	for port := range pr.All() {
306
-//	    // ...
307
-//	}
308
-func (pr PortRange) All() iter.Seq[Port] {
309
-	return func(yield func(Port) bool) {
310
-		for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
311
-			if !yield(Port{num: uint16(i), proto: pr.proto}) {
312
-				return
313
-			}
314
-		}
315
-	}
316
-}
317
-
318
-// parsePortNumber parses rawPort into an int, unwrapping strconv errors
319
-// and returning a single "out of range" error for any value outside 0–65535.
320
-func parsePortNumber(rawPort string) (uint16, error) {
321
-	if rawPort == "" {
322
-		return 0, errors.New("value is empty")
323
-	}
324
-	port, err := strconv.ParseUint(rawPort, 10, 16)
325
-	if err != nil {
326
-		var numErr *strconv.NumError
327
-		if errors.As(err, &numErr) {
328
-			err = numErr.Err
329
-		}
330
-		return 0, err
331
-	}
332
-
333
-	return uint16(port), nil
334
-}
335
-
336
-// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent.
337
-// If proto is not specified, it defaults to "tcp".
338
-func normalizePortProto(proto string) unique.Handle[NetworkProtocol] {
339
-	if proto == "" {
340
-		return unique.Make(TCP)
341
-	}
342
-
343
-	proto = strings.ToLower(proto)
344
-
345
-	return unique.Make(NetworkProtocol(proto))
346
-}
... ...
@@ -6,10 +6,13 @@ import (
6 6
 
7 7
 // NetworkSettings exposes the network settings in the api
8 8
 type NetworkSettings struct {
9
-	SandboxID  string  // SandboxID uniquely represents a container's network stack
10
-	SandboxKey string  // SandboxKey identifies the sandbox
11
-	Ports      PortMap // Ports is a collection of PortBinding indexed by Port
12
-	Networks   map[string]*network.EndpointSettings
9
+	SandboxID  string // SandboxID uniquely represents a container's network stack
10
+	SandboxKey string // SandboxKey identifies the sandbox
11
+
12
+	// Ports is a collection of [network.PortBinding] indexed by [network.Port]
13
+	Ports network.PortMap
14
+
15
+	Networks map[string]*network.EndpointSettings
13 16
 }
14 17
 
15 18
 // NetworkSettingsSummary provides a summary of container's networks
16 19
deleted file mode 100644
... ...
@@ -1,600 +0,0 @@
1
-package container
2
-
3
-import (
4
-	"encoding/json"
5
-	"fmt"
6
-	"reflect"
7
-	"slices"
8
-	"strings"
9
-	"testing"
10
-
11
-	"gotest.tools/v3/assert"
12
-)
13
-
14
-type TestRanger interface {
15
-	Range() PortRange
16
-}
17
-
18
-var _ TestRanger = Port{}
19
-var _ TestRanger = PortRange{}
20
-
21
-func TestPort(t *testing.T) {
22
-	t.Run("Zero Value", func(t *testing.T) {
23
-		var p Port
24
-		assert.Check(t, p.IsZero())
25
-		assert.Check(t, !p.IsValid())
26
-		assert.Equal(t, p.String(), "invalid port")
27
-
28
-		t.Run("Marshal Unmarshal", func(t *testing.T) {
29
-			var p Port
30
-			bytes, err := p.MarshalText()
31
-			assert.NilError(t, err)
32
-			assert.Check(t, len(bytes) == 0)
33
-
34
-			err = p.UnmarshalText([]byte(""))
35
-			assert.NilError(t, err)
36
-			assert.Equal(t, p, Port{})
37
-		})
38
-
39
-		t.Run("JSON Marshal Unmarshal", func(t *testing.T) {
40
-			var p Port
41
-			bytes, err := json.Marshal(p)
42
-			assert.NilError(t, err)
43
-			assert.Equal(t, string(bytes), `""`)
44
-
45
-			err = json.Unmarshal([]byte(`""`), &p)
46
-			assert.NilError(t, err)
47
-			assert.Equal(t, p, Port{})
48
-		})
49
-	})
50
-
51
-	t.Run("PortFrom", func(t *testing.T) {
52
-		tests := []struct {
53
-			num   uint16
54
-			proto NetworkProtocol
55
-		}{
56
-			{0, TCP},
57
-			{80, TCP},
58
-			{8080, TCP},
59
-			{65535, TCP},
60
-			{80, UDP},
61
-			{8080, SCTP},
62
-		}
63
-
64
-		for _, tc := range tests {
65
-			t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) {
66
-				p, ok := PortFrom(tc.num, tc.proto)
67
-				assert.Check(t, ok)
68
-				assert.Equal(t, p.Num(), tc.num)
69
-				assert.Equal(t, p.Proto(), tc.proto)
70
-			})
71
-		}
72
-
73
-		t.Run("Normalize Protocol", func(t *testing.T) {
74
-			pr1 := portFrom(1234, "tcp")
75
-			pr2 := portFrom(1234, "TCP")
76
-			pr3 := portFrom(1234, "tCp")
77
-			assert.Equal(t, pr1, pr2)
78
-			assert.Equal(t, pr2, pr3)
79
-		})
80
-
81
-		negativeTests := []struct {
82
-			num   uint16
83
-			proto NetworkProtocol
84
-		}{
85
-			{0, ""},
86
-			{80, ""},
87
-		}
88
-		for _, tc := range negativeTests {
89
-			t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) {
90
-				p, ok := PortFrom(tc.num, tc.proto)
91
-				assert.Check(t, !ok)
92
-				assert.Check(t, p.IsZero())
93
-				assert.Check(t, !p.IsValid())
94
-				assert.Equal(t, p.String(), "invalid port")
95
-			})
96
-		}
97
-	})
98
-
99
-	t.Run("ParsePort", func(t *testing.T) {
100
-		tests := []struct {
101
-			in        string
102
-			port      Port      // output of ParsePort()
103
-			str       string    // output of String().
104
-			portRange PortRange // output of Range()
105
-		}{
106
-			// Zero port
107
-			{
108
-				in:        "0/tcp",
109
-				port:      portFrom(0, TCP),
110
-				str:       "0/tcp",
111
-				portRange: portRangeFrom(0, 0, TCP),
112
-			},
113
-			// Max valid port
114
-			{
115
-				in:        "65535/tcp",
116
-				port:      portFrom(65535, TCP),
117
-				str:       "65535/tcp",
118
-				portRange: portRangeFrom(65535, 65535, TCP),
119
-			},
120
-			// Simple valid ports
121
-			{
122
-				in:        "1234/tcp",
123
-				port:      portFrom(1234, TCP),
124
-				str:       "1234/tcp",
125
-				portRange: portRangeFrom(1234, 1234, TCP),
126
-			},
127
-			{
128
-				in:        "1234/udp",
129
-				port:      portFrom(1234, UDP),
130
-				str:       "1234/udp",
131
-				portRange: portRangeFrom(1234, 1234, UDP),
132
-			},
133
-			{
134
-				in:        "1234/sctp",
135
-				port:      portFrom(1234, SCTP),
136
-				str:       "1234/sctp",
137
-				portRange: portRangeFrom(1234, 1234, SCTP),
138
-			},
139
-			// Default protocol is tcp
140
-			{
141
-				in:        "1234",
142
-				port:      portFrom(1234, TCP),
143
-				str:       "1234/tcp",
144
-				portRange: portRangeFrom(1234, 1234, TCP),
145
-			},
146
-			// Default protocol is tcp
147
-			{
148
-				in:        "1234/",
149
-				port:      portFrom(1234, TCP),
150
-				str:       "1234/tcp",
151
-				portRange: portRangeFrom(1234, 1234, TCP),
152
-			},
153
-			{
154
-				in:        "1234/tcp:ipv6only",
155
-				port:      portFrom(1234, "tcp:ipv6only"),
156
-				str:       "1234/tcp:ipv6only",
157
-				portRange: portRangeFrom(1234, 1234, "tcp:ipv6only"),
158
-			},
159
-		}
160
-
161
-		for _, tc := range tests {
162
-			t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) {
163
-				got, err := ParsePort(tc.in)
164
-				assert.NilError(t, err)
165
-				assert.Equal(t, got, tc.port)
166
-
167
-				MustParsePort(tc.in) // should not panic
168
-
169
-				assert.Check(t, !got.IsZero())
170
-				assert.Check(t, got.IsValid())
171
-
172
-				// Check that ParsePort is a pure function.
173
-				got2, err := ParsePort(tc.in)
174
-				assert.NilError(t, err)
175
-				assert.Equal(t, got2, got)
176
-
177
-				// Check that ParsePort(port.String()) is the identity function.
178
-				got3, err := ParsePort(got.String())
179
-				assert.NilError(t, err)
180
-				assert.Equal(t, got3, got)
181
-
182
-				// Check String() output
183
-				s := got.String()
184
-				wants := tc.str
185
-				if wants == "" {
186
-					wants = tc.in
187
-				}
188
-				assert.Equal(t, s, wants)
189
-
190
-				js := `"` + tc.in + `"`
191
-				var jsgot Port
192
-				err = json.Unmarshal([]byte(js), &jsgot)
193
-				assert.NilError(t, err)
194
-				assert.Equal(t, jsgot, got)
195
-
196
-				jsb, err := json.Marshal(jsgot)
197
-				assert.NilError(t, err)
198
-
199
-				jswant := `"` + wants + `"`
200
-				assert.Equal(t, string(jsb), jswant)
201
-
202
-				// Check Range() output
203
-				r := got.Range()
204
-				assert.Equal(t, r, tc.portRange)
205
-			})
206
-		}
207
-
208
-		t.Run("Normalize Protocol", func(t *testing.T) {
209
-			p1 := MustParsePort("1234/tcp")
210
-			p2 := MustParsePort("1234/TCP")
211
-			p3 := MustParsePort("1234/tCp")
212
-			assert.Equal(t, p1, p2)
213
-			assert.Equal(t, p2, p3)
214
-		})
215
-
216
-		negativeTests := []string{
217
-			// Empty string
218
-			"",
219
-			// Whitespace-only string
220
-			" ",
221
-			// No port number
222
-			"/",
223
-			// No port number (protocol only)
224
-			"/tcp",
225
-			// Negative port
226
-			"-1",
227
-			// Too large port
228
-			"65536",
229
-			// Non-numeric port
230
-			"foo",
231
-			// Port range instead of single port
232
-			"1234-1240/udp",
233
-			// Port range instead of single port without protocol
234
-			"1234-1240",
235
-			// Garbage port
236
-			"asd1234/tcp",
237
-		}
238
-
239
-		for _, s := range negativeTests {
240
-			t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) {
241
-				got, err := ParsePort(s)
242
-				assert.ErrorContains(t, err, "invalid port")
243
-				assert.Check(t, got.IsZero())
244
-				assert.Check(t, !got.IsValid())
245
-
246
-				// Skip JSON unmarshalling test for empty string as that should succeed.
247
-				// See test "Zero Value" above.
248
-				if s == "" {
249
-					return
250
-				}
251
-
252
-				var jsgot Port
253
-				js := []byte(`"` + s + `"`)
254
-				err = json.Unmarshal(js, &jsgot)
255
-				assert.ErrorContains(t, err, "invalid port")
256
-				assert.Equal(t, jsgot, Port{})
257
-			})
258
-		}
259
-	})
260
-}
261
-
262
-func TestPortRange(t *testing.T) {
263
-	t.Run("Zero Value", func(t *testing.T) {
264
-		var pr PortRange
265
-		assert.Check(t, pr.IsZero())
266
-		assert.Check(t, !pr.IsValid())
267
-		assert.Equal(t, pr.String(), "invalid port range")
268
-
269
-		t.Run("Marshal Unmarshal", func(t *testing.T) {
270
-			var pr PortRange
271
-			bytes, err := pr.MarshalText()
272
-			assert.NilError(t, err)
273
-			assert.Check(t, len(bytes) == 0)
274
-
275
-			err = pr.UnmarshalText([]byte(""))
276
-			assert.NilError(t, err)
277
-			assert.Equal(t, pr, PortRange{})
278
-		})
279
-
280
-		t.Run("JSON Marshal Unmarshal", func(t *testing.T) {
281
-			var pr PortRange
282
-			bytes, err := json.Marshal(pr)
283
-			assert.NilError(t, err)
284
-			assert.Equal(t, string(bytes), `""`)
285
-
286
-			err = json.Unmarshal([]byte(`""`), &pr)
287
-			assert.NilError(t, err)
288
-			assert.Equal(t, pr, PortRange{})
289
-		})
290
-	})
291
-
292
-	t.Run("PortRangeFrom", func(t *testing.T) {
293
-		tests := []struct {
294
-			start uint16
295
-			end   uint16
296
-			proto NetworkProtocol
297
-		}{
298
-			{0, 0, TCP},
299
-			{0, 1234, TCP},
300
-			{80, 80, TCP},
301
-			{80, 8080, TCP},
302
-			{1234, 65535, TCP},
303
-			{80, 80, UDP},
304
-			{80, 8080, SCTP},
305
-		}
306
-
307
-		for _, tc := range tests {
308
-			t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) {
309
-				pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto)
310
-				assert.Check(t, ok)
311
-				assert.Equal(t, pr.Start(), tc.start)
312
-				assert.Equal(t, pr.End(), tc.end)
313
-				assert.Equal(t, pr.Proto(), tc.proto)
314
-			})
315
-		}
316
-
317
-		t.Run("Normalize Protocol", func(t *testing.T) {
318
-			pr1, _ := PortRangeFrom(1234, 5678, "tcp")
319
-			pr2, _ := PortRangeFrom(1234, 5678, "TCP")
320
-			pr3, _ := PortRangeFrom(1234, 5678, "tCp")
321
-			assert.Equal(t, pr1, pr2)
322
-			assert.Equal(t, pr2, pr3)
323
-		})
324
-
325
-		negativeTests := []struct {
326
-			start uint16
327
-			end   uint16
328
-			proto NetworkProtocol
329
-		}{
330
-			{1234, 80, TCP}, // end < start
331
-			{0, 0, ""},      // empty protocol
332
-		}
333
-		for _, tc := range negativeTests {
334
-			t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) {
335
-				pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto)
336
-				assert.Check(t, !ok)
337
-				assert.Check(t, pr.IsZero())
338
-				assert.Check(t, !pr.IsValid())
339
-			})
340
-		}
341
-	})
342
-
343
-	t.Run("ParsePortRange", func(t *testing.T) {
344
-		tests := []struct {
345
-			in        string
346
-			portRange PortRange // output of ParsePortRange() and Range()
347
-			str       string    // output of String(). If "", use in.
348
-
349
-		}{
350
-			// Zero port
351
-			{
352
-				in:        "0-1234/tcp",
353
-				portRange: portRangeFrom(0, 1234, TCP),
354
-				str:       "0-1234/tcp",
355
-			},
356
-			// Max valid port
357
-			{
358
-				in:        "1234-65535/tcp",
359
-				portRange: portRangeFrom(1234, 65535, TCP),
360
-				str:       "1234-65535/tcp",
361
-			},
362
-			// Simple valid ports
363
-			{
364
-				in:        "1234-4567/tcp",
365
-				portRange: portRangeFrom(1234, 4567, TCP),
366
-				str:       "1234-4567/tcp",
367
-			},
368
-			{
369
-				in:        "1234-4567/udp",
370
-				portRange: portRangeFrom(1234, 4567, UDP),
371
-				str:       "1234-4567/udp",
372
-			},
373
-			// Default protocol is tcp
374
-			{
375
-				in:        "1234-4567",
376
-				portRange: portRangeFrom(1234, 4567, TCP),
377
-				str:       "1234-4567/tcp",
378
-			},
379
-			// Default protocol is tcp
380
-			{
381
-				in:        "1234-4567/",
382
-				portRange: portRangeFrom(1234, 4567, TCP),
383
-				str:       "1234-4567/tcp",
384
-			},
385
-			{
386
-				in:        "1234/tcp",
387
-				portRange: portRangeFrom(1234, 1234, TCP),
388
-				str:       "1234/tcp",
389
-			},
390
-			{
391
-				in:        "1234",
392
-				portRange: portRangeFrom(1234, 1234, TCP),
393
-				str:       "1234/tcp",
394
-			},
395
-			{
396
-				in:        "1234-5678/tcp:ipv6only",
397
-				portRange: portRangeFrom(1234, 5678, "tcp:ipv6only"),
398
-				str:       "1234-5678/tcp:ipv6only",
399
-			},
400
-		}
401
-
402
-		for _, tc := range tests {
403
-			t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) {
404
-				got, err := ParsePortRange(tc.in)
405
-				assert.NilError(t, err)
406
-				assert.Equal(t, got, tc.portRange)
407
-				assert.Check(t, !got.IsZero())
408
-				assert.Check(t, got.IsValid())
409
-
410
-				MustParsePortRange(tc.in) // should not panic
411
-
412
-				// Check that ParsePortRange is a pure function.
413
-				got2, err := ParsePortRange(tc.in)
414
-				assert.NilError(t, err)
415
-				assert.Equal(t, got2, got)
416
-
417
-				// Check that ParsePortRange(port.String()) is the identity function.
418
-				got3, err := ParsePortRange(got.String())
419
-				assert.NilError(t, err)
420
-				assert.Equal(t, got3, got)
421
-
422
-				// Check String() output
423
-				s := got.String()
424
-				wants := tc.str
425
-				if wants == "" {
426
-					wants = tc.in
427
-				}
428
-				assert.Equal(t, s, wants)
429
-
430
-				js := `"` + tc.in + `"`
431
-				var jsgot PortRange
432
-				err = json.Unmarshal([]byte(js), &jsgot)
433
-				assert.NilError(t, err)
434
-				assert.Equal(t, jsgot, got)
435
-
436
-				jsb, err := json.Marshal(jsgot)
437
-				assert.NilError(t, err)
438
-				jswant := `"` + wants + `"`
439
-				assert.Equal(t, string(jsb), jswant)
440
-
441
-				// Check Range() output
442
-				r := got.Range()
443
-				assert.Equal(t, r, tc.portRange)
444
-			})
445
-
446
-			t.Run("Normalize Protocol", func(t *testing.T) {
447
-				pr1 := MustParsePortRange("1234-5678/tcp")
448
-				pr2 := MustParsePortRange("1234-5678/TCP")
449
-				pr3 := MustParsePortRange("1234-5678/tCp")
450
-				assert.Equal(t, pr1, pr2)
451
-				assert.Equal(t, pr2, pr3)
452
-			})
453
-
454
-			negativeTests := []string{
455
-				// Empty string
456
-				"",
457
-				// Whitespace-only string
458
-				" ",
459
-				// No port number
460
-				"/",
461
-				// No port number (protocol only)
462
-				"/tcp",
463
-				// Negative start port
464
-				"-1-1234",
465
-				// Negative end port
466
-				"1234--1",
467
-				// Too large start port
468
-				"65536-65537",
469
-				// Too large end port
470
-				"1234-65536",
471
-				// Non-numeric start port
472
-				"foo-1234",
473
-				// Non-numeric end port
474
-				"1234-bar",
475
-				// Start port greater than end port
476
-				"1234-1000",
477
-				// Garbage port range
478
-				"asd1234-5678/tcp",
479
-			}
480
-
481
-			for _, s := range negativeTests {
482
-				t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) {
483
-					got, err := ParsePortRange(s)
484
-					assert.Check(t, err != nil)
485
-					assert.Check(t, got.IsZero())
486
-					assert.Check(t, !got.IsValid())
487
-
488
-					// Skip JSON unmarshalling test for empty string as that should succeed.
489
-					// See test "Zero Value" above.
490
-					if s == "" {
491
-						return
492
-					}
493
-
494
-					var jsgot PortRange
495
-					js := []byte(`"` + s + `"`)
496
-					err = json.Unmarshal(js, &jsgot)
497
-					assert.Check(t, err != nil)
498
-					assert.Equal(t, jsgot, PortRange{})
499
-				})
500
-			}
501
-		}
502
-	})
503
-
504
-	t.Run("PortRange All()", func(t *testing.T) {
505
-		tests := []struct {
506
-			in   string
507
-			want []Port
508
-		}{
509
-			{
510
-				in:   "1000-1000/tcp",
511
-				want: []Port{portFrom(1000, TCP)},
512
-			},
513
-			{
514
-				in:   "1000-1002/tcp",
515
-				want: []Port{portFrom(1000, TCP), portFrom(1001, TCP), portFrom(1002, TCP)},
516
-			},
517
-			{
518
-				in:   "0-0/tcp",
519
-				want: []Port{portFrom(0, TCP)},
520
-			},
521
-			{
522
-				in:   "65535-65535/tcp",
523
-				want: []Port{portFrom(65535, TCP)},
524
-			},
525
-			{
526
-				in:   "65530-65535/tcp",
527
-				want: []Port{portFrom(65530, TCP), portFrom(65531, TCP), portFrom(65532, TCP), portFrom(65533, TCP), portFrom(65534, TCP), portFrom(65535, TCP)},
528
-			},
529
-		}
530
-
531
-		for _, tc := range tests {
532
-			pr := MustParsePortRange(tc.in)
533
-			ports := slices.Collect(pr.All())
534
-			if !reflect.DeepEqual(ports, tc.want) {
535
-				t.Errorf("PortRange.All() = %#v, want %#v", ports, tc.want)
536
-			}
537
-		}
538
-
539
-		t.Run("All() stop early", func(t *testing.T) {
540
-			want := []Port{portFrom(1000, TCP), portFrom(1001, TCP)}
541
-			pr := MustParsePortRange("1000-2000/tcp")
542
-			var ports []Port
543
-			for p := range pr.All() {
544
-				ports = append(ports, p)
545
-				if len(ports) == 2 {
546
-					break
547
-				}
548
-			}
549
-			if !reflect.DeepEqual(ports, want) {
550
-				t.Errorf("PortRange.All() = %#v, want %#v", ports, want)
551
-			}
552
-		})
553
-	})
554
-}
555
-
556
-func BenchmarkPortRangeAll(b *testing.B) {
557
-	b.Run("Single Port", func(b *testing.B) {
558
-		pr := MustParsePortRange("1234/tcp")
559
-		b.ResetTimer()
560
-		for i := 0; i < b.N; i++ {
561
-			var sink int64
562
-			for p := range pr.All() {
563
-				sink += int64(p.Num()) // prevent compiler optimization
564
-			}
565
-			if sink < 0 {
566
-				b.Fatal("unreachable")
567
-			}
568
-		}
569
-	})
570
-
571
-	b.Run("Range", func(b *testing.B) {
572
-		pr := MustParsePortRange("0-65535/tcp")
573
-		b.ResetTimer()
574
-		for i := 0; i < b.N; i++ {
575
-			var sink int64
576
-			for p := range pr.All() {
577
-				sink += int64(p.Num()) // prevent compiler optimization
578
-			}
579
-			if sink < 0 {
580
-				b.Fatal("unreachable")
581
-			}
582
-		}
583
-	})
584
-}
585
-
586
-func portFrom(num uint16, proto NetworkProtocol) Port {
587
-	p, ok := PortFrom(num, proto)
588
-	if !ok {
589
-		panic("invalid port")
590
-	}
591
-	return p
592
-}
593
-
594
-func portRangeFrom(start, end uint16, proto NetworkProtocol) PortRange {
595
-	pr, ok := PortRangeFrom(start, end, proto)
596
-	if !ok {
597
-		panic("invalid port range")
598
-	}
599
-	return pr
600
-}
601 1
new file mode 100644
... ...
@@ -0,0 +1,346 @@
0
+package network
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"iter"
6
+	"net/netip"
7
+	"strconv"
8
+	"strings"
9
+	"unique"
10
+)
11
+
12
+// IPProtocol represents a network protocol for a port.
13
+type IPProtocol string
14
+
15
+const (
16
+	TCP  IPProtocol = "tcp"
17
+	UDP  IPProtocol = "udp"
18
+	SCTP IPProtocol = "sctp"
19
+)
20
+
21
+// Sentinel port proto value for zero Port and PortRange values.
22
+var protoZero unique.Handle[IPProtocol]
23
+
24
+// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
25
+//
26
+// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value.
27
+type Port struct {
28
+	num   uint16
29
+	proto unique.Handle[IPProtocol]
30
+}
31
+
32
+// ParsePort parses s as a [Port].
33
+//
34
+// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent.
35
+// If a port number is provided, but no protocol, the default ("tcp") protocol is returned.
36
+func ParsePort(s string) (Port, error) {
37
+	if s == "" {
38
+		return Port{}, errors.New("invalid port: value is empty")
39
+	}
40
+
41
+	port, proto, _ := strings.Cut(s, "/")
42
+
43
+	portNum, err := parsePortNumber(port)
44
+	if err != nil {
45
+		return Port{}, fmt.Errorf("invalid port '%s': %w", port, err)
46
+	}
47
+
48
+	normalizedPortProto := normalizePortProto(proto)
49
+	return Port{num: portNum, proto: normalizedPortProto}, nil
50
+}
51
+
52
+// MustParsePort calls [ParsePort](s) and panics on error.
53
+//
54
+// It is intended for use in tests with hard-coded strings.
55
+func MustParsePort(s string) Port {
56
+	p, err := ParsePort(s)
57
+	if err != nil {
58
+		panic(err)
59
+	}
60
+	return p
61
+}
62
+
63
+// PortFrom returns a [Port] with the given number and protocol.
64
+//
65
+// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
66
+func PortFrom(num uint16, proto IPProtocol) (p Port, ok bool) {
67
+	if proto == "" {
68
+		return Port{}, false
69
+	}
70
+	normalized := normalizePortProto(string(proto))
71
+	return Port{num: num, proto: normalized}, true
72
+}
73
+
74
+// Num returns p's port number.
75
+func (p Port) Num() uint16 {
76
+	return p.num
77
+}
78
+
79
+// Proto returns p's network protocol.
80
+func (p Port) Proto() IPProtocol {
81
+	return p.proto.Value()
82
+}
83
+
84
+// IsZero reports whether p is the zero value.
85
+func (p Port) IsZero() bool {
86
+	return p.proto == protoZero
87
+}
88
+
89
+// IsValid reports whether p is an initialized valid port (not the zero value).
90
+func (p Port) IsValid() bool {
91
+	return p.proto != protoZero
92
+}
93
+
94
+// String returns a string representation of the port in the format "<portnum>/<proto>".
95
+// If the port is the zero value, it returns "invalid port".
96
+func (p Port) String() string {
97
+	switch p.proto {
98
+	case protoZero:
99
+		return "invalid port"
100
+	default:
101
+		return string(p.AppendTo(nil))
102
+	}
103
+}
104
+
105
+// AppendText implements [encoding.TextAppender] interface.
106
+// It is the same as [Port.AppendTo] but returns an error to satisfy the interface.
107
+func (p Port) AppendText(b []byte) ([]byte, error) {
108
+	return p.AppendTo(b), nil
109
+}
110
+
111
+// AppendTo appends a text encoding of p to b and returns the extended buffer.
112
+func (p Port) AppendTo(b []byte) []byte {
113
+	if p.IsZero() {
114
+		return b
115
+	}
116
+	return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value())
117
+}
118
+
119
+// MarshalText implements [encoding.TextMarshaler] interface.
120
+func (p Port) MarshalText() ([]byte, error) {
121
+	return p.AppendText(nil)
122
+}
123
+
124
+// UnmarshalText implements [encoding.TextUnmarshaler] interface.
125
+func (p *Port) UnmarshalText(text []byte) error {
126
+	if len(text) == 0 {
127
+		*p = Port{}
128
+		return nil
129
+	}
130
+
131
+	port, err := ParsePort(string(text))
132
+	if err != nil {
133
+		return err
134
+	}
135
+
136
+	*p = port
137
+	return nil
138
+}
139
+
140
+// Range returns a [PortRange] representing the single port.
141
+func (p Port) Range() PortRange {
142
+	return PortRange{start: p.num, end: p.num, proto: p.proto}
143
+}
144
+
145
+// PortSet is a collection of structs indexed by [Port].
146
+type PortSet = map[Port]struct{}
147
+
148
+// PortBinding represents a binding between a Host IP address and a Host Port.
149
+type PortBinding struct {
150
+	// HostIP is the host IP Address
151
+	HostIP netip.Addr `json:"HostIp"`
152
+	// HostPort is the host port number
153
+	HostPort string `json:"HostPort"`
154
+}
155
+
156
+// PortMap is a collection of [PortBinding] indexed by [Port].
157
+type PortMap = map[Port][]PortBinding
158
+
159
+// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp".
160
+//
161
+// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value.
162
+type PortRange struct {
163
+	start uint16
164
+	end   uint16
165
+	proto unique.Handle[IPProtocol]
166
+}
167
+
168
+// ParsePortRange parses s as a [PortRange].
169
+//
170
+// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent.
171
+// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned.
172
+func ParsePortRange(s string) (PortRange, error) {
173
+	if s == "" {
174
+		return PortRange{}, errors.New("invalid port range: value is empty")
175
+	}
176
+
177
+	portRange, proto, _ := strings.Cut(s, "/")
178
+
179
+	start, end, ok := strings.Cut(portRange, "-")
180
+	startVal, err := parsePortNumber(start)
181
+	if err != nil {
182
+		return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err)
183
+	}
184
+
185
+	portProto := normalizePortProto(proto)
186
+
187
+	if !ok || start == end {
188
+		return PortRange{start: startVal, end: startVal, proto: portProto}, nil
189
+	}
190
+
191
+	endVal, err := parsePortNumber(end)
192
+	if err != nil {
193
+		return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err)
194
+	}
195
+	if endVal < startVal {
196
+		return PortRange{}, errors.New("invalid port range: " + s)
197
+	}
198
+	return PortRange{start: startVal, end: endVal, proto: portProto}, nil
199
+}
200
+
201
+// MustParsePortRange calls [ParsePortRange](s) and panics on error.
202
+// It is intended for use in tests with hard-coded strings.
203
+func MustParsePortRange(s string) PortRange {
204
+	pr, err := ParsePortRange(s)
205
+	if err != nil {
206
+		panic(err)
207
+	}
208
+	return pr
209
+}
210
+
211
+// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
212
+//
213
+// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
214
+func PortRangeFrom(start, end uint16, proto IPProtocol) (pr PortRange, ok bool) {
215
+	if end < start || proto == "" {
216
+		return PortRange{}, false
217
+	}
218
+	normalized := normalizePortProto(string(proto))
219
+	return PortRange{start: start, end: end, proto: normalized}, true
220
+}
221
+
222
+// Start returns pr's start port number.
223
+func (pr PortRange) Start() uint16 {
224
+	return pr.start
225
+}
226
+
227
+// End returns pr's end port number.
228
+func (pr PortRange) End() uint16 {
229
+	return pr.end
230
+}
231
+
232
+// Proto returns pr's network protocol.
233
+func (pr PortRange) Proto() IPProtocol {
234
+	return pr.proto.Value()
235
+}
236
+
237
+// IsZero reports whether pr is the zero value.
238
+func (pr PortRange) IsZero() bool {
239
+	return pr.proto == protoZero
240
+}
241
+
242
+// IsValid reports whether pr is an initialized valid port range (not the zero value).
243
+func (pr PortRange) IsValid() bool {
244
+	return pr.proto != protoZero
245
+}
246
+
247
+// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
248
+// If the port range is the zero value, it returns "invalid port range".
249
+func (pr PortRange) String() string {
250
+	switch pr.proto {
251
+	case protoZero:
252
+		return "invalid port range"
253
+	default:
254
+		return string(pr.AppendTo(nil))
255
+	}
256
+}
257
+
258
+// AppendText implements [encoding.TextAppender] interface.
259
+// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface.
260
+func (pr PortRange) AppendText(b []byte) ([]byte, error) {
261
+	return pr.AppendTo(b), nil
262
+}
263
+
264
+// AppendTo appends a text encoding of pr to b and returns the extended buffer.
265
+func (pr PortRange) AppendTo(b []byte) []byte {
266
+	if pr.IsZero() {
267
+		return b
268
+	}
269
+	if pr.start == pr.end {
270
+		return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value())
271
+	}
272
+	return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value())
273
+}
274
+
275
+// MarshalText implements [encoding.TextMarshaler] interface.
276
+func (pr PortRange) MarshalText() ([]byte, error) {
277
+	return pr.AppendText(nil)
278
+}
279
+
280
+// UnmarshalText implements [encoding.TextUnmarshaler] interface.
281
+func (pr *PortRange) UnmarshalText(text []byte) error {
282
+	if len(text) == 0 {
283
+		*pr = PortRange{}
284
+		return nil
285
+	}
286
+
287
+	portRange, err := ParsePortRange(string(text))
288
+	if err != nil {
289
+		return err
290
+	}
291
+	*pr = portRange
292
+	return nil
293
+}
294
+
295
+// Range returns pr.
296
+func (pr PortRange) Range() PortRange {
297
+	return pr
298
+}
299
+
300
+// All returns an iterator over all the individual ports in the range.
301
+//
302
+// For example:
303
+//
304
+//	for port := range pr.All() {
305
+//	    // ...
306
+//	}
307
+func (pr PortRange) All() iter.Seq[Port] {
308
+	return func(yield func(Port) bool) {
309
+		for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
310
+			if !yield(Port{num: uint16(i), proto: pr.proto}) {
311
+				return
312
+			}
313
+		}
314
+	}
315
+}
316
+
317
+// parsePortNumber parses rawPort into an int, unwrapping strconv errors
318
+// and returning a single "out of range" error for any value outside 0–65535.
319
+func parsePortNumber(rawPort string) (uint16, error) {
320
+	if rawPort == "" {
321
+		return 0, errors.New("value is empty")
322
+	}
323
+	port, err := strconv.ParseUint(rawPort, 10, 16)
324
+	if err != nil {
325
+		var numErr *strconv.NumError
326
+		if errors.As(err, &numErr) {
327
+			err = numErr.Err
328
+		}
329
+		return 0, err
330
+	}
331
+
332
+	return uint16(port), nil
333
+}
334
+
335
+// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent.
336
+// If proto is not specified, it defaults to "tcp".
337
+func normalizePortProto(proto string) unique.Handle[IPProtocol] {
338
+	if proto == "" {
339
+		return unique.Make(TCP)
340
+	}
341
+
342
+	proto = strings.ToLower(proto)
343
+
344
+	return unique.Make(IPProtocol(proto))
345
+}
0 346
new file mode 100644
... ...
@@ -0,0 +1,600 @@
0
+package network
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"reflect"
6
+	"slices"
7
+	"strings"
8
+	"testing"
9
+
10
+	"gotest.tools/v3/assert"
11
+)
12
+
13
+type TestRanger interface {
14
+	Range() PortRange
15
+}
16
+
17
+var _ TestRanger = Port{}
18
+var _ TestRanger = PortRange{}
19
+
20
+func TestPort(t *testing.T) {
21
+	t.Run("Zero Value", func(t *testing.T) {
22
+		var p Port
23
+		assert.Check(t, p.IsZero())
24
+		assert.Check(t, !p.IsValid())
25
+		assert.Equal(t, p.String(), "invalid port")
26
+
27
+		t.Run("Marshal Unmarshal", func(t *testing.T) {
28
+			var p Port
29
+			bytes, err := p.MarshalText()
30
+			assert.NilError(t, err)
31
+			assert.Check(t, len(bytes) == 0)
32
+
33
+			err = p.UnmarshalText([]byte(""))
34
+			assert.NilError(t, err)
35
+			assert.Equal(t, p, Port{})
36
+		})
37
+
38
+		t.Run("JSON Marshal Unmarshal", func(t *testing.T) {
39
+			var p Port
40
+			bytes, err := json.Marshal(p)
41
+			assert.NilError(t, err)
42
+			assert.Equal(t, string(bytes), `""`)
43
+
44
+			err = json.Unmarshal([]byte(`""`), &p)
45
+			assert.NilError(t, err)
46
+			assert.Equal(t, p, Port{})
47
+		})
48
+	})
49
+
50
+	t.Run("PortFrom", func(t *testing.T) {
51
+		tests := []struct {
52
+			num   uint16
53
+			proto IPProtocol
54
+		}{
55
+			{0, TCP},
56
+			{80, TCP},
57
+			{8080, TCP},
58
+			{65535, TCP},
59
+			{80, UDP},
60
+			{8080, SCTP},
61
+		}
62
+
63
+		for _, tc := range tests {
64
+			t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) {
65
+				p, ok := PortFrom(tc.num, tc.proto)
66
+				assert.Check(t, ok)
67
+				assert.Equal(t, p.Num(), tc.num)
68
+				assert.Equal(t, p.Proto(), tc.proto)
69
+			})
70
+		}
71
+
72
+		t.Run("Normalize Protocol", func(t *testing.T) {
73
+			pr1 := portFrom(1234, "tcp")
74
+			pr2 := portFrom(1234, "TCP")
75
+			pr3 := portFrom(1234, "tCp")
76
+			assert.Equal(t, pr1, pr2)
77
+			assert.Equal(t, pr2, pr3)
78
+		})
79
+
80
+		negativeTests := []struct {
81
+			num   uint16
82
+			proto IPProtocol
83
+		}{
84
+			{0, ""},
85
+			{80, ""},
86
+		}
87
+		for _, tc := range negativeTests {
88
+			t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) {
89
+				p, ok := PortFrom(tc.num, tc.proto)
90
+				assert.Check(t, !ok)
91
+				assert.Check(t, p.IsZero())
92
+				assert.Check(t, !p.IsValid())
93
+				assert.Equal(t, p.String(), "invalid port")
94
+			})
95
+		}
96
+	})
97
+
98
+	t.Run("ParsePort", func(t *testing.T) {
99
+		tests := []struct {
100
+			in        string
101
+			port      Port      // output of ParsePort()
102
+			str       string    // output of String().
103
+			portRange PortRange // output of Range()
104
+		}{
105
+			// Zero port
106
+			{
107
+				in:        "0/tcp",
108
+				port:      portFrom(0, TCP),
109
+				str:       "0/tcp",
110
+				portRange: portRangeFrom(0, 0, TCP),
111
+			},
112
+			// Max valid port
113
+			{
114
+				in:        "65535/tcp",
115
+				port:      portFrom(65535, TCP),
116
+				str:       "65535/tcp",
117
+				portRange: portRangeFrom(65535, 65535, TCP),
118
+			},
119
+			// Simple valid ports
120
+			{
121
+				in:        "1234/tcp",
122
+				port:      portFrom(1234, TCP),
123
+				str:       "1234/tcp",
124
+				portRange: portRangeFrom(1234, 1234, TCP),
125
+			},
126
+			{
127
+				in:        "1234/udp",
128
+				port:      portFrom(1234, UDP),
129
+				str:       "1234/udp",
130
+				portRange: portRangeFrom(1234, 1234, UDP),
131
+			},
132
+			{
133
+				in:        "1234/sctp",
134
+				port:      portFrom(1234, SCTP),
135
+				str:       "1234/sctp",
136
+				portRange: portRangeFrom(1234, 1234, SCTP),
137
+			},
138
+			// Default protocol is tcp
139
+			{
140
+				in:        "1234",
141
+				port:      portFrom(1234, TCP),
142
+				str:       "1234/tcp",
143
+				portRange: portRangeFrom(1234, 1234, TCP),
144
+			},
145
+			// Default protocol is tcp
146
+			{
147
+				in:        "1234/",
148
+				port:      portFrom(1234, TCP),
149
+				str:       "1234/tcp",
150
+				portRange: portRangeFrom(1234, 1234, TCP),
151
+			},
152
+			{
153
+				in:        "1234/tcp:ipv6only",
154
+				port:      portFrom(1234, "tcp:ipv6only"),
155
+				str:       "1234/tcp:ipv6only",
156
+				portRange: portRangeFrom(1234, 1234, "tcp:ipv6only"),
157
+			},
158
+		}
159
+
160
+		for _, tc := range tests {
161
+			t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) {
162
+				got, err := ParsePort(tc.in)
163
+				assert.NilError(t, err)
164
+				assert.Equal(t, got, tc.port)
165
+
166
+				MustParsePort(tc.in) // should not panic
167
+
168
+				assert.Check(t, !got.IsZero())
169
+				assert.Check(t, got.IsValid())
170
+
171
+				// Check that ParsePort is a pure function.
172
+				got2, err := ParsePort(tc.in)
173
+				assert.NilError(t, err)
174
+				assert.Equal(t, got2, got)
175
+
176
+				// Check that ParsePort(port.String()) is the identity function.
177
+				got3, err := ParsePort(got.String())
178
+				assert.NilError(t, err)
179
+				assert.Equal(t, got3, got)
180
+
181
+				// Check String() output
182
+				s := got.String()
183
+				wants := tc.str
184
+				if wants == "" {
185
+					wants = tc.in
186
+				}
187
+				assert.Equal(t, s, wants)
188
+
189
+				js := `"` + tc.in + `"`
190
+				var jsgot Port
191
+				err = json.Unmarshal([]byte(js), &jsgot)
192
+				assert.NilError(t, err)
193
+				assert.Equal(t, jsgot, got)
194
+
195
+				jsb, err := json.Marshal(jsgot)
196
+				assert.NilError(t, err)
197
+
198
+				jswant := `"` + wants + `"`
199
+				assert.Equal(t, string(jsb), jswant)
200
+
201
+				// Check Range() output
202
+				r := got.Range()
203
+				assert.Equal(t, r, tc.portRange)
204
+			})
205
+		}
206
+
207
+		t.Run("Normalize Protocol", func(t *testing.T) {
208
+			p1 := MustParsePort("1234/tcp")
209
+			p2 := MustParsePort("1234/TCP")
210
+			p3 := MustParsePort("1234/tCp")
211
+			assert.Equal(t, p1, p2)
212
+			assert.Equal(t, p2, p3)
213
+		})
214
+
215
+		negativeTests := []string{
216
+			// Empty string
217
+			"",
218
+			// Whitespace-only string
219
+			" ",
220
+			// No port number
221
+			"/",
222
+			// No port number (protocol only)
223
+			"/tcp",
224
+			// Negative port
225
+			"-1",
226
+			// Too large port
227
+			"65536",
228
+			// Non-numeric port
229
+			"foo",
230
+			// Port range instead of single port
231
+			"1234-1240/udp",
232
+			// Port range instead of single port without protocol
233
+			"1234-1240",
234
+			// Garbage port
235
+			"asd1234/tcp",
236
+		}
237
+
238
+		for _, s := range negativeTests {
239
+			t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) {
240
+				got, err := ParsePort(s)
241
+				assert.ErrorContains(t, err, "invalid port")
242
+				assert.Check(t, got.IsZero())
243
+				assert.Check(t, !got.IsValid())
244
+
245
+				// Skip JSON unmarshalling test for empty string as that should succeed.
246
+				// See test "Zero Value" above.
247
+				if s == "" {
248
+					return
249
+				}
250
+
251
+				var jsgot Port
252
+				js := []byte(`"` + s + `"`)
253
+				err = json.Unmarshal(js, &jsgot)
254
+				assert.ErrorContains(t, err, "invalid port")
255
+				assert.Equal(t, jsgot, Port{})
256
+			})
257
+		}
258
+	})
259
+}
260
+
261
+func TestPortRange(t *testing.T) {
262
+	t.Run("Zero Value", func(t *testing.T) {
263
+		var pr PortRange
264
+		assert.Check(t, pr.IsZero())
265
+		assert.Check(t, !pr.IsValid())
266
+		assert.Equal(t, pr.String(), "invalid port range")
267
+
268
+		t.Run("Marshal Unmarshal", func(t *testing.T) {
269
+			var pr PortRange
270
+			bytes, err := pr.MarshalText()
271
+			assert.NilError(t, err)
272
+			assert.Check(t, len(bytes) == 0)
273
+
274
+			err = pr.UnmarshalText([]byte(""))
275
+			assert.NilError(t, err)
276
+			assert.Equal(t, pr, PortRange{})
277
+		})
278
+
279
+		t.Run("JSON Marshal Unmarshal", func(t *testing.T) {
280
+			var pr PortRange
281
+			bytes, err := json.Marshal(pr)
282
+			assert.NilError(t, err)
283
+			assert.Equal(t, string(bytes), `""`)
284
+
285
+			err = json.Unmarshal([]byte(`""`), &pr)
286
+			assert.NilError(t, err)
287
+			assert.Equal(t, pr, PortRange{})
288
+		})
289
+	})
290
+
291
+	t.Run("PortRangeFrom", func(t *testing.T) {
292
+		tests := []struct {
293
+			start uint16
294
+			end   uint16
295
+			proto IPProtocol
296
+		}{
297
+			{0, 0, TCP},
298
+			{0, 1234, TCP},
299
+			{80, 80, TCP},
300
+			{80, 8080, TCP},
301
+			{1234, 65535, TCP},
302
+			{80, 80, UDP},
303
+			{80, 8080, SCTP},
304
+		}
305
+
306
+		for _, tc := range tests {
307
+			t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) {
308
+				pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto)
309
+				assert.Check(t, ok)
310
+				assert.Equal(t, pr.Start(), tc.start)
311
+				assert.Equal(t, pr.End(), tc.end)
312
+				assert.Equal(t, pr.Proto(), tc.proto)
313
+			})
314
+		}
315
+
316
+		t.Run("Normalize Protocol", func(t *testing.T) {
317
+			pr1, _ := PortRangeFrom(1234, 5678, "tcp")
318
+			pr2, _ := PortRangeFrom(1234, 5678, "TCP")
319
+			pr3, _ := PortRangeFrom(1234, 5678, "tCp")
320
+			assert.Equal(t, pr1, pr2)
321
+			assert.Equal(t, pr2, pr3)
322
+		})
323
+
324
+		negativeTests := []struct {
325
+			start uint16
326
+			end   uint16
327
+			proto IPProtocol
328
+		}{
329
+			{1234, 80, TCP}, // end < start
330
+			{0, 0, ""},      // empty protocol
331
+		}
332
+		for _, tc := range negativeTests {
333
+			t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) {
334
+				pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto)
335
+				assert.Check(t, !ok)
336
+				assert.Check(t, pr.IsZero())
337
+				assert.Check(t, !pr.IsValid())
338
+			})
339
+		}
340
+	})
341
+
342
+	t.Run("ParsePortRange", func(t *testing.T) {
343
+		tests := []struct {
344
+			in        string
345
+			portRange PortRange // output of ParsePortRange() and Range()
346
+			str       string    // output of String(). If "", use in.
347
+
348
+		}{
349
+			// Zero port
350
+			{
351
+				in:        "0-1234/tcp",
352
+				portRange: portRangeFrom(0, 1234, TCP),
353
+				str:       "0-1234/tcp",
354
+			},
355
+			// Max valid port
356
+			{
357
+				in:        "1234-65535/tcp",
358
+				portRange: portRangeFrom(1234, 65535, TCP),
359
+				str:       "1234-65535/tcp",
360
+			},
361
+			// Simple valid ports
362
+			{
363
+				in:        "1234-4567/tcp",
364
+				portRange: portRangeFrom(1234, 4567, TCP),
365
+				str:       "1234-4567/tcp",
366
+			},
367
+			{
368
+				in:        "1234-4567/udp",
369
+				portRange: portRangeFrom(1234, 4567, UDP),
370
+				str:       "1234-4567/udp",
371
+			},
372
+			// Default protocol is tcp
373
+			{
374
+				in:        "1234-4567",
375
+				portRange: portRangeFrom(1234, 4567, TCP),
376
+				str:       "1234-4567/tcp",
377
+			},
378
+			// Default protocol is tcp
379
+			{
380
+				in:        "1234-4567/",
381
+				portRange: portRangeFrom(1234, 4567, TCP),
382
+				str:       "1234-4567/tcp",
383
+			},
384
+			{
385
+				in:        "1234/tcp",
386
+				portRange: portRangeFrom(1234, 1234, TCP),
387
+				str:       "1234/tcp",
388
+			},
389
+			{
390
+				in:        "1234",
391
+				portRange: portRangeFrom(1234, 1234, TCP),
392
+				str:       "1234/tcp",
393
+			},
394
+			{
395
+				in:        "1234-5678/tcp:ipv6only",
396
+				portRange: portRangeFrom(1234, 5678, "tcp:ipv6only"),
397
+				str:       "1234-5678/tcp:ipv6only",
398
+			},
399
+		}
400
+
401
+		for _, tc := range tests {
402
+			t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) {
403
+				got, err := ParsePortRange(tc.in)
404
+				assert.NilError(t, err)
405
+				assert.Equal(t, got, tc.portRange)
406
+				assert.Check(t, !got.IsZero())
407
+				assert.Check(t, got.IsValid())
408
+
409
+				MustParsePortRange(tc.in) // should not panic
410
+
411
+				// Check that ParsePortRange is a pure function.
412
+				got2, err := ParsePortRange(tc.in)
413
+				assert.NilError(t, err)
414
+				assert.Equal(t, got2, got)
415
+
416
+				// Check that ParsePortRange(port.String()) is the identity function.
417
+				got3, err := ParsePortRange(got.String())
418
+				assert.NilError(t, err)
419
+				assert.Equal(t, got3, got)
420
+
421
+				// Check String() output
422
+				s := got.String()
423
+				wants := tc.str
424
+				if wants == "" {
425
+					wants = tc.in
426
+				}
427
+				assert.Equal(t, s, wants)
428
+
429
+				js := `"` + tc.in + `"`
430
+				var jsgot PortRange
431
+				err = json.Unmarshal([]byte(js), &jsgot)
432
+				assert.NilError(t, err)
433
+				assert.Equal(t, jsgot, got)
434
+
435
+				jsb, err := json.Marshal(jsgot)
436
+				assert.NilError(t, err)
437
+				jswant := `"` + wants + `"`
438
+				assert.Equal(t, string(jsb), jswant)
439
+
440
+				// Check Range() output
441
+				r := got.Range()
442
+				assert.Equal(t, r, tc.portRange)
443
+			})
444
+
445
+			t.Run("Normalize Protocol", func(t *testing.T) {
446
+				pr1 := MustParsePortRange("1234-5678/tcp")
447
+				pr2 := MustParsePortRange("1234-5678/TCP")
448
+				pr3 := MustParsePortRange("1234-5678/tCp")
449
+				assert.Equal(t, pr1, pr2)
450
+				assert.Equal(t, pr2, pr3)
451
+			})
452
+
453
+			negativeTests := []string{
454
+				// Empty string
455
+				"",
456
+				// Whitespace-only string
457
+				" ",
458
+				// No port number
459
+				"/",
460
+				// No port number (protocol only)
461
+				"/tcp",
462
+				// Negative start port
463
+				"-1-1234",
464
+				// Negative end port
465
+				"1234--1",
466
+				// Too large start port
467
+				"65536-65537",
468
+				// Too large end port
469
+				"1234-65536",
470
+				// Non-numeric start port
471
+				"foo-1234",
472
+				// Non-numeric end port
473
+				"1234-bar",
474
+				// Start port greater than end port
475
+				"1234-1000",
476
+				// Garbage port range
477
+				"asd1234-5678/tcp",
478
+			}
479
+
480
+			for _, s := range negativeTests {
481
+				t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) {
482
+					got, err := ParsePortRange(s)
483
+					assert.Check(t, err != nil)
484
+					assert.Check(t, got.IsZero())
485
+					assert.Check(t, !got.IsValid())
486
+
487
+					// Skip JSON unmarshalling test for empty string as that should succeed.
488
+					// See test "Zero Value" above.
489
+					if s == "" {
490
+						return
491
+					}
492
+
493
+					var jsgot PortRange
494
+					js := []byte(`"` + s + `"`)
495
+					err = json.Unmarshal(js, &jsgot)
496
+					assert.Check(t, err != nil)
497
+					assert.Equal(t, jsgot, PortRange{})
498
+				})
499
+			}
500
+		}
501
+	})
502
+
503
+	t.Run("PortRange All()", func(t *testing.T) {
504
+		tests := []struct {
505
+			in   string
506
+			want []Port
507
+		}{
508
+			{
509
+				in:   "1000-1000/tcp",
510
+				want: []Port{portFrom(1000, TCP)},
511
+			},
512
+			{
513
+				in:   "1000-1002/tcp",
514
+				want: []Port{portFrom(1000, TCP), portFrom(1001, TCP), portFrom(1002, TCP)},
515
+			},
516
+			{
517
+				in:   "0-0/tcp",
518
+				want: []Port{portFrom(0, TCP)},
519
+			},
520
+			{
521
+				in:   "65535-65535/tcp",
522
+				want: []Port{portFrom(65535, TCP)},
523
+			},
524
+			{
525
+				in:   "65530-65535/tcp",
526
+				want: []Port{portFrom(65530, TCP), portFrom(65531, TCP), portFrom(65532, TCP), portFrom(65533, TCP), portFrom(65534, TCP), portFrom(65535, TCP)},
527
+			},
528
+		}
529
+
530
+		for _, tc := range tests {
531
+			pr := MustParsePortRange(tc.in)
532
+			ports := slices.Collect(pr.All())
533
+			if !reflect.DeepEqual(ports, tc.want) {
534
+				t.Errorf("PortRange.All() = %#v, want %#v", ports, tc.want)
535
+			}
536
+		}
537
+
538
+		t.Run("All() stop early", func(t *testing.T) {
539
+			want := []Port{portFrom(1000, TCP), portFrom(1001, TCP)}
540
+			pr := MustParsePortRange("1000-2000/tcp")
541
+			var ports []Port
542
+			for p := range pr.All() {
543
+				ports = append(ports, p)
544
+				if len(ports) == 2 {
545
+					break
546
+				}
547
+			}
548
+			if !reflect.DeepEqual(ports, want) {
549
+				t.Errorf("PortRange.All() = %#v, want %#v", ports, want)
550
+			}
551
+		})
552
+	})
553
+}
554
+
555
+func BenchmarkPortRangeAll(b *testing.B) {
556
+	b.Run("Single Port", func(b *testing.B) {
557
+		pr := MustParsePortRange("1234/tcp")
558
+		b.ResetTimer()
559
+		for i := 0; i < b.N; i++ {
560
+			var sink int64
561
+			for p := range pr.All() {
562
+				sink += int64(p.Num()) // prevent compiler optimization
563
+			}
564
+			if sink < 0 {
565
+				b.Fatal("unreachable")
566
+			}
567
+		}
568
+	})
569
+
570
+	b.Run("Range", func(b *testing.B) {
571
+		pr := MustParsePortRange("0-65535/tcp")
572
+		b.ResetTimer()
573
+		for i := 0; i < b.N; i++ {
574
+			var sink int64
575
+			for p := range pr.All() {
576
+				sink += int64(p.Num()) // prevent compiler optimization
577
+			}
578
+			if sink < 0 {
579
+				b.Fatal("unreachable")
580
+			}
581
+		}
582
+	})
583
+}
584
+
585
+func portFrom(num uint16, proto IPProtocol) Port {
586
+	p, ok := PortFrom(num, proto)
587
+	if !ok {
588
+		panic("invalid port")
589
+	}
590
+	return p
591
+}
592
+
593
+func portRangeFrom(start, end uint16, proto IPProtocol) PortRange {
594
+	pr, ok := PortRangeFrom(start, end, proto)
595
+	if !ok {
596
+		panic("invalid port range")
597
+	}
598
+	return pr
599
+}
... ...
@@ -21,8 +21,8 @@ import (
21 21
 	"github.com/moby/buildkit/frontend/dockerfile/instructions"
22 22
 	"github.com/moby/buildkit/frontend/dockerfile/parser"
23 23
 	"github.com/moby/buildkit/frontend/dockerfile/shell"
24
-	"github.com/moby/moby/api/types/container"
25 24
 	"github.com/moby/moby/api/types/jsonstream"
25
+	"github.com/moby/moby/api/types/network"
26 26
 	"github.com/moby/moby/v2/daemon/builder"
27 27
 	"github.com/moby/moby/v2/daemon/internal/image"
28 28
 	"github.com/moby/moby/v2/daemon/internal/netiputil"
... ...
@@ -533,7 +533,7 @@ func dispatchExpose(ctx context.Context, d dispatchRequest, c *instructions.Expo
533 533
 	}
534 534
 
535 535
 	if d.state.runConfig.ExposedPorts == nil {
536
-		d.state.runConfig.ExposedPorts = make(container.PortSet)
536
+		d.state.runConfig.ExposedPorts = make(network.PortSet)
537 537
 	}
538 538
 	for p := range ps {
539 539
 		d.state.runConfig.ExposedPorts[p] = struct{}{}
... ...
@@ -546,10 +546,10 @@ func dispatchExpose(ctx context.Context, d dispatchRequest, c *instructions.Expo
546 546
 //
547 547
 // parsePortSpecs receives port specs in the format of ip:public:private/proto and parses
548 548
 // these in to the internal types
549
-func parsePortSpecs(ports []string) (map[container.Port]struct{}, map[container.Port][]container.PortBinding, error) {
549
+func parsePortSpecs(ports []string) (map[network.Port]struct{}, network.PortMap, error) {
550 550
 	var (
551
-		exposedPorts = make(map[container.Port]struct{}, len(ports))
552
-		bindings     = make(map[container.Port][]container.PortBinding)
551
+		exposedPorts = make(map[network.Port]struct{}, len(ports))
552
+		bindings     = make(network.PortMap)
553 553
 	)
554 554
 	for _, p := range ports {
555 555
 		portMappings, err := parsePortSpec(p)
... ...
@@ -571,8 +571,8 @@ func parsePortSpecs(ports []string) (map[container.Port]struct{}, map[container.
571 571
 
572 572
 // Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L172-L237
573 573
 //
574
-// parsePortSpec parses a port specification string into a slice of [container.PortMap]
575
-func parsePortSpec(rawPort string) ([]container.PortMap, error) {
574
+// parsePortSpec parses a port specification string into a slice of [network.PortMap]
575
+func parsePortSpec(rawPort string) ([]network.PortMap, error) {
576 576
 	ip, hostPort, containerPort := splitParts(rawPort)
577 577
 	proto, containerPort := splitProtoPort(containerPort)
578 578
 	if containerPort == "" {
... ...
@@ -597,7 +597,7 @@ func parsePortSpec(rawPort string) ([]container.PortMap, error) {
597 597
 		return nil, fmt.Errorf("invalid IP address: %w", err)
598 598
 	}
599 599
 
600
-	pr, err := container.ParsePortRange(containerPort)
600
+	pr, err := network.ParsePortRange(containerPort)
601 601
 	if err != nil {
602 602
 		return nil, errors.New("invalid containerPort: " + containerPort)
603 603
 	}
... ...
@@ -609,7 +609,7 @@ func parsePortSpec(rawPort string) ([]container.PortMap, error) {
609 609
 
610 610
 	var startHostPort, endHostPort uint16
611 611
 	if hostPort != "" {
612
-		hostPortRange, err := container.ParsePortRange(hostPort)
612
+		hostPortRange, err := network.ParsePortRange(hostPort)
613 613
 		if err != nil {
614 614
 			return nil, errors.New("invalid hostPort: " + hostPort)
615 615
 		}
... ...
@@ -626,7 +626,7 @@ func parsePortSpec(rawPort string) ([]container.PortMap, error) {
626 626
 	}
627 627
 
628 628
 	count := endPort - startPort + 1
629
-	ports := make([]container.PortMap, 0, count)
629
+	ports := make([]network.PortMap, 0, count)
630 630
 
631 631
 	for i := uint16(0); i < count; i++ {
632 632
 		hPort := ""
... ...
@@ -638,8 +638,8 @@ func parsePortSpec(rawPort string) ([]container.PortMap, error) {
638 638
 				hPort += "-" + strconv.Itoa(int(endHostPort))
639 639
 			}
640 640
 		}
641
-		ports = append(ports, container.PortMap{
642
-			container.MustParsePort(fmt.Sprintf("%d/%s", startPort+i, proto)): []container.PortBinding{{HostIP: addr, HostPort: hPort}},
641
+		ports = append(ports, network.PortMap{
642
+			network.MustParsePort(fmt.Sprintf("%d/%s", startPort+i, proto)): []network.PortBinding{{HostIP: addr, HostPort: hPort}},
643 643
 		})
644 644
 	}
645 645
 	return ports, nil
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/moby/buildkit/frontend/dockerfile/parser"
15 15
 	"github.com/moby/buildkit/frontend/dockerfile/shell"
16 16
 	"github.com/moby/moby/api/types/container"
17
+	"github.com/moby/moby/api/types/network"
17 18
 	"github.com/moby/moby/v2/daemon/builder"
18 19
 	"github.com/moby/moby/v2/daemon/internal/image"
19 20
 	"github.com/moby/moby/v2/daemon/pkg/oci"
... ...
@@ -337,7 +338,7 @@ func TestExpose(t *testing.T) {
337 337
 	assert.Assert(t, sb.state.runConfig.ExposedPorts != nil)
338 338
 	assert.Assert(t, is.Len(sb.state.runConfig.ExposedPorts, 1))
339 339
 
340
-	assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, container.MustParsePort("80/tcp")))
340
+	assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, network.MustParsePort("80/tcp")))
341 341
 }
342 342
 
343 343
 func TestUser(t *testing.T) {
... ...
@@ -632,29 +633,29 @@ func TestDispatchUnsupportedOptions(t *testing.T) {
632 632
 // Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L390-L499
633 633
 func TestParsePortSpecs(t *testing.T) {
634 634
 	var (
635
-		portMap    map[container.Port]struct{}
636
-		bindingMap map[container.Port][]container.PortBinding
635
+		portSet    network.PortSet
636
+		bindingMap network.PortMap
637 637
 		err        error
638 638
 	)
639 639
 
640
-	tcp1234 := container.MustParsePort("1234/tcp")
641
-	udp2345 := container.MustParsePort("2345/udp")
642
-	sctp3456 := container.MustParsePort("3456/sctp")
640
+	tcp1234 := network.MustParsePort("1234/tcp")
641
+	udp2345 := network.MustParsePort("2345/udp")
642
+	sctp3456 := network.MustParsePort("3456/sctp")
643 643
 
644
-	portMap, bindingMap, err = parsePortSpecs([]string{tcp1234.String(), udp2345.String(), sctp3456.String()})
644
+	portSet, bindingMap, err = parsePortSpecs([]string{tcp1234.String(), udp2345.String(), sctp3456.String()})
645 645
 	if err != nil {
646 646
 		t.Fatalf("Error while processing ParsePortSpecs: %s", err)
647 647
 	}
648 648
 
649
-	if _, ok := portMap[tcp1234]; !ok {
649
+	if _, ok := portSet[tcp1234]; !ok {
650 650
 		t.Fatal("1234/tcp was not parsed properly")
651 651
 	}
652 652
 
653
-	if _, ok := portMap[udp2345]; !ok {
653
+	if _, ok := portSet[udp2345]; !ok {
654 654
 		t.Fatal("2345/udp was not parsed properly")
655 655
 	}
656 656
 
657
-	if _, ok := portMap[sctp3456]; !ok {
657
+	if _, ok := portSet[sctp3456]; !ok {
658 658
 		t.Fatal("3456/sctp was not parsed properly")
659 659
 	}
660 660
 
... ...
@@ -672,20 +673,20 @@ func TestParsePortSpecs(t *testing.T) {
672 672
 		}
673 673
 	}
674 674
 
675
-	portMap, bindingMap, err = parsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp", "3456:3456/sctp"})
675
+	portSet, bindingMap, err = parsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp", "3456:3456/sctp"})
676 676
 	if err != nil {
677 677
 		t.Fatalf("Error while processing ParsePortSpecs: %s", err)
678 678
 	}
679 679
 
680
-	if _, ok := portMap[tcp1234]; !ok {
680
+	if _, ok := portSet[tcp1234]; !ok {
681 681
 		t.Fatal("1234/tcp was not parsed properly")
682 682
 	}
683 683
 
684
-	if _, ok := portMap[udp2345]; !ok {
684
+	if _, ok := portSet[udp2345]; !ok {
685 685
 		t.Fatal("2345/udp was not parsed properly")
686 686
 	}
687 687
 
688
-	if _, ok := portMap[sctp3456]; !ok {
688
+	if _, ok := portSet[sctp3456]; !ok {
689 689
 		t.Fatal("3456/sctp was not parsed properly")
690 690
 	}
691 691
 
... ...
@@ -705,20 +706,20 @@ func TestParsePortSpecs(t *testing.T) {
705 705
 		}
706 706
 	}
707 707
 
708
-	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"})
708
+	portSet, 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"})
709 709
 	if err != nil {
710 710
 		t.Fatalf("Error while processing ParsePortSpecs: %s", err)
711 711
 	}
712 712
 
713
-	if _, ok := portMap[tcp1234]; !ok {
713
+	if _, ok := portSet[tcp1234]; !ok {
714 714
 		t.Fatal("1234/tcp was not parsed properly")
715 715
 	}
716 716
 
717
-	if _, ok := portMap[udp2345]; !ok {
717
+	if _, ok := portSet[udp2345]; !ok {
718 718
 		t.Fatal("2345/udp was not parsed properly")
719 719
 	}
720 720
 
721
-	if _, ok := portMap[sctp3456]; !ok {
721
+	if _, ok := portSet[sctp3456]; !ok {
722 722
 		t.Fatal("3456/sctp was not parsed properly")
723 723
 	}
724 724
 
... ...
@@ -784,9 +785,9 @@ func TestParsePortSpecFull(t *testing.T) {
784 784
 		t.Fatalf("expected nil error, got: %v", err)
785 785
 	}
786 786
 
787
-	expected := []container.PortMap{
787
+	expected := []network.PortMap{
788 788
 		{
789
-			container.MustParsePort("3333/tcp"): []container.PortBinding{
789
+			network.MustParsePort("3333/tcp"): []network.PortBinding{
790 790
 				{
791 791
 					HostIP:   netip.IPv4Unspecified(),
792 792
 					HostPort: "1234",
... ...
@@ -794,7 +795,7 @@ func TestParsePortSpecFull(t *testing.T) {
794 794
 			},
795 795
 		},
796 796
 		{
797
-			container.MustParsePort("3334/tcp"): []container.PortBinding{
797
+			network.MustParsePort("3334/tcp"): []network.PortBinding{
798 798
 				{
799 799
 					HostIP:   netip.IPv4Unspecified(),
800 800
 					HostPort: "1235",
... ...
@@ -813,15 +814,15 @@ func TestPartPortSpecIPV6(t *testing.T) {
813 813
 	type test struct {
814 814
 		name     string
815 815
 		spec     string
816
-		expected []container.PortMap
816
+		expected []network.PortMap
817 817
 	}
818 818
 	cases := []test{
819 819
 		{
820 820
 			name: "square angled IPV6 without host port",
821 821
 			spec: "[2001:4860:0:2001::68]::333",
822
-			expected: []container.PortMap{
822
+			expected: []network.PortMap{
823 823
 				{
824
-					container.MustParsePort("333/tcp"): []container.PortBinding{
824
+					network.MustParsePort("333/tcp"): []network.PortBinding{
825 825
 						{
826 826
 							HostIP:   netip.MustParseAddr("2001:4860:0:2001::68"),
827 827
 							HostPort: "",
... ...
@@ -833,9 +834,9 @@ func TestPartPortSpecIPV6(t *testing.T) {
833 833
 		{
834 834
 			name: "square angled IPV6 with host port",
835 835
 			spec: "[::1]:80:80",
836
-			expected: []container.PortMap{
836
+			expected: []network.PortMap{
837 837
 				{
838
-					container.MustParsePort("80/tcp"): []container.PortBinding{
838
+					network.MustParsePort("80/tcp"): []network.PortBinding{
839 839
 						{
840 840
 							HostIP:   netip.IPv6Loopback(),
841 841
 							HostPort: "80",
... ...
@@ -847,9 +848,9 @@ func TestPartPortSpecIPV6(t *testing.T) {
847 847
 		{
848 848
 			name: "IPV6 without host port",
849 849
 			spec: "2001:4860:0:2001::68::333",
850
-			expected: []container.PortMap{
850
+			expected: []network.PortMap{
851 851
 				{
852
-					container.MustParsePort("333/tcp"): []container.PortBinding{
852
+					network.MustParsePort("333/tcp"): []network.PortBinding{
853 853
 						{
854 854
 							HostIP:   netip.MustParseAddr("2001:4860:0:2001::68"),
855 855
 							HostPort: "",
... ...
@@ -861,9 +862,9 @@ func TestPartPortSpecIPV6(t *testing.T) {
861 861
 		{
862 862
 			name: "IPV6 with host port",
863 863
 			spec: "::1:80:80",
864
-			expected: []container.PortMap{
864
+			expected: []network.PortMap{
865 865
 				{
866
-					container.MustParsePort("80/tcp"): []container.PortBinding{
866
+					network.MustParsePort("80/tcp"): []network.PortBinding{
867 867
 						{
868 868
 							HostIP:   netip.IPv6Loopback(),
869 869
 							HostPort: "80",
... ...
@@ -875,9 +876,9 @@ func TestPartPortSpecIPV6(t *testing.T) {
875 875
 		{
876 876
 			name: ":: IPV6, without host port",
877 877
 			spec: "::::80",
878
-			expected: []container.PortMap{
878
+			expected: []network.PortMap{
879 879
 				{
880
-					container.MustParsePort("80/tcp"): []container.PortBinding{
880
+					network.MustParsePort("80/tcp"): []network.PortBinding{
881 881
 						{
882 882
 							HostIP:   netip.IPv6Unspecified(),
883 883
 							HostPort: "",
... ...
@@ -903,25 +904,25 @@ func TestPartPortSpecIPV6(t *testing.T) {
903 903
 // Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L501-L600
904 904
 func TestParsePortSpecsWithRange(t *testing.T) {
905 905
 	var (
906
-		portMap    map[container.Port]struct{}
907
-		bindingMap map[container.Port][]container.PortBinding
906
+		portSet    network.PortSet
907
+		bindingMap network.PortMap
908 908
 		err        error
909 909
 	)
910 910
 
911
-	portMap, bindingMap, err = parsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp", "3456-3458/sctp"})
911
+	portSet, bindingMap, err = parsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp", "3456-3458/sctp"})
912 912
 	if err != nil {
913 913
 		t.Fatalf("Error while processing ParsePortSpecs: %s", err)
914 914
 	}
915 915
 
916
-	if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok {
916
+	if _, ok := portSet[network.MustParsePort("1235/tcp")]; !ok {
917 917
 		t.Fatal("1234-1236/tcp was not parsed properly")
918 918
 	}
919 919
 
920
-	if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok {
920
+	if _, ok := portSet[network.MustParsePort("2346/udp")]; !ok {
921 921
 		t.Fatal("2345-2347/udp was not parsed properly")
922 922
 	}
923 923
 
924
-	if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok {
924
+	if _, ok := portSet[network.MustParsePort("3456/sctp")]; !ok {
925 925
 		t.Fatal("3456-3458/sctp was not parsed properly")
926 926
 	}
927 927
 
... ...
@@ -939,20 +940,20 @@ func TestParsePortSpecsWithRange(t *testing.T) {
939 939
 		}
940 940
 	}
941 941
 
942
-	portMap, bindingMap, err = parsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp", "3456-3458:3456-3458/sctp"})
942
+	portSet, bindingMap, err = parsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp", "3456-3458:3456-3458/sctp"})
943 943
 	if err != nil {
944 944
 		t.Fatalf("Error while processing ParsePortSpecs: %s", err)
945 945
 	}
946 946
 
947
-	if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok {
947
+	if _, ok := portSet[network.MustParsePort("1235/tcp")]; !ok {
948 948
 		t.Fatal("1234-1236 was not parsed properly")
949 949
 	}
950 950
 
951
-	if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok {
951
+	if _, ok := portSet[network.MustParsePort("2346/udp")]; !ok {
952 952
 		t.Fatal("2345-2347 was not parsed properly")
953 953
 	}
954 954
 
955
-	if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok {
955
+	if _, ok := portSet[network.MustParsePort("3456/sctp")]; !ok {
956 956
 		t.Fatal("3456-3458 was not parsed properly")
957 957
 	}
958 958
 
... ...
@@ -971,20 +972,20 @@ func TestParsePortSpecsWithRange(t *testing.T) {
971 971
 		}
972 972
 	}
973 973
 
974
-	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"})
974
+	portSet, 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"})
975 975
 	if err != nil {
976 976
 		t.Fatalf("Error while processing ParsePortSpecs: %s", err)
977 977
 	}
978 978
 
979
-	if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok {
979
+	if _, ok := portSet[network.MustParsePort("1235/tcp")]; !ok {
980 980
 		t.Fatal("1234-1236 was not parsed properly")
981 981
 	}
982 982
 
983
-	if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok {
983
+	if _, ok := portSet[network.MustParsePort("2346/udp")]; !ok {
984 984
 		t.Fatal("2345-2347 was not parsed properly")
985 985
 	}
986 986
 
987
-	if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok {
987
+	if _, ok := portSet[network.MustParsePort("3456/sctp")]; !ok {
988 988
 		t.Fatal("3456-3458 was not parsed properly")
989 989
 	}
990 990
 
... ...
@@ -8,6 +8,7 @@ import (
8 8
 
9 9
 	"github.com/moby/go-archive"
10 10
 	"github.com/moby/moby/api/types/container"
11
+	"github.com/moby/moby/api/types/network"
11 12
 	"github.com/moby/moby/v2/daemon/builder"
12 13
 	"github.com/moby/moby/v2/daemon/builder/remotecontext"
13 14
 	"github.com/moby/moby/v2/daemon/internal/image"
... ...
@@ -135,9 +136,9 @@ func fullMutableRunConfig() *container.Config {
135 135
 	return &container.Config{
136 136
 		Cmd: []string{"command", "arg1"},
137 137
 		Env: []string{"env1=foo", "env2=bar"},
138
-		ExposedPorts: container.PortSet{
139
-			container.MustParsePort("1000/tcp"): {},
140
-			container.MustParsePort("1001/tcp"): {},
138
+		ExposedPorts: network.PortSet{
139
+			network.MustParsePort("1000/tcp"): {},
140
+			network.MustParsePort("1001/tcp"): {},
141 141
 		},
142 142
 		Volumes: map[string]struct{}{
143 143
 			"one": {},
... ...
@@ -160,7 +161,7 @@ func TestDeepCopyRunConfig(t *testing.T) {
160 160
 
161 161
 	ctrCfg.Cmd[1] = "arg2"
162 162
 	ctrCfg.Env[1] = "env2=new"
163
-	ctrCfg.ExposedPorts[container.MustParsePort("10002")] = struct{}{}
163
+	ctrCfg.ExposedPorts[network.MustParsePort("10002")] = struct{}{}
164 164
 	ctrCfg.Volumes["three"] = struct{}{}
165 165
 	ctrCfg.Entrypoint[1] = "arg2"
166 166
 	ctrCfg.OnBuild[0] = "start"
... ...
@@ -143,8 +143,8 @@ func (c *containerConfig) image() string {
143 143
 	return reference.FamiliarString(reference.TagNameOnly(ref))
144 144
 }
145 145
 
146
-func (c *containerConfig) portBindings() container.PortMap {
147
-	portBindings := container.PortMap{}
146
+func (c *containerConfig) portBindings() network.PortMap {
147
+	portBindings := network.PortMap{}
148 148
 	if c.task.Endpoint == nil {
149 149
 		return portBindings
150 150
 	}
... ...
@@ -158,12 +158,12 @@ func (c *containerConfig) portBindings() container.PortMap {
158 158
 			continue
159 159
 		}
160 160
 
161
-		port, ok := container.PortFrom(uint16(portConfig.TargetPort), container.NetworkProtocol(portConfig.Protocol.String()))
161
+		port, ok := network.PortFrom(uint16(portConfig.TargetPort), network.IPProtocol(portConfig.Protocol.String()))
162 162
 		if !ok {
163 163
 			continue
164 164
 		}
165 165
 
166
-		binding := []container.PortBinding{
166
+		binding := []network.PortBinding{
167 167
 			{},
168 168
 		}
169 169
 
... ...
@@ -188,8 +188,8 @@ func (c *containerConfig) init() *bool {
188 188
 	return &init
189 189
 }
190 190
 
191
-func (c *containerConfig) exposedPorts() map[container.Port]struct{} {
192
-	exposedPorts := make(map[container.Port]struct{})
191
+func (c *containerConfig) exposedPorts() map[network.Port]struct{} {
192
+	exposedPorts := make(map[network.Port]struct{})
193 193
 	if c.task.Endpoint == nil {
194 194
 		return exposedPorts
195 195
 	}
... ...
@@ -203,7 +203,7 @@ func (c *containerConfig) exposedPorts() map[container.Port]struct{} {
203 203
 			continue
204 204
 		}
205 205
 
206
-		port, ok := container.PortFrom(uint16(portConfig.TargetPort), container.NetworkProtocol(portConfig.Protocol.String()))
206
+		port, ok := network.PortFrom(uint16(portConfig.TargetPort), network.IPProtocol(portConfig.Protocol.String()))
207 207
 		if !ok {
208 208
 			continue
209 209
 		}
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	gogotypes "github.com/gogo/protobuf/types"
12 12
 	"github.com/moby/moby/api/types/container"
13 13
 	"github.com/moby/moby/api/types/events"
14
+	"github.com/moby/moby/api/types/network"
14 15
 	executorpkg "github.com/moby/moby/v2/daemon/cluster/executor"
15 16
 	"github.com/moby/moby/v2/daemon/libnetwork"
16 17
 	"github.com/moby/swarmkit/v2/agent/exec"
... ...
@@ -639,17 +640,17 @@ func parsePortStatus(ctnr container.InspectResponse) (*api.PortStatus, error) {
639 639
 	return status, nil
640 640
 }
641 641
 
642
-func parsePortMap(portMap container.PortMap) ([]*api.PortConfig, error) {
642
+func parsePortMap(portMap network.PortMap) ([]*api.PortConfig, error) {
643 643
 	exposedPorts := make([]*api.PortConfig, 0, len(portMap))
644 644
 
645 645
 	for port, mapping := range portMap {
646 646
 		var protocol api.PortConfig_Protocol
647 647
 		switch port.Proto() {
648
-		case container.TCP:
648
+		case network.TCP:
649 649
 			protocol = api.ProtocolTCP
650
-		case container.UDP:
650
+		case network.UDP:
651 651
 			protocol = api.ProtocolUDP
652
-		case container.SCTP:
652
+		case network.SCTP:
653 653
 			protocol = api.ProtocolSCTP
654 654
 		default:
655 655
 			return nil, fmt.Errorf("invalid protocol: %s", port.Proto())
... ...
@@ -372,7 +372,7 @@ func validateHealthCheck(healthConfig *containertypes.HealthConfig) error {
372 372
 	return nil
373 373
 }
374 374
 
375
-func validatePortBindings(ports containertypes.PortMap) error {
375
+func validatePortBindings(ports networktypes.PortMap) error {
376 376
 	for port := range ports {
377 377
 		if !port.IsValid() {
378 378
 			return errors.Errorf("invalid port specification: %q", port.String())
... ...
@@ -384,7 +384,7 @@ func validatePortBindings(ports containertypes.PortMap) error {
384 384
 				continue
385 385
 			}
386 386
 
387
-			if _, err := containertypes.ParsePortRange(pb.HostPort); err != nil {
387
+			if _, err := networktypes.ParsePortRange(pb.HostPort); err != nil {
388 388
 				return errors.Errorf("invalid port specification: %q", pb.HostPort)
389 389
 			}
390 390
 		}
... ...
@@ -22,6 +22,7 @@ import (
22 22
 	containertypes "github.com/moby/moby/api/types/container"
23 23
 	"github.com/moby/moby/api/types/events"
24 24
 	mounttypes "github.com/moby/moby/api/types/mount"
25
+	networktypes "github.com/moby/moby/api/types/network"
25 26
 	swarmtypes "github.com/moby/moby/api/types/swarm"
26 27
 	"github.com/moby/moby/v2/daemon/internal/image"
27 28
 	libcontainerdtypes "github.com/moby/moby/v2/daemon/internal/libcontainerd/types"
... ...
@@ -651,7 +652,7 @@ func (container *Container) BackfillEmptyPBs() {
651 651
 		if len(pb) > 0 || pb == nil {
652 652
 			continue
653 653
 		}
654
-		container.HostConfig.PortBindings[portProto] = []containertypes.PortBinding{
654
+		container.HostConfig.PortBindings[portProto] = []networktypes.PortBinding{
655 655
 			{}, // Backfill an empty PortBinding
656 656
 		}
657 657
 	}
... ...
@@ -39,8 +39,8 @@ type Snapshot struct {
39 39
 	Running      bool
40 40
 	Paused       bool
41 41
 	Managed      bool
42
-	ExposedPorts container.PortSet
43
-	PortBindings container.PortSet
42
+	ExposedPorts network.PortSet
43
+	PortBindings network.PortSet
44 44
 	Health       container.HealthStatus
45 45
 	HostConfig   struct {
46 46
 		Isolation string
... ...
@@ -318,8 +318,8 @@ func (v *View) transform(ctr *Container) *Snapshot {
318 318
 		Name:         ctr.Name,
319 319
 		Pid:          ctr.State.Pid,
320 320
 		Managed:      ctr.Managed,
321
-		ExposedPorts: make(container.PortSet),
322
-		PortBindings: make(container.PortSet),
321
+		ExposedPorts: make(network.PortSet),
322
+		PortBindings: make(network.PortSet),
323 323
 		Health:       health,
324 324
 		Running:      ctr.State.Running,
325 325
 		Paused:       ctr.State.Paused,
... ...
@@ -398,8 +398,8 @@ func (v *View) transform(ctr *Container) *Snapshot {
398 398
 				continue
399 399
 			}
400 400
 			for _, binding := range bindings {
401
-				// TODO(thaJeztah): if this is always a port/proto (no range), we can simplify this to [container.ParsePort].
402
-				h, err := container.ParsePortRange(binding.HostPort)
401
+				// TODO(thaJeztah): if this is always a port/proto (no range), we can simplify this to [network.ParsePort].
402
+				h, err := network.ParsePortRange(binding.HostPort)
403 403
 				if err != nil {
404 404
 					log.G(context.TODO()).WithError(err).Warn("invalid host port map")
405 405
 					continue
... ...
@@ -94,7 +94,7 @@ func buildSandboxOptions(cfg *config.Config, ctr *container.Container) ([]libnet
94 94
 		}
95 95
 	}
96 96
 
97
-	portBindings := make(containertypes.PortMap, len(ctr.HostConfig.PortBindings))
97
+	portBindings := make(networktypes.PortMap, len(ctr.HostConfig.PortBindings))
98 98
 	for p, b := range ctr.HostConfig.PortBindings {
99 99
 		portBindings[p] = slices.Clone(b)
100 100
 	}
... ...
@@ -119,13 +119,13 @@ func buildSandboxOptions(cfg *config.Config, ctr *container.Container) ([]libnet
119 119
 
120 120
 		for _, binding := range bindings {
121 121
 			var (
122
-				portRange containertypes.PortRange
122
+				portRange networktypes.PortRange
123 123
 				err       error
124 124
 			)
125 125
 
126 126
 			// Empty HostPort means to map to an ephemeral port.
127 127
 			if binding.HostPort != "" {
128
-				portRange, err = containertypes.ParsePortRange(binding.HostPort)
128
+				portRange, err = networktypes.ParsePortRange(binding.HostPort)
129 129
 				if err != nil {
130 130
 					return nil, fmt.Errorf("error parsing HostPort value(%s):%v", binding.HostPort, err)
131 131
 				}
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"testing"
7 7
 
8 8
 	containertypes "github.com/moby/moby/api/types/container"
9
+	"github.com/moby/moby/api/types/network"
9 10
 	"github.com/moby/moby/v2/daemon/config"
10 11
 	"gotest.tools/v3/assert"
11 12
 )
... ...
@@ -14,12 +15,12 @@ import (
14 14
 // This should not be tested on Windows because Windows doesn't support "host" network mode.
15 15
 func TestContainerWarningHostAndPublishPorts(t *testing.T) {
16 16
 	testCases := []struct {
17
-		ports    containertypes.PortMap
17
+		ports    network.PortMap
18 18
 		warnings []string
19 19
 	}{
20
-		{ports: containertypes.PortMap{}},
21
-		{ports: containertypes.PortMap{
22
-			containertypes.MustParsePort("8080"): []containertypes.PortBinding{{HostPort: "8989"}},
20
+		{ports: network.PortMap{}},
21
+		{ports: network.PortMap{
22
+			network.MustParsePort("8080"): []network.PortBinding{{HostPort: "8989"}},
23 23
 		}, warnings: []string{"Published ports are discarded when using host network mode"}},
24 24
 	}
25 25
 	muteLogs(t)
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"testing"
5 5
 
6 6
 	"github.com/moby/moby/api/types/container"
7
+	"github.com/moby/moby/api/types/network"
7 8
 	"gotest.tools/v3/assert"
8 9
 	is "gotest.tools/v3/assert/cmp"
9 10
 )
... ...
@@ -11,8 +12,8 @@ import (
11 11
 // regression test for https://github.com/moby/moby/issues/45904
12 12
 func TestContainerConfigToDockerImageConfig(t *testing.T) {
13 13
 	ociCFG := containerConfigToDockerOCIImageConfig(&container.Config{
14
-		ExposedPorts: container.PortSet{
15
-			container.MustParsePort("80/tcp"): struct{}{},
14
+		ExposedPorts: network.PortSet{
15
+			network.MustParsePort("80/tcp"): struct{}{},
16 16
 		},
17 17
 	})
18 18
 
... ...
@@ -5,6 +5,7 @@ import (
5 5
 
6 6
 	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
7 7
 	"github.com/moby/moby/api/types/container"
8
+	"github.com/moby/moby/api/types/network"
8 9
 	"github.com/moby/moby/v2/daemon/internal/image"
9 10
 	"github.com/moby/moby/v2/dockerversion"
10 11
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -95,9 +96,9 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock
95 95
 }
96 96
 
97 97
 func dockerOCIImageConfigToContainerConfig(cfg imagespec.DockerOCIImageConfig) *container.Config {
98
-	exposedPorts := make(container.PortSet, len(cfg.ExposedPorts))
98
+	exposedPorts := make(network.PortSet, len(cfg.ExposedPorts))
99 99
 	for k := range cfg.ExposedPorts {
100
-		if p, err := container.ParsePort(k); err == nil {
100
+		if p, err := network.ParsePort(k); err == nil {
101 101
 			exposedPorts[p] = struct{}{}
102 102
 		}
103 103
 	}
... ...
@@ -11,6 +11,7 @@ import (
11 11
 
12 12
 	cerrdefs "github.com/containerd/errdefs"
13 13
 	containertypes "github.com/moby/moby/api/types/container"
14
+	"github.com/moby/moby/api/types/network"
14 15
 	"github.com/moby/moby/v2/daemon/container"
15 16
 	"github.com/moby/moby/v2/daemon/internal/idtools"
16 17
 	"github.com/moby/moby/v2/daemon/libnetwork"
... ...
@@ -220,9 +221,9 @@ func TestContainerInitDNS(t *testing.T) {
220 220
 
221 221
 func TestMerge(t *testing.T) {
222 222
 	configImage := &containertypes.Config{
223
-		ExposedPorts: containertypes.PortSet{
224
-			containertypes.MustParsePort("1111/tcp"): struct{}{},
225
-			containertypes.MustParsePort("2222/tcp"): struct{}{},
223
+		ExposedPorts: network.PortSet{
224
+			network.MustParsePort("1111/tcp"): struct{}{},
225
+			network.MustParsePort("2222/tcp"): struct{}{},
226 226
 		},
227 227
 		Env: []string{"VAR1=1", "VAR2=2"},
228 228
 		Volumes: map[string]struct{}{
... ...
@@ -232,9 +233,9 @@ func TestMerge(t *testing.T) {
232 232
 	}
233 233
 
234 234
 	configUser := &containertypes.Config{
235
-		ExposedPorts: containertypes.PortSet{
236
-			containertypes.MustParsePort("2222/tcp"): struct{}{},
237
-			containertypes.MustParsePort("3333/tcp"): struct{}{},
235
+		ExposedPorts: network.PortSet{
236
+			network.MustParsePort("2222/tcp"): struct{}{},
237
+			network.MustParsePort("3333/tcp"): struct{}{},
238 238
 		},
239 239
 		Env: []string{"VAR2=3", "VAR3=3"},
240 240
 		Volumes: map[string]struct{}{
... ...
@@ -273,8 +274,8 @@ func TestMerge(t *testing.T) {
273 273
 	}
274 274
 
275 275
 	configImage2 := &containertypes.Config{
276
-		ExposedPorts: map[containertypes.Port]struct{}{
277
-			containertypes.MustParsePort("0/tcp"): {},
276
+		ExposedPorts: map[network.Port]struct{}{
277
+			network.MustParsePort("0/tcp"): {},
278 278
 		},
279 279
 	}
280 280
 
... ...
@@ -34,7 +34,7 @@ func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options
34 34
 	}
35 35
 
36 36
 	// TODO(thaJeztah): do we need a deep copy here? Otherwise we could use maps.Clone (see https://github.com/moby/moby/commit/7917a36cc787ada58987320e67cc6d96858f3b55)
37
-	ports := make(containertypes.PortMap, len(ctr.NetworkSettings.Ports))
37
+	ports := make(networktypes.PortMap, len(ctr.NetworkSettings.Ports))
38 38
 	for k, pm := range ctr.NetworkSettings.Ports {
39 39
 		ports[k] = pm
40 40
 	}
... ...
@@ -5,24 +5,25 @@ import (
5 5
 	"testing"
6 6
 
7 7
 	"github.com/moby/moby/api/types/container"
8
+	"github.com/moby/moby/api/types/network"
8 9
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
9 10
 	"gotest.tools/v3/assert"
10 11
 	is "gotest.tools/v3/assert/cmp"
11 12
 )
12 13
 
13 14
 func TestCompare(t *testing.T) {
14
-	ports1 := container.PortSet{
15
-		container.MustParsePort("1111/tcp"): struct{}{},
16
-		container.MustParsePort("2222/tcp"): struct{}{},
15
+	ports1 := network.PortSet{
16
+		network.MustParsePort("1111/tcp"): struct{}{},
17
+		network.MustParsePort("2222/tcp"): struct{}{},
17 18
 	}
18
-	ports2 := container.PortSet{
19
-		container.MustParsePort("3333/tcp"): struct{}{},
20
-		container.MustParsePort("4444/tcp"): struct{}{},
19
+	ports2 := network.PortSet{
20
+		network.MustParsePort("3333/tcp"): struct{}{},
21
+		network.MustParsePort("4444/tcp"): struct{}{},
21 22
 	}
22
-	ports3 := container.PortSet{
23
-		container.MustParsePort("1111/tcp"): struct{}{},
24
-		container.MustParsePort("2222/tcp"): struct{}{},
25
-		container.MustParsePort("5555/tcp"): struct{}{},
23
+	ports3 := network.PortSet{
24
+		network.MustParsePort("1111/tcp"): struct{}{},
25
+		network.MustParsePort("2222/tcp"): struct{}{},
26
+		network.MustParsePort("5555/tcp"): struct{}{},
26 27
 	}
27 28
 	volumes1 := map[string]struct{}{
28 29
 		"/test1": {},
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"slices"
9 9
 	"strings"
10 10
 
11
-	"github.com/moby/moby/api/types/container"
11
+	"github.com/moby/moby/api/types/network"
12 12
 )
13 13
 
14 14
 // Link struct holds information about parent/child linked container
... ...
@@ -22,17 +22,17 @@ type Link struct {
22 22
 	// Child environments variables
23 23
 	ChildEnvironment []string
24 24
 	// Child exposed ports
25
-	Ports []container.Port
25
+	Ports []network.Port
26 26
 }
27 27
 
28 28
 // EnvVars generates environment variables for the linked container
29 29
 // for the Link with the given options.
30
-func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[container.Port]struct{}) []string {
30
+func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[network.Port]struct{}) []string {
31 31
 	return NewLink(parentIP, childIP, name, env, exposedPorts).ToEnv()
32 32
 }
33 33
 
34 34
 // NewLink initializes a new Link struct with the provided options.
35
-func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[container.Port]struct{}) *Link {
35
+func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[network.Port]struct{}) *Link {
36 36
 	ports := slices.Collect(maps.Keys(exposedPorts))
37 37
 
38 38
 	return &Link{
... ...
@@ -55,7 +55,7 @@ func (l *Link) ToEnv() []string {
55 55
 	slices.SortFunc(l.Ports, withTCPPriority)
56 56
 
57 57
 	env := make([]string, 0, 1+len(l.Ports)*4)
58
-	var pStart, pEnd container.Port
58
+	var pStart, pEnd network.Port
59 59
 
60 60
 	for i, p := range l.Ports {
61 61
 		if i == 0 {
... ...
@@ -111,14 +111,14 @@ func (l *Link) ToEnv() []string {
111 111
 
112 112
 // withTCPPriority prioritizes ports using TCP over other protocols before
113 113
 // comparing port-number and protocol.
114
-func withTCPPriority(ip, jp container.Port) int {
114
+func withTCPPriority(ip, jp network.Port) int {
115 115
 	if ip.Proto() == jp.Proto() {
116 116
 		return cmp.Compare(ip.Num(), jp.Num())
117 117
 	}
118
-	if ip.Proto() == container.TCP {
118
+	if ip.Proto() == network.TCP {
119 119
 		return -1
120 120
 	}
121
-	if jp.Proto() == container.TCP {
121
+	if jp.Proto() == network.TCP {
122 122
 		return 1
123 123
 	}
124 124
 	return cmp.Compare(ip.Proto(), jp.Proto())
... ...
@@ -6,13 +6,13 @@ import (
6 6
 	"testing"
7 7
 
8 8
 	"github.com/google/go-cmp/cmp/cmpopts"
9
-	"github.com/moby/moby/api/types/container"
9
+	"github.com/moby/moby/api/types/network"
10 10
 	"gotest.tools/v3/assert"
11 11
 )
12 12
 
13 13
 func TestLinkNaming(t *testing.T) {
14
-	actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, container.PortSet{
15
-		container.MustParsePort("6379/tcp"): struct{}{},
14
+	actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, network.PortSet{
15
+		network.MustParsePort("6379/tcp"): struct{}{},
16 16
 	})
17 17
 
18 18
 	expectedEnv := []string{
... ...
@@ -29,8 +29,8 @@ func TestLinkNaming(t *testing.T) {
29 29
 }
30 30
 
31 31
 func TestLinkNew(t *testing.T) {
32
-	tcp6379 := container.MustParsePort("6379/tcp")
33
-	link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", nil, container.PortSet{
32
+	tcp6379 := network.MustParsePort("6379/tcp")
33
+	link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", nil, network.PortSet{
34 34
 		tcp6379: struct{}{},
35 35
 	})
36 36
 
... ...
@@ -38,15 +38,15 @@ func TestLinkNew(t *testing.T) {
38 38
 		Name:     "/db/docker",
39 39
 		ParentIP: "172.0.17.3",
40 40
 		ChildIP:  "172.0.17.2",
41
-		Ports:    []container.Port{tcp6379},
41
+		Ports:    []network.Port{tcp6379},
42 42
 	}
43 43
 
44
-	assert.DeepEqual(t, expected, link, cmpopts.EquateComparable(container.Port{}))
44
+	assert.DeepEqual(t, expected, link, cmpopts.EquateComparable(network.Port{}))
45 45
 }
46 46
 
47 47
 func TestLinkEnv(t *testing.T) {
48
-	tcp6379 := container.MustParsePort("6379/tcp")
49
-	actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{
48
+	tcp6379 := network.MustParsePort("6379/tcp")
49
+	actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, network.PortSet{
50 50
 		tcp6379: struct{}{},
51 51
 	})
52 52
 
... ...
@@ -67,39 +67,39 @@ func TestLinkEnv(t *testing.T) {
67 67
 // TestSortPorts verifies that ports are sorted with TCP taking priority,
68 68
 // and ports with the same protocol to be sorted by port.
69 69
 func TestSortPorts(t *testing.T) {
70
-	ports := []container.Port{
71
-		container.MustParsePort("6379/tcp"),
72
-		container.MustParsePort("6376/udp"),
73
-		container.MustParsePort("6380/tcp"),
74
-		container.MustParsePort("6376/sctp"),
75
-		container.MustParsePort("6381/tcp"),
76
-		container.MustParsePort("6381/udp"),
77
-		container.MustParsePort("6375/udp"),
78
-		container.MustParsePort("6375/sctp"),
70
+	ports := []network.Port{
71
+		network.MustParsePort("6379/tcp"),
72
+		network.MustParsePort("6376/udp"),
73
+		network.MustParsePort("6380/tcp"),
74
+		network.MustParsePort("6376/sctp"),
75
+		network.MustParsePort("6381/tcp"),
76
+		network.MustParsePort("6381/udp"),
77
+		network.MustParsePort("6375/udp"),
78
+		network.MustParsePort("6375/sctp"),
79 79
 	}
80 80
 
81
-	expected := []container.Port{
82
-		container.MustParsePort("6379/tcp"),
83
-		container.MustParsePort("6380/tcp"),
84
-		container.MustParsePort("6381/tcp"),
85
-		container.MustParsePort("6375/sctp"),
86
-		container.MustParsePort("6376/sctp"),
87
-		container.MustParsePort("6375/udp"),
88
-		container.MustParsePort("6376/udp"),
89
-		container.MustParsePort("6381/udp"),
81
+	expected := []network.Port{
82
+		network.MustParsePort("6379/tcp"),
83
+		network.MustParsePort("6380/tcp"),
84
+		network.MustParsePort("6381/tcp"),
85
+		network.MustParsePort("6375/sctp"),
86
+		network.MustParsePort("6376/sctp"),
87
+		network.MustParsePort("6375/udp"),
88
+		network.MustParsePort("6376/udp"),
89
+		network.MustParsePort("6381/udp"),
90 90
 	}
91 91
 
92 92
 	slices.SortFunc(ports, withTCPPriority)
93
-	assert.DeepEqual(t, expected, ports, cmpopts.EquateComparable(container.Port{}))
93
+	assert.DeepEqual(t, expected, ports, cmpopts.EquateComparable(network.Port{}))
94 94
 }
95 95
 
96 96
 func TestLinkMultipleEnv(t *testing.T) {
97
-	actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{
98
-		container.MustParsePort("6300/udp"): struct{}{},
99
-		container.MustParsePort("6379/tcp"): struct{}{},
100
-		container.MustParsePort("6380/tcp"): struct{}{},
101
-		container.MustParsePort("6381/tcp"): struct{}{},
102
-		container.MustParsePort("6382/udp"): struct{}{},
97
+	actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, network.PortSet{
98
+		network.MustParsePort("6300/udp"): struct{}{},
99
+		network.MustParsePort("6379/tcp"): struct{}{},
100
+		network.MustParsePort("6380/tcp"): struct{}{},
101
+		network.MustParsePort("6381/tcp"): struct{}{},
102
+		network.MustParsePort("6382/udp"): struct{}{},
103 103
 	})
104 104
 
105 105
 	expectedEnv := []string{
... ...
@@ -145,12 +145,12 @@ func BenchmarkLinkMultipleEnv(b *testing.B) {
145 145
 	b.ReportAllocs()
146 146
 	b.ResetTimer()
147 147
 	for i := 0; i < b.N; i++ {
148
-		_ = EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{
149
-			container.MustParsePort("6300/udp"): struct{}{},
150
-			container.MustParsePort("6379/tcp"): struct{}{},
151
-			container.MustParsePort("6380/tcp"): struct{}{},
152
-			container.MustParsePort("6381/tcp"): struct{}{},
153
-			container.MustParsePort("6382/udp"): struct{}{},
148
+		_ = EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, network.PortSet{
149
+			network.MustParsePort("6300/udp"): struct{}{},
150
+			network.MustParsePort("6379/tcp"): struct{}{},
151
+			network.MustParsePort("6380/tcp"): struct{}{},
152
+			network.MustParsePort("6381/tcp"): struct{}{},
153
+			network.MustParsePort("6382/udp"): struct{}{},
154 154
 		})
155 155
 	}
156 156
 }
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"github.com/containerd/log"
14 14
 	containertypes "github.com/moby/moby/api/types/container"
15 15
 	"github.com/moby/moby/api/types/filters"
16
+	"github.com/moby/moby/api/types/network"
16 17
 	"github.com/moby/moby/v2/daemon/container"
17 18
 	"github.com/moby/moby/v2/daemon/internal/image"
18 19
 	"github.com/moby/moby/v2/daemon/server/backend"
... ...
@@ -403,7 +404,7 @@ func portOp(key string, filter map[string]bool) func(value string) error {
403 403
 			return fmt.Errorf("filter for '%s' should not contain ':': %s", key, value)
404 404
 		}
405 405
 		// support two formats, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
406
-		portRange, err := containertypes.ParsePortRange(value)
406
+		portRange, err := network.ParsePortRange(value)
407 407
 		if err != nil {
408 408
 			return fmt.Errorf("error while looking up for %s %s: %s", key, value, err)
409 409
 		}
... ...
@@ -1027,13 +1027,13 @@ func buildPortsRelatedCreateEndpointOptions(c *container.Container, n *libnetwor
1027 1027
 
1028 1028
 		for _, binding := range bindings {
1029 1029
 			var (
1030
-				portRange containertypes.PortRange
1030
+				portRange networktypes.PortRange
1031 1031
 				err       error
1032 1032
 			)
1033 1033
 
1034 1034
 			// Empty HostPort means to map to an ephemeral port.
1035 1035
 			if binding.HostPort != "" {
1036
-				portRange, err = containertypes.ParsePortRange(binding.HostPort)
1036
+				portRange, err = networktypes.ParsePortRange(binding.HostPort)
1037 1037
 				if err != nil {
1038 1038
 					return nil, fmt.Errorf("error parsing HostPort value(%s):%v", binding.HostPort, err)
1039 1039
 				}
... ...
@@ -1063,8 +1063,8 @@ func buildPortsRelatedCreateEndpointOptions(c *container.Container, n *libnetwor
1063 1063
 }
1064 1064
 
1065 1065
 // getPortMapInfo retrieves the current port-mapping programmed for the given sandbox
1066
-func getPortMapInfo(sb *libnetwork.Sandbox) containertypes.PortMap {
1067
-	pm := containertypes.PortMap{}
1066
+func getPortMapInfo(sb *libnetwork.Sandbox) networktypes.PortMap {
1067
+	pm := networktypes.PortMap{}
1068 1068
 	if sb == nil {
1069 1069
 		return pm
1070 1070
 	}
... ...
@@ -1075,7 +1075,7 @@ func getPortMapInfo(sb *libnetwork.Sandbox) containertypes.PortMap {
1075 1075
 	return pm
1076 1076
 }
1077 1077
 
1078
-func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint) {
1078
+func getEndpointPortMapInfo(pm networktypes.PortMap, ep *libnetwork.Endpoint) {
1079 1079
 	driverInfo, _ := ep.DriverInfo()
1080 1080
 	if driverInfo == nil {
1081 1081
 		// It is not an error for epInfo to be nil
... ...
@@ -1085,7 +1085,7 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint)
1085 1085
 	if expData, ok := driverInfo[netlabel.ExposedPorts]; ok {
1086 1086
 		if exposedPorts, ok := expData.([]lntypes.TransportPort); ok {
1087 1087
 			for _, tp := range exposedPorts {
1088
-				natPort, ok := containertypes.PortFrom(tp.Port, containertypes.NetworkProtocol(tp.Proto.String()))
1088
+				natPort, ok := networktypes.PortFrom(tp.Port, networktypes.IPProtocol(tp.Proto.String()))
1089 1089
 				if !ok {
1090 1090
 					log.G(context.TODO()).Errorf("Invalid exposed port: %s", tp.String())
1091 1091
 					continue
... ...
@@ -1105,7 +1105,7 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint)
1105 1105
 	if portMapping, ok := mapData.([]lntypes.PortBinding); ok {
1106 1106
 		for _, pp := range portMapping {
1107 1107
 			// Use an empty string for the host natPort if there's no natPort assigned.
1108
-			natPort, ok := containertypes.PortFrom(pp.Port, containertypes.NetworkProtocol(pp.Proto.String()))
1108
+			natPort, ok := networktypes.PortFrom(pp.Port, networktypes.IPProtocol(pp.Proto.String()))
1109 1109
 			if !ok {
1110 1110
 				log.G(context.TODO()).Errorf("Invalid port binding: %s", pp.String())
1111 1111
 				continue
... ...
@@ -1115,7 +1115,7 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint)
1115 1115
 			if pp.HostPort > 0 {
1116 1116
 				hp = strconv.Itoa(int(pp.HostPort))
1117 1117
 			}
1118
-			natBndg := containertypes.PortBinding{HostPort: hp}
1118
+			natBndg := networktypes.PortBinding{HostPort: hp}
1119 1119
 			natBndg.HostIP, _ = netip.AddrFromSlice(pp.HostIP)
1120 1120
 			pm[natPort] = append(pm[natPort], natBndg)
1121 1121
 		}
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"net"
5 5
 	"sync"
6 6
 
7
-	"github.com/moby/moby/api/types/container"
8 7
 	networktypes "github.com/moby/moby/api/types/network"
9 8
 	clustertypes "github.com/moby/moby/v2/daemon/cluster/provider"
10 9
 	"github.com/pkg/errors"
... ...
@@ -17,7 +16,7 @@ type Settings struct {
17 17
 	SandboxKey       string
18 18
 	Networks         map[string]*EndpointSettings
19 19
 	Service          *clustertypes.ServiceConfig
20
-	Ports            container.PortMap
20
+	Ports            networktypes.PortMap
21 21
 	HasSwarmEndpoint bool
22 22
 }
23 23
 
... ...
@@ -901,7 +901,7 @@ func handlePortBindingsBC(hostConfig *container.HostConfig, version string) stri
901 901
 			emptyPBs = append(emptyPBs, port.String())
902 902
 		}
903 903
 
904
-		hostConfig.PortBindings[port] = []container.PortBinding{{}}
904
+		hostConfig.PortBindings[port] = []network.PortBinding{{}}
905 905
 	}
906 906
 
907 907
 	if len(emptyPBs) > 0 {
... ...
@@ -504,8 +504,8 @@ func (s *DockerAPISuite) TestContainerAPIBadPort(c *testing.T) {
504 504
 	}
505 505
 
506 506
 	hostConfig := container.HostConfig{
507
-		PortBindings: container.PortMap{
508
-			container.MustParsePort("8080/tcp"): []container.PortBinding{
507
+		PortBindings: network.PortMap{
508
+			network.MustParsePort("8080/tcp"): []network.PortBinding{
509 509
 				{
510 510
 					HostPort: "aa80",
511 511
 				},
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"strings"
10 10
 	"testing"
11 11
 
12
-	containertypes "github.com/moby/moby/api/types/container"
12
+	"github.com/moby/moby/api/types/network"
13 13
 	"github.com/moby/moby/v2/integration-cli/cli"
14 14
 	"github.com/moby/moby/v2/integration-cli/cli/build"
15 15
 	"github.com/moby/moby/v2/internal/testutil/fakecontext"
... ...
@@ -92,7 +92,7 @@ func (s *DockerCLICreateSuite) TestCreateWithPortRange(c *testing.T) {
92 92
 
93 93
 	var containers []struct {
94 94
 		HostConfig *struct {
95
-			PortBindings map[containertypes.Port][]containertypes.PortBinding
95
+			PortBindings network.PortMap
96 96
 		}
97 97
 	}
98 98
 	err := json.Unmarshal([]byte(out), &containers)
... ...
@@ -118,7 +118,7 @@ func (s *DockerCLICreateSuite) TestCreateWithLargePortRange(c *testing.T) {
118 118
 
119 119
 	var containers []struct {
120 120
 		HostConfig *struct {
121
-			PortBindings map[containertypes.Port][]containertypes.PortBinding
121
+			PortBindings network.PortMap
122 122
 		}
123 123
 	}
124 124
 
... ...
@@ -22,7 +22,7 @@ import (
22 22
 	"testing"
23 23
 	"time"
24 24
 
25
-	"github.com/moby/moby/api/types/container"
25
+	"github.com/moby/moby/api/types/network"
26 26
 	"github.com/moby/moby/client"
27 27
 	"github.com/moby/moby/client/pkg/stringid"
28 28
 	"github.com/moby/moby/v2/integration-cli/cli"
... ...
@@ -2168,7 +2168,7 @@ func (s *DockerCLIRunSuite) TestRunAllowPortRangeThroughExpose(c *testing.T) {
2168 2168
 	id = strings.TrimSpace(id)
2169 2169
 
2170 2170
 	portstr := inspectFieldJSON(c, id, "NetworkSettings.Ports")
2171
-	var ports container.PortMap
2171
+	var ports network.PortMap
2172 2172
 	if err := json.Unmarshal([]byte(portstr), &ports); err != nil {
2173 2173
 		c.Fatal(err)
2174 2174
 	}
... ...
@@ -2505,7 +2505,7 @@ func (s *DockerCLIRunSuite) TestRunAllowPortRangeThroughPublish(c *testing.T) {
2505 2505
 	id = strings.TrimSpace(id)
2506 2506
 	portStr := inspectFieldJSON(c, id, "NetworkSettings.Ports")
2507 2507
 
2508
-	var ports container.PortMap
2508
+	var ports network.PortMap
2509 2509
 	err := json.Unmarshal([]byte(portStr), &ports)
2510 2510
 	assert.NilError(c, err, "failed to unmarshal: %v", portStr)
2511 2511
 	for port, binding := range ports {
... ...
@@ -3,7 +3,7 @@ package container
3 3
 import (
4 4
 	"testing"
5 5
 
6
-	containertypes "github.com/moby/moby/api/types/container"
6
+	"github.com/moby/moby/api/types/network"
7 7
 	"github.com/moby/moby/client"
8 8
 	"github.com/moby/moby/v2/integration/internal/container"
9 9
 	"github.com/moby/moby/v2/internal/testutil"
... ...
@@ -71,13 +71,13 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) {
71 71
 	defer d.Stop(t)
72 72
 
73 73
 	apiClient := d.NewClientT(t)
74
-	mappedPort := containertypes.MustParsePort("80/tcp")
74
+	mappedPort := network.MustParsePort("80/tcp")
75 75
 
76 76
 	// The intention of this container is to ignore stop signals.
77 77
 	// Sadly this means the test will take longer, but at least this test can be parallelized.
78 78
 	cid := container.Run(ctx, t, apiClient,
79 79
 		container.WithExposedPorts("80/tcp"),
80
-		container.WithPortMap(containertypes.PortMap{mappedPort: {{}}}),
80
+		container.WithPortMap(network.PortMap{mappedPort: {{}}}),
81 81
 		container.WithCmd("/bin/sh", "-c", "while true; do echo hello; sleep 1; done"))
82 82
 	defer func() {
83 83
 		err := apiClient.ContainerRemove(ctx, cid, client.ContainerRemoveOptions{Force: true})
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"strings"
11 11
 	"testing"
12 12
 
13
-	containertypes "github.com/moby/moby/api/types/container"
13
+	"github.com/moby/moby/api/types/network"
14 14
 	"github.com/moby/moby/client"
15 15
 	"github.com/moby/moby/v2/integration/internal/container"
16 16
 	"gotest.tools/v3/assert"
... ...
@@ -122,8 +122,8 @@ func startServerContainer(ctx context.Context, t *testing.T, msg string, port ui
122 122
 		container.WithCmd("sh", "-c", fmt.Sprintf("echo %q | nc -lp %d", msg, port)),
123 123
 		container.WithExposedPorts(fmt.Sprintf("%d/tcp", port)),
124 124
 		func(c *container.TestContainerConfig) {
125
-			c.HostConfig.PortBindings = containertypes.PortMap{
126
-				containertypes.MustParsePort(fmt.Sprintf("%d/tcp", port)): []containertypes.PortBinding{
125
+			c.HostConfig.PortBindings = network.PortMap{
126
+				network.MustParsePort(fmt.Sprintf("%d/tcp", port)): []network.PortBinding{
127 127
 					{
128 128
 						HostPort: fmt.Sprintf("%d", port),
129 129
 					},
... ...
@@ -74,18 +74,18 @@ func WithSysctls(sysctls map[string]string) func(*TestContainerConfig) {
74 74
 // WithExposedPorts sets the exposed ports of the container
75 75
 func WithExposedPorts(ports ...string) func(*TestContainerConfig) {
76 76
 	return func(c *TestContainerConfig) {
77
-		c.Config.ExposedPorts = map[container.Port]struct{}{}
77
+		c.Config.ExposedPorts = map[network.Port]struct{}{}
78 78
 		for _, port := range ports {
79
-			p, _ := container.ParsePort(port)
79
+			p, _ := network.ParsePort(port)
80 80
 			c.Config.ExposedPorts[p] = struct{}{}
81 81
 		}
82 82
 	}
83 83
 }
84 84
 
85 85
 // WithPortMap sets/replaces port mappings.
86
-func WithPortMap(pm container.PortMap) func(*TestContainerConfig) {
86
+func WithPortMap(pm network.PortMap) func(*TestContainerConfig) {
87 87
 	return func(c *TestContainerConfig) {
88
-		c.HostConfig.PortBindings = container.PortMap{}
88
+		c.HostConfig.PortBindings = network.PortMap{}
89 89
 		for p, b := range pm {
90 90
 			c.HostConfig.PortBindings[p] = slices.Clone(b)
91 91
 		}
... ...
@@ -516,19 +516,19 @@ func TestEndpointWithCustomIfname(t *testing.T) {
516 516
 func TestPublishedPortAlreadyInUse(t *testing.T) {
517 517
 	ctx := setupTest(t)
518 518
 	apiClient := testEnv.APIClient()
519
-	mappedPort := containertypes.MustParsePort("80/tcp")
519
+	mappedPort := networktypes.MustParsePort("80/tcp")
520 520
 
521 521
 	ctr1 := ctr.Run(ctx, t, apiClient,
522 522
 		ctr.WithCmd("top"),
523 523
 		ctr.WithExposedPorts("80/tcp"),
524
-		ctr.WithPortMap(containertypes.PortMap{mappedPort: {{HostPort: "8000"}}}))
524
+		ctr.WithPortMap(networktypes.PortMap{mappedPort: {{HostPort: "8000"}}}))
525 525
 	defer ctr.Remove(ctx, t, apiClient, ctr1, client.ContainerRemoveOptions{Force: true})
526 526
 
527 527
 	ctr2 := ctr.Create(ctx, t, apiClient,
528 528
 		ctr.WithCmd("top"),
529 529
 		ctr.WithRestartPolicy(containertypes.RestartPolicyAlways),
530 530
 		ctr.WithExposedPorts("80/tcp"),
531
-		ctr.WithPortMap(containertypes.PortMap{mappedPort: {{HostPort: "8000"}}}))
531
+		ctr.WithPortMap(networktypes.PortMap{mappedPort: {{HostPort: "8000"}}}))
532 532
 	defer ctr.Remove(ctx, t, apiClient, ctr2, client.ContainerRemoveOptions{Force: true})
533 533
 
534 534
 	err := apiClient.ContainerStart(ctx, ctr2, client.ContainerStartOptions{})
... ...
@@ -563,18 +563,18 @@ func TestAllPortMappingsAreReturned(t *testing.T) {
563 563
 
564 564
 	ctrID := ctr.Run(ctx, t, apiClient,
565 565
 		ctr.WithExposedPorts("80/tcp", "81/tcp"),
566
-		ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}),
566
+		ctr.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}),
567 567
 		ctr.WithEndpointSettings("testnetv4", &networktypes.EndpointSettings{}),
568 568
 		ctr.WithEndpointSettings("testnetv6", &networktypes.EndpointSettings{}))
569 569
 	defer ctr.Remove(ctx, t, apiClient, ctrID, client.ContainerRemoveOptions{Force: true})
570 570
 
571 571
 	inspect := ctr.Inspect(ctx, t, apiClient, ctrID)
572
-	assert.DeepEqual(t, inspect.NetworkSettings.Ports, containertypes.PortMap{
573
-		containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
572
+	assert.DeepEqual(t, inspect.NetworkSettings.Ports, networktypes.PortMap{
573
+		networktypes.MustParsePort("80/tcp"): []networktypes.PortBinding{
574 574
 			{HostIP: netip.IPv4Unspecified(), HostPort: "8000"},
575 575
 			{HostIP: netip.IPv6Unspecified(), HostPort: "8000"},
576 576
 		},
577
-		containertypes.MustParsePort("81/tcp"): nil,
577
+		networktypes.MustParsePort("81/tcp"): nil,
578 578
 	}, cmpopts.EquateComparable(netip.Addr{}))
579 579
 }
580 580
 
... ...
@@ -602,7 +602,7 @@ func TestFirewalldReloadNoZombies(t *testing.T) {
602 602
 
603 603
 	cid := ctr.Run(ctx, t, c,
604 604
 		ctr.WithExposedPorts("80/tcp", "81/tcp"),
605
-		ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}))
605
+		ctr.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}))
606 606
 	defer func() {
607 607
 		if !removed {
608 608
 			ctr.Remove(ctx, t, c, cid, client.ContainerRemoveOptions{Force: true})
... ...
@@ -792,7 +792,7 @@ func TestPortMappingRestore(t *testing.T) {
792 792
 	const svrName = "svr"
793 793
 	cid := ctr.Run(ctx, t, c,
794 794
 		ctr.WithExposedPorts("80/tcp"),
795
-		ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}),
795
+		ctr.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {}}),
796 796
 		ctr.WithName(svrName),
797 797
 		ctr.WithRestartPolicy(containertypes.RestartPolicyUnlessStopped),
798 798
 		ctr.WithCmd("httpd", "-f"),
... ...
@@ -803,9 +803,9 @@ func TestPortMappingRestore(t *testing.T) {
803 803
 		t.Helper()
804 804
 		insp := ctr.Inspect(ctx, t, c, cid)
805 805
 		assert.Check(t, is.Equal(insp.State.Running, true))
806
-		if assert.Check(t, is.Contains(insp.NetworkSettings.Ports, containertypes.MustParsePort("80/tcp"))) &&
807
-			assert.Check(t, is.Len(insp.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")], 2)) {
808
-			hostPort := insp.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
806
+		if assert.Check(t, is.Contains(insp.NetworkSettings.Ports, networktypes.MustParsePort("80/tcp"))) &&
807
+			assert.Check(t, is.Len(insp.NetworkSettings.Ports[networktypes.MustParsePort("80/tcp")], 2)) {
808
+			hostPort := insp.NetworkSettings.Ports[networktypes.MustParsePort("80/tcp")][0].HostPort
809 809
 			res := ctr.RunAttach(ctx, t, c,
810 810
 				ctr.WithExtraHost("thehost:host-gateway"),
811 811
 				ctr.WithCmd("wget", "-T3", "http://"+net.JoinHostPort("thehost", hostPort)),
... ...
@@ -950,7 +950,7 @@ func TestEmptyPortBindingsBC(t *testing.T) {
950 950
 	d.StartWithBusybox(ctx, t)
951 951
 	defer d.Stop(t)
952 952
 
953
-	createInspect := func(t *testing.T, version string, pbs []containertypes.PortBinding) (containertypes.PortMap, []string) {
953
+	createInspect := func(t *testing.T, version string, pbs []networktypes.PortBinding) (networktypes.PortMap, []string) {
954 954
 		apiClient := d.NewClientT(t, client.WithVersion(version))
955 955
 		defer apiClient.Close()
956 956
 
... ...
@@ -965,7 +965,7 @@ func TestEmptyPortBindingsBC(t *testing.T) {
965 965
 		// Create a container with an empty list of port bindings for container port 80/tcp.
966 966
 		config := ctr.NewTestConfig(ctr.WithCmd("top"),
967 967
 			ctr.WithExposedPorts("80/tcp"),
968
-			ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): pbs}))
968
+			ctr.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): pbs}))
969 969
 		c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
970 970
 		assert.NilError(t, err)
971 971
 		defer apiClient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true})
... ...
@@ -978,46 +978,46 @@ func TestEmptyPortBindingsBC(t *testing.T) {
978 978
 	}
979 979
 
980 980
 	t.Run("backfilling on old client version", func(t *testing.T) {
981
-		expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
981
+		expMappings := networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {
982 982
 			{}, // An empty PortBinding is backfilled
983 983
 		}}
984 984
 		expWarnings := make([]string, 0)
985 985
 
986
-		mappings, warnings := createInspect(t, "1.51", []containertypes.PortBinding{})
986
+		mappings, warnings := createInspect(t, "1.51", []networktypes.PortBinding{})
987 987
 		assert.DeepEqual(t, expMappings, mappings, cmpopts.EquateComparable(netip.Addr{}))
988 988
 		assert.DeepEqual(t, expWarnings, warnings, cmpopts.EquateComparable(netip.Addr{}))
989 989
 	})
990 990
 
991 991
 	t.Run("backfilling on API 1.52, with a warning", func(t *testing.T) {
992
-		expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
992
+		expMappings := networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {
993 993
 			{}, // An empty PortBinding is backfilled
994 994
 		}}
995 995
 		expWarnings := []string{
996 996
 			"Following container port(s) have an empty list of port-bindings: 80/tcp. Starting with API 1.53, such bindings will be discarded.",
997 997
 		}
998 998
 
999
-		mappings, warnings := createInspect(t, "1.52", []containertypes.PortBinding{})
999
+		mappings, warnings := createInspect(t, "1.52", []networktypes.PortBinding{})
1000 1000
 		assert.DeepEqual(t, expMappings, mappings, cmpopts.EquateComparable(netip.Addr{}))
1001 1001
 		assert.DeepEqual(t, expWarnings, warnings, cmpopts.EquateComparable(netip.Addr{}))
1002 1002
 	})
1003 1003
 
1004 1004
 	t.Run("no backfilling on API 1.53", func(t *testing.T) {
1005
-		expMappings := containertypes.PortMap{}
1005
+		expMappings := networktypes.PortMap{}
1006 1006
 		expWarnings := make([]string, 0)
1007 1007
 
1008
-		mappings, warnings := createInspect(t, "1.53", []containertypes.PortBinding{})
1008
+		mappings, warnings := createInspect(t, "1.53", []networktypes.PortBinding{})
1009 1009
 		assert.DeepEqual(t, expMappings, mappings, cmpopts.EquateComparable(netip.Addr{}))
1010 1010
 		assert.DeepEqual(t, expWarnings, warnings, cmpopts.EquateComparable(netip.Addr{}))
1011 1011
 	})
1012 1012
 
1013 1013
 	for _, apiVersion := range []string{"1.51", "1.52", "1.53"} {
1014 1014
 		t.Run("no backfilling on API "+apiVersion+" with non-empty bindings", func(t *testing.T) {
1015
-			expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
1015
+			expMappings := networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {
1016 1016
 				{HostPort: "8080"},
1017 1017
 			}}
1018 1018
 			expWarnings := make([]string, 0)
1019 1019
 
1020
-			mappings, warnings := createInspect(t, apiVersion, []containertypes.PortBinding{{HostPort: "8080"}})
1020
+			mappings, warnings := createInspect(t, apiVersion, []networktypes.PortBinding{{HostPort: "8080"}})
1021 1021
 			assert.DeepEqual(t, expMappings, mappings, cmpopts.EquateComparable(netip.Addr{}))
1022 1022
 			assert.DeepEqual(t, expWarnings, warnings, cmpopts.EquateComparable(netip.Addr{}))
1023 1023
 		})
... ...
@@ -1042,7 +1042,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
1042 1042
 
1043 1043
 	cid := ctr.Create(ctx, t, c,
1044 1044
 		ctr.WithExposedPorts("80/tcp"),
1045
-		ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}))
1045
+		ctr.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {}}))
1046 1046
 	defer c.ContainerRemove(ctx, cid, client.ContainerRemoveOptions{Force: true})
1047 1047
 
1048 1048
 	// Stop the daemon to safely tamper with the on-disk state.
... ...
@@ -1051,7 +1051,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
1051 1051
 	d.TamperWithContainerConfig(t, cid, func(container *container.Container) {
1052 1052
 		// Simulate a container created with an older version of the Engine
1053 1053
 		// by setting an empty list of port bindings.
1054
-		container.HostConfig.PortBindings = containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}
1054
+		container.HostConfig.PortBindings = networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {}}
1055 1055
 	})
1056 1056
 
1057 1057
 	// Restart the daemon — it should backfill the empty port binding slice.
... ...
@@ -1059,7 +1059,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
1059 1059
 
1060 1060
 	inspect := ctr.Inspect(ctx, t, c, cid)
1061 1061
 
1062
-	expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
1062
+	expMappings := networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {
1063 1063
 		{}, // An empty PortBinding is backfilled
1064 1064
 	}}
1065 1065
 	assert.DeepEqual(t, expMappings, inspect.HostConfig.PortBindings, cmpopts.EquateComparable(netip.Addr{}))
... ...
@@ -30,7 +30,7 @@ import (
30 30
 	"text/template"
31 31
 	"time"
32 32
 
33
-	containertypes "github.com/moby/moby/api/types/container"
33
+	networktypes "github.com/moby/moby/api/types/network"
34 34
 	swarmtypes "github.com/moby/moby/api/types/swarm"
35 35
 	"github.com/moby/moby/client"
36 36
 	"github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge"
... ...
@@ -53,7 +53,7 @@ var (
53 53
 
54 54
 type ctrDesc struct {
55 55
 	name         string
56
-	portMappings containertypes.PortMap
56
+	portMappings networktypes.PortMap
57 57
 }
58 58
 
59 59
 type networkDesc struct {
... ...
@@ -82,7 +82,7 @@ var index = []section{
82 82
 			containers: []ctrDesc{
83 83
 				{
84 84
 					name:         "c1",
85
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
85
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
86 86
 				},
87 87
 			},
88 88
 		}},
... ...
@@ -95,7 +95,7 @@ var index = []section{
95 95
 			containers: []ctrDesc{
96 96
 				{
97 97
 					name:         "c1",
98
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
98
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
99 99
 				},
100 100
 			},
101 101
 		}},
... ...
@@ -108,7 +108,7 @@ var index = []section{
108 108
 			containers: []ctrDesc{
109 109
 				{
110 110
 					name:         "c1",
111
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
111
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
112 112
 				},
113 113
 			},
114 114
 		}},
... ...
@@ -142,7 +142,7 @@ var index = []section{
142 142
 			containers: []ctrDesc{
143 143
 				{
144 144
 					name:         "c1",
145
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
145
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
146 146
 				},
147 147
 			},
148 148
 		}},
... ...
@@ -155,7 +155,7 @@ var index = []section{
155 155
 			containers: []ctrDesc{
156 156
 				{
157 157
 					name:         "c1",
158
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
158
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
159 159
 				},
160 160
 			},
161 161
 		}},
... ...
@@ -167,7 +167,7 @@ var index = []section{
167 167
 			containers: []ctrDesc{
168 168
 				{
169 169
 					name:         "c1",
170
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
170
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
171 171
 				},
172 172
 			},
173 173
 		}},
... ...
@@ -179,7 +179,7 @@ var index = []section{
179 179
 			containers: []ctrDesc{
180 180
 				{
181 181
 					name:         "c1",
182
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "8080"}}},
182
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "8080"}}},
183 183
 				},
184 184
 			},
185 185
 		}},
... ...
@@ -28,7 +28,7 @@ import (
28 28
 	"testing"
29 29
 	"text/template"
30 30
 
31
-	containertypes "github.com/moby/moby/api/types/container"
31
+	networktypes "github.com/moby/moby/api/types/network"
32 32
 	swarmtypes "github.com/moby/moby/api/types/swarm"
33 33
 	"github.com/moby/moby/client"
34 34
 	"github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge"
... ...
@@ -50,7 +50,7 @@ var (
50 50
 
51 51
 type ctrDesc struct {
52 52
 	name         string
53
-	portMappings containertypes.PortMap
53
+	portMappings networktypes.PortMap
54 54
 }
55 55
 
56 56
 type networkDesc struct {
... ...
@@ -79,7 +79,7 @@ var index = []section{
79 79
 			containers: []ctrDesc{
80 80
 				{
81 81
 					name:         "c1",
82
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
82
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
83 83
 				},
84 84
 			},
85 85
 		}},
... ...
@@ -92,7 +92,7 @@ var index = []section{
92 92
 			containers: []ctrDesc{
93 93
 				{
94 94
 					name:         "c1",
95
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
95
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
96 96
 				},
97 97
 			},
98 98
 		}},
... ...
@@ -105,7 +105,7 @@ var index = []section{
105 105
 			containers: []ctrDesc{
106 106
 				{
107 107
 					name:         "c1",
108
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
108
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
109 109
 				},
110 110
 			},
111 111
 		}},
... ...
@@ -139,7 +139,7 @@ var index = []section{
139 139
 			containers: []ctrDesc{
140 140
 				{
141 141
 					name:         "c1",
142
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
142
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
143 143
 				},
144 144
 			},
145 145
 		}},
... ...
@@ -152,7 +152,7 @@ var index = []section{
152 152
 			containers: []ctrDesc{
153 153
 				{
154 154
 					name:         "c1",
155
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
155
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
156 156
 				},
157 157
 			},
158 158
 		}},
... ...
@@ -178,7 +178,7 @@ var index = []section{
178 178
 			containers: []ctrDesc{
179 179
 				{
180 180
 					name:         "c1",
181
-					portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "8080"}}},
181
+					portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "8080"}}},
182 182
 				},
183 183
 			},
184 184
 		}},
... ...
@@ -17,7 +17,6 @@ import (
17 17
 	"time"
18 18
 
19 19
 	"github.com/google/go-cmp/cmp/cmpopts"
20
-	containertypes "github.com/moby/moby/api/types/container"
21 20
 	networktypes "github.com/moby/moby/api/types/network"
22 21
 	"github.com/moby/moby/client"
23 22
 	"github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge"
... ...
@@ -388,7 +387,7 @@ func TestBridgeINCRouted(t *testing.T) {
388 388
 			container.WithNetworkMode(netName),
389 389
 			container.WithName("ctr-"+gwMode),
390 390
 			container.WithExposedPorts("80/tcp"),
391
-			container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}),
391
+			container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {}}),
392 392
 		)
393 393
 		t.Cleanup(func() {
394 394
 			c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
... ...
@@ -565,7 +564,7 @@ func TestAccessToPublishedPort(t *testing.T) {
565 565
 				container.WithNetworkMode(serverNetName),
566 566
 				container.WithName("ctr-server"),
567 567
 				container.WithExposedPorts("80/tcp"),
568
-				container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {containertypes.PortBinding{HostPort: "8080"}}}),
568
+				container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {networktypes.PortBinding{HostPort: "8080"}}}),
569 569
 				container.WithCmd("httpd", "-f"),
570 570
 			)
571 571
 			defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
... ...
@@ -688,7 +687,7 @@ func TestInterNetworkDirectRouting(t *testing.T) {
688 688
 				container.WithNetworkMode(serverNetName),
689 689
 				container.WithName("ctr-pub"),
690 690
 				container.WithExposedPorts("80/tcp"),
691
-				container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {containertypes.PortBinding{HostPort: "8080"}}}),
691
+				container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {networktypes.PortBinding{HostPort: "8080"}}}),
692 692
 				container.WithCmd("httpd", "-f"),
693 693
 			)
694 694
 			defer c.ContainerRemove(ctx, ctrPubId, client.ContainerRemoveOptions{Force: true})
... ...
@@ -1063,7 +1062,7 @@ func TestDisableIPv6OnInterface(t *testing.T) {
1063 1063
 				container.WithName(ctrName),
1064 1064
 				container.WithNetworkMode(tc.netName),
1065 1065
 				container.WithExposedPorts("80/tcp"),
1066
-				container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}),
1066
+				container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}),
1067 1067
 				container.WithEndpointSettings(tc.netName, &networktypes.EndpointSettings{
1068 1068
 					DriverOpts: map[string]string{
1069 1069
 						netlabel.EndpointSysctls: "net.ipv6.conf.IFNAME.disable_ipv6=1",
... ...
@@ -1505,7 +1504,7 @@ func TestGatewaySelection(t *testing.T) {
1505 1505
 		container.WithName(ctrName),
1506 1506
 		container.WithNetworkMode(netName4),
1507 1507
 		container.WithExposedPorts("80"),
1508
-		container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostPort: "8080"}}}),
1508
+		container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80"): {{HostPort: "8080"}}}),
1509 1509
 		container.WithCmd("httpd", "-f"),
1510 1510
 	)
1511 1511
 	defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
... ...
@@ -1956,7 +1955,7 @@ func TestDropInForwardChain(t *testing.T) {
1956 1956
 		ctrId := container.Run(ctx, t, c,
1957 1957
 			container.WithNetworkMode(netName46),
1958 1958
 			container.WithExposedPorts("80"),
1959
-			container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostPort: hostPort}}}),
1959
+			container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80"): {{HostPort: hostPort}}}),
1960 1960
 			container.WithCmd("httpd", "-f"),
1961 1961
 		)
1962 1962
 		defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"testing"
8 8
 	"time"
9 9
 
10
-	containertypes "github.com/moby/moby/api/types/container"
10
+	networktypes "github.com/moby/moby/api/types/network"
11 11
 	"github.com/moby/moby/client"
12 12
 	"github.com/moby/moby/v2/integration/internal/container"
13 13
 	"github.com/moby/moby/v2/integration/internal/network"
... ...
@@ -98,13 +98,13 @@ func TestFlakyPortMappedHairpinWindows(t *testing.T) {
98 98
 	serverId := container.Run(ctx, t, c,
99 99
 		container.WithNetworkMode(serverNetName),
100 100
 		container.WithExposedPorts("80"),
101
-		container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: netip.IPv4Unspecified()}}}),
101
+		container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80"): {{HostIP: netip.IPv4Unspecified()}}}),
102 102
 		container.WithCmd("httpd", "-f"),
103 103
 	)
104 104
 	defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
105 105
 
106 106
 	inspect := container.Inspect(ctx, t, c, serverId)
107
-	hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
107
+	hostPort := inspect.NetworkSettings.Ports[networktypes.MustParsePort("80/tcp")][0].HostPort
108 108
 
109 109
 	attachCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
110 110
 	defer cancel()
... ...
@@ -17,7 +17,6 @@ import (
17 17
 
18 18
 	"github.com/google/go-cmp/cmp/cmpopts"
19 19
 	"github.com/moby/moby/api/pkg/stdcopy"
20
-	containertypes "github.com/moby/moby/api/types/container"
21 20
 	networktypes "github.com/moby/moby/api/types/network"
22 21
 	"github.com/moby/moby/client"
23 22
 	"github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge"
... ...
@@ -69,12 +68,12 @@ func TestDisableNAT(t *testing.T) {
69 69
 		name       string
70 70
 		gwMode4    string
71 71
 		gwMode6    string
72
-		expPortMap containertypes.PortMap
72
+		expPortMap networktypes.PortMap
73 73
 	}{
74 74
 		{
75 75
 			name: "defaults",
76
-			expPortMap: containertypes.PortMap{
77
-				containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
76
+			expPortMap: networktypes.PortMap{
77
+				networktypes.MustParsePort("80/tcp"): []networktypes.PortBinding{
78 78
 					{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: "8080"},
79 79
 					{HostIP: netip.MustParseAddr("::"), HostPort: "8080"},
80 80
 				},
... ...
@@ -84,8 +83,8 @@ func TestDisableNAT(t *testing.T) {
84 84
 			name:    "nat4 routed6",
85 85
 			gwMode4: "nat",
86 86
 			gwMode6: "routed",
87
-			expPortMap: containertypes.PortMap{
88
-				containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
87
+			expPortMap: networktypes.PortMap{
88
+				networktypes.MustParsePort("80/tcp"): []networktypes.PortBinding{
89 89
 					{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: "8080"},
90 90
 					{HostIP: netip.MustParseAddr("::"), HostPort: ""},
91 91
 				},
... ...
@@ -95,8 +94,8 @@ func TestDisableNAT(t *testing.T) {
95 95
 			name:    "nat6 routed4",
96 96
 			gwMode4: "routed",
97 97
 			gwMode6: "nat",
98
-			expPortMap: containertypes.PortMap{
99
-				containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
98
+			expPortMap: networktypes.PortMap{
99
+				networktypes.MustParsePort("80/tcp"): []networktypes.PortBinding{
100 100
 					{HostIP: netip.MustParseAddr("::"), HostPort: "8080"},
101 101
 					{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: ""},
102 102
 				},
... ...
@@ -125,7 +124,7 @@ func TestDisableNAT(t *testing.T) {
125 125
 			id := container.Run(ctx, t, c,
126 126
 				container.WithNetworkMode(netName),
127 127
 				container.WithExposedPorts("80/tcp"),
128
-				container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}),
128
+				container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}),
129 129
 			)
130 130
 			defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
131 131
 
... ...
@@ -163,13 +162,13 @@ func TestPortMappedHairpinTCP(t *testing.T) {
163 163
 	serverId := container.Run(ctx, t, c,
164 164
 		container.WithNetworkMode(serverNetName),
165 165
 		container.WithExposedPorts("80"),
166
-		container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: netip.MustParseAddr("0.0.0.0")}}}),
166
+		container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80"): {{HostIP: netip.MustParseAddr("0.0.0.0")}}}),
167 167
 		container.WithCmd("httpd", "-f"),
168 168
 	)
169 169
 	defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
170 170
 
171 171
 	inspect := container.Inspect(ctx, t, c, serverId)
172
-	hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
172
+	hostPort := inspect.NetworkSettings.Ports[networktypes.MustParsePort("80/tcp")][0].HostPort
173 173
 
174 174
 	clientCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
175 175
 	defer cancel()
... ...
@@ -210,13 +209,13 @@ func TestPortMappedHairpinUDP(t *testing.T) {
210 210
 	serverId := container.Run(ctx, t, c,
211 211
 		container.WithNetworkMode(serverNetName),
212 212
 		container.WithExposedPorts("54/udp"),
213
-		container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("54/udp"): {{HostIP: netip.MustParseAddr("0.0.0.0")}}}),
213
+		container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("54/udp"): {{HostIP: netip.MustParseAddr("0.0.0.0")}}}),
214 214
 		container.WithCmd("/bin/sh", "-c", "echo 'foobar.internal 192.168.155.23' | dnsd -c - -p 54"),
215 215
 	)
216 216
 	defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
217 217
 
218 218
 	inspect := container.Inspect(ctx, t, c, serverId)
219
-	hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("54/udp")][0].HostPort
219
+	hostPort := inspect.NetworkSettings.Ports[networktypes.MustParsePort("54/udp")][0].HostPort
220 220
 
221 221
 	// nslookup gets an answer quickly from the dns server, but then tries to
222 222
 	// query another DNS server (for some unknown reasons) and times out. Hence,
... ...
@@ -252,13 +251,13 @@ func TestProxy4To6(t *testing.T) {
252 252
 	serverId := container.Run(ctx, t, c,
253 253
 		container.WithNetworkMode(netName),
254 254
 		container.WithExposedPorts("80"),
255
-		container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: netip.MustParseAddr("::1")}}}),
255
+		container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80"): {{HostIP: netip.MustParseAddr("::1")}}}),
256 256
 		container.WithCmd("httpd", "-f"),
257 257
 	)
258 258
 	defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
259 259
 
260 260
 	inspect := container.Inspect(ctx, t, c, serverId)
261
-	hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
261
+	hostPort := inspect.NetworkSettings.Ports[networktypes.MustParsePort("80/tcp")][0].HostPort
262 262
 
263 263
 	var resp *http.Response
264 264
 	addr := "http://[::1]:" + hostPort
... ...
@@ -376,7 +375,7 @@ func TestAccessPublishedPortFromHost(t *testing.T) {
376 376
 			serverID := container.Run(ctx, t, c,
377 377
 				container.WithName(sanitizeCtrName(t.Name()+"-server")),
378 378
 				container.WithExposedPorts("80/tcp"),
379
-				container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}),
379
+				container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}),
380 380
 				container.WithCmd("httpd", "-f"),
381 381
 				container.WithNetworkMode(bridgeName))
382 382
 			defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
... ...
@@ -456,7 +455,7 @@ func TestAccessPublishedPortFromRemoteHost(t *testing.T) {
456 456
 	serverID := container.Run(ctx, t, c,
457 457
 		container.WithName(sanitizeCtrName(t.Name()+"-server")),
458 458
 		container.WithExposedPorts("80/tcp"),
459
-		container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}),
459
+		container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}),
460 460
 		container.WithCmd("httpd", "-f"),
461 461
 		container.WithNetworkMode(bridgeName))
462 462
 	defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
... ...
@@ -554,13 +553,13 @@ func TestAccessPublishedPortFromCtr(t *testing.T) {
554 554
 			serverId := container.Run(ctx, t, c,
555 555
 				container.WithNetworkMode(netName),
556 556
 				container.WithExposedPorts("80"),
557
-				container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("0.0.0.0")}}}),
557
+				container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("0.0.0.0")}}}),
558 558
 				container.WithCmd("httpd", "-f"),
559 559
 			)
560 560
 			defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
561 561
 
562 562
 			inspect := container.Inspect(ctx, t, c, serverId)
563
-			hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
563
+			hostPort := inspect.NetworkSettings.Ports[networktypes.MustParsePort("80/tcp")][0].HostPort
564 564
 
565 565
 			clientCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
566 566
 			defer cancel()
... ...
@@ -604,8 +603,8 @@ func TestRestartUserlandProxyUnder2MSL(t *testing.T) {
604 604
 	ctrOpts := []func(*container.TestContainerConfig){
605 605
 		container.WithName(ctrName),
606 606
 		container.WithExposedPorts("80/tcp"),
607
-		container.WithPortMap(containertypes.PortMap{
608
-			containertypes.MustParsePort("80/tcp"): {{HostPort: "1780"}},
607
+		container.WithPortMap(networktypes.PortMap{
608
+			networktypes.MustParsePort("80/tcp"): {{HostPort: "1780"}},
609 609
 		}),
610 610
 		container.WithCmd("httpd", "-f"),
611 611
 		container.WithNetworkMode(netName),
... ...
@@ -703,8 +702,8 @@ func TestDirectRoutingOpenPorts(t *testing.T) {
703 703
 			container.WithNetworkMode(netName),
704 704
 			container.WithName("ctr-"+gwMode),
705 705
 			container.WithExposedPorts("80/tcp"),
706
-			container.WithPortMap(containertypes.PortMap{
707
-				containertypes.MustParsePort("80/tcp"): {},
706
+			container.WithPortMap(networktypes.PortMap{
707
+				networktypes.MustParsePort("80/tcp"): {},
708 708
 			}),
709 709
 		)
710 710
 		t.Cleanup(func() {
... ...
@@ -984,8 +983,8 @@ func TestRoutedNonGateway(t *testing.T) {
984 984
 	ctrId := container.Run(ctx, t, c,
985 985
 		container.WithCmd("httpd", "-f"),
986 986
 		container.WithExposedPorts("80/tcp"),
987
-		container.WithPortMap(containertypes.PortMap{
988
-			containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}},
987
+		container.WithPortMap(networktypes.PortMap{
988
+			networktypes.MustParsePort("80/tcp"): {{HostPort: "8080"}},
989 989
 		}),
990 990
 		container.WithNetworkMode(natNetName),
991 991
 		container.WithNetworkMode(routedNetName),
... ...
@@ -1140,8 +1139,8 @@ func TestAccessPublishedPortFromAnotherNetwork(t *testing.T) {
1140 1140
 					container.WithName("server"),
1141 1141
 					container.WithCmd("nc", "-lp", "5000"),
1142 1142
 					container.WithExposedPorts("5000/tcp"),
1143
-					container.WithPortMap(containertypes.PortMap{
1144
-						containertypes.MustParsePort("5000/tcp"): {{HostPort: "5000"}},
1143
+					container.WithPortMap(networktypes.PortMap{
1144
+						networktypes.MustParsePort("5000/tcp"): {{HostPort: "5000"}},
1145 1145
 					}),
1146 1146
 					container.WithNetworkMode(servnet))
1147 1147
 				defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
... ...
@@ -1338,8 +1337,8 @@ func testDirectRemoteAccessOnExposedPort(t *testing.T, ctx context.Context, d *d
1338 1338
 					container.WithName(sanitizeCtrName(t.Name()+"-server")),
1339 1339
 					container.WithCmd("nc", "-lup", "5000"),
1340 1340
 					container.WithExposedPorts("5000/udp"),
1341
-					container.WithPortMap(containertypes.PortMap{
1342
-						containertypes.MustParsePort("5000/udp"): {{HostPort: hostPort}},
1341
+					container.WithPortMap(networktypes.PortMap{
1342
+						networktypes.MustParsePort("5000/udp"): {{HostPort: hostPort}},
1343 1343
 					}),
1344 1344
 					container.WithNetworkMode(bridgeName),
1345 1345
 					container.WithEndpointSettings(bridgeName, &networktypes.EndpointSettings{
... ...
@@ -1426,8 +1425,8 @@ func TestAccessPortPublishedOnLoopbackAddress(t *testing.T) {
1426 1426
 			container.WithCmd("nc", "-lup", "5000"),
1427 1427
 			container.WithExposedPorts("5000/udp"),
1428 1428
 			// This port is mapped on 127.0.0.2, so it should not be remotely accessible.
1429
-			container.WithPortMap(containertypes.PortMap{
1430
-				containertypes.MustParsePort("5000/udp"): {{HostIP: netip.MustParseAddr(loIP), HostPort: hostPort}},
1429
+			container.WithPortMap(networktypes.PortMap{
1430
+				networktypes.MustParsePort("5000/udp"): {{HostIP: netip.MustParseAddr(loIP), HostPort: hostPort}},
1431 1431
 			}),
1432 1432
 			container.WithNetworkMode(bridgeName))
1433 1433
 		defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
... ...
@@ -1539,7 +1538,7 @@ func TestSkipRawRules(t *testing.T) {
1539 1539
 
1540 1540
 				ctrId := container.Run(ctx, t, c,
1541 1541
 					container.WithExposedPorts("80/tcp"),
1542
-					container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
1542
+					container.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {
1543 1543
 						{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "8080"},
1544 1544
 						{HostPort: "8081"},
1545 1545
 					}}),
... ...
@@ -1571,10 +1570,10 @@ func TestMixAnyWithSpecificHostAddrs(t *testing.T) {
1571 1571
 
1572 1572
 			ctrId := container.Run(ctx, t, c,
1573 1573
 				container.WithExposedPorts("80/"+proto, "81/"+proto, "82/"+proto),
1574
-				container.WithPortMap(containertypes.PortMap{
1575
-					containertypes.MustParsePort("81/" + proto): {{}},
1576
-					containertypes.MustParsePort("82/" + proto): {{}},
1577
-					containertypes.MustParsePort("80/" + proto): {{HostIP: netip.MustParseAddr("127.0.0.1")}},
1574
+				container.WithPortMap(networktypes.PortMap{
1575
+					networktypes.MustParsePort("81/" + proto): {{}},
1576
+					networktypes.MustParsePort("82/" + proto): {{}},
1577
+					networktypes.MustParsePort("80/" + proto): {{HostIP: netip.MustParseAddr("127.0.0.1")}},
1578 1578
 				}),
1579 1579
 			)
1580 1580
 			defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"testing"
13 13
 
14 14
 	containertypes "github.com/moby/moby/api/types/container"
15
+	"github.com/moby/moby/api/types/network"
15 16
 	"github.com/moby/moby/client"
16 17
 	"github.com/moby/moby/v2/internal/testutil"
17 18
 	"github.com/moby/moby/v2/internal/testutil/environment"
... ...
@@ -164,7 +165,7 @@ COPY . /static`); err != nil {
164 164
 	// Find out the system assigned port
165 165
 	i, err := c.ContainerInspect(context.Background(), b.ID)
166 166
 	assert.NilError(t, err)
167
-	ports, exists := i.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")]
167
+	ports, exists := i.NetworkSettings.Ports[network.MustParsePort("80/tcp")]
168 168
 	assert.Assert(t, exists, "unable to find port 80/tcp for %s", ctrName)
169 169
 	if len(ports) == 0 {
170 170
 		t.Fatalf("no ports mapped for 80/tcp for %s: %#v", ctrName, i.NetworkSettings.Ports)
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"time"
5 5
 
6 6
 	dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
7
+	"github.com/moby/moby/api/types/network"
7 8
 )
8 9
 
9 10
 // MinimumDuration puts a minimum on user configured duration.
... ...
@@ -28,7 +29,7 @@ type Config struct {
28 28
 	AttachStdin     bool                // Attach the standard input, makes possible user interaction
29 29
 	AttachStdout    bool                // Attach the standard output
30 30
 	AttachStderr    bool                // Attach the standard error
31
-	ExposedPorts    PortSet             `json:",omitempty"` // List of exposed ports
31
+	ExposedPorts    network.PortSet     `json:",omitempty"` // List of exposed ports
32 32
 	Tty             bool                // Attach standard streams to a tty, including stdin if it is not closed.
33 33
 	OpenStdin       bool                // Open stdin
34 34
 	StdinOnce       bool                // If true, close stdin after the 1 attached client disconnects.
... ...
@@ -421,7 +421,7 @@ type HostConfig struct {
421 421
 	ContainerIDFile string            // File (path) where the containerId is written
422 422
 	LogConfig       LogConfig         // Configuration of the logs for this container
423 423
 	NetworkMode     NetworkMode       // Network mode to use for the container
424
-	PortBindings    PortMap           // Port mapping between the exposed port (container) and the host
424
+	PortBindings    network.PortMap   // Port mapping between the exposed port (container) and the host
425 425
 	RestartPolicy   RestartPolicy     // Restart policy to be used for the container
426 426
 	AutoRemove      bool              // Automatically remove container when it exits
427 427
 	VolumeDriver    string            // Name of the volume driver used to mount volumes
428 428
deleted file mode 100644
... ...
@@ -1,346 +0,0 @@
1
-package container
2
-
3
-import (
4
-	"errors"
5
-	"fmt"
6
-	"iter"
7
-	"net/netip"
8
-	"strconv"
9
-	"strings"
10
-	"unique"
11
-)
12
-
13
-// NetworkProtocol represents a network protocol for a port.
14
-type NetworkProtocol string
15
-
16
-const (
17
-	TCP  NetworkProtocol = "tcp"
18
-	UDP  NetworkProtocol = "udp"
19
-	SCTP NetworkProtocol = "sctp"
20
-)
21
-
22
-// Sentinel port proto value for zero Port and PortRange values.
23
-var protoZero unique.Handle[NetworkProtocol]
24
-
25
-// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
26
-//
27
-// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value.
28
-type Port struct {
29
-	num   uint16
30
-	proto unique.Handle[NetworkProtocol]
31
-}
32
-
33
-// ParsePort parses s as a [Port].
34
-//
35
-// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent.
36
-// If a port number is provided, but no protocol, the default ("tcp") protocol is returned.
37
-func ParsePort(s string) (Port, error) {
38
-	if s == "" {
39
-		return Port{}, errors.New("invalid port: value is empty")
40
-	}
41
-
42
-	port, proto, _ := strings.Cut(s, "/")
43
-
44
-	portNum, err := parsePortNumber(port)
45
-	if err != nil {
46
-		return Port{}, fmt.Errorf("invalid port '%s': %w", port, err)
47
-	}
48
-
49
-	normalizedPortProto := normalizePortProto(proto)
50
-	return Port{num: portNum, proto: normalizedPortProto}, nil
51
-}
52
-
53
-// MustParsePort calls [ParsePort](s) and panics on error.
54
-//
55
-// It is intended for use in tests with hard-coded strings.
56
-func MustParsePort(s string) Port {
57
-	p, err := ParsePort(s)
58
-	if err != nil {
59
-		panic(err)
60
-	}
61
-	return p
62
-}
63
-
64
-// PortFrom returns a [Port] with the given number and protocol.
65
-//
66
-// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
67
-func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) {
68
-	if proto == "" {
69
-		return Port{}, false
70
-	}
71
-	normalized := normalizePortProto(string(proto))
72
-	return Port{num: num, proto: normalized}, true
73
-}
74
-
75
-// Num returns p's port number.
76
-func (p Port) Num() uint16 {
77
-	return p.num
78
-}
79
-
80
-// Proto returns p's network protocol.
81
-func (p Port) Proto() NetworkProtocol {
82
-	return p.proto.Value()
83
-}
84
-
85
-// IsZero reports whether p is the zero value.
86
-func (p Port) IsZero() bool {
87
-	return p.proto == protoZero
88
-}
89
-
90
-// IsValid reports whether p is an initialized valid port (not the zero value).
91
-func (p Port) IsValid() bool {
92
-	return p.proto != protoZero
93
-}
94
-
95
-// String returns a string representation of the port in the format "<portnum>/<proto>".
96
-// If the port is the zero value, it returns "invalid port".
97
-func (p Port) String() string {
98
-	switch p.proto {
99
-	case protoZero:
100
-		return "invalid port"
101
-	default:
102
-		return string(p.AppendTo(nil))
103
-	}
104
-}
105
-
106
-// AppendText implements [encoding.TextAppender] interface.
107
-// It is the same as [Port.AppendTo] but returns an error to satisfy the interface.
108
-func (p Port) AppendText(b []byte) ([]byte, error) {
109
-	return p.AppendTo(b), nil
110
-}
111
-
112
-// AppendTo appends a text encoding of p to b and returns the extended buffer.
113
-func (p Port) AppendTo(b []byte) []byte {
114
-	if p.IsZero() {
115
-		return b
116
-	}
117
-	return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value())
118
-}
119
-
120
-// MarshalText implements [encoding.TextMarshaler] interface.
121
-func (p Port) MarshalText() ([]byte, error) {
122
-	return p.AppendText(nil)
123
-}
124
-
125
-// UnmarshalText implements [encoding.TextUnmarshaler] interface.
126
-func (p *Port) UnmarshalText(text []byte) error {
127
-	if len(text) == 0 {
128
-		*p = Port{}
129
-		return nil
130
-	}
131
-
132
-	port, err := ParsePort(string(text))
133
-	if err != nil {
134
-		return err
135
-	}
136
-
137
-	*p = port
138
-	return nil
139
-}
140
-
141
-// Range returns a [PortRange] representing the single port.
142
-func (p Port) Range() PortRange {
143
-	return PortRange{start: p.num, end: p.num, proto: p.proto}
144
-}
145
-
146
-// PortSet is a collection of structs indexed by [Port].
147
-type PortSet = map[Port]struct{}
148
-
149
-// PortBinding represents a binding between a Host IP address and a Host Port.
150
-type PortBinding struct {
151
-	// HostIP is the host IP Address
152
-	HostIP netip.Addr `json:"HostIp"`
153
-	// HostPort is the host port number
154
-	HostPort string `json:"HostPort"`
155
-}
156
-
157
-// PortMap is a collection of [PortBinding] indexed by [Port].
158
-type PortMap = map[Port][]PortBinding
159
-
160
-// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp".
161
-//
162
-// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value.
163
-type PortRange struct {
164
-	start uint16
165
-	end   uint16
166
-	proto unique.Handle[NetworkProtocol]
167
-}
168
-
169
-// ParsePortRange parses s as a [PortRange].
170
-//
171
-// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent.
172
-// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned.
173
-func ParsePortRange(s string) (PortRange, error) {
174
-	if s == "" {
175
-		return PortRange{}, errors.New("invalid port range: value is empty")
176
-	}
177
-
178
-	portRange, proto, _ := strings.Cut(s, "/")
179
-
180
-	start, end, ok := strings.Cut(portRange, "-")
181
-	startVal, err := parsePortNumber(start)
182
-	if err != nil {
183
-		return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err)
184
-	}
185
-
186
-	portProto := normalizePortProto(proto)
187
-
188
-	if !ok || start == end {
189
-		return PortRange{start: startVal, end: startVal, proto: portProto}, nil
190
-	}
191
-
192
-	endVal, err := parsePortNumber(end)
193
-	if err != nil {
194
-		return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err)
195
-	}
196
-	if endVal < startVal {
197
-		return PortRange{}, errors.New("invalid port range: " + s)
198
-	}
199
-	return PortRange{start: startVal, end: endVal, proto: portProto}, nil
200
-}
201
-
202
-// MustParsePortRange calls [ParsePortRange](s) and panics on error.
203
-// It is intended for use in tests with hard-coded strings.
204
-func MustParsePortRange(s string) PortRange {
205
-	pr, err := ParsePortRange(s)
206
-	if err != nil {
207
-		panic(err)
208
-	}
209
-	return pr
210
-}
211
-
212
-// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
213
-//
214
-// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
215
-func PortRangeFrom(start, end uint16, proto NetworkProtocol) (pr PortRange, ok bool) {
216
-	if end < start || proto == "" {
217
-		return PortRange{}, false
218
-	}
219
-	normalized := normalizePortProto(string(proto))
220
-	return PortRange{start: start, end: end, proto: normalized}, true
221
-}
222
-
223
-// Start returns pr's start port number.
224
-func (pr PortRange) Start() uint16 {
225
-	return pr.start
226
-}
227
-
228
-// End returns pr's end port number.
229
-func (pr PortRange) End() uint16 {
230
-	return pr.end
231
-}
232
-
233
-// Proto returns pr's network protocol.
234
-func (pr PortRange) Proto() NetworkProtocol {
235
-	return pr.proto.Value()
236
-}
237
-
238
-// IsZero reports whether pr is the zero value.
239
-func (pr PortRange) IsZero() bool {
240
-	return pr.proto == protoZero
241
-}
242
-
243
-// IsValid reports whether pr is an initialized valid port range (not the zero value).
244
-func (pr PortRange) IsValid() bool {
245
-	return pr.proto != protoZero
246
-}
247
-
248
-// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
249
-// If the port range is the zero value, it returns "invalid port range".
250
-func (pr PortRange) String() string {
251
-	switch pr.proto {
252
-	case protoZero:
253
-		return "invalid port range"
254
-	default:
255
-		return string(pr.AppendTo(nil))
256
-	}
257
-}
258
-
259
-// AppendText implements [encoding.TextAppender] interface.
260
-// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface.
261
-func (pr PortRange) AppendText(b []byte) ([]byte, error) {
262
-	return pr.AppendTo(b), nil
263
-}
264
-
265
-// AppendTo appends a text encoding of pr to b and returns the extended buffer.
266
-func (pr PortRange) AppendTo(b []byte) []byte {
267
-	if pr.IsZero() {
268
-		return b
269
-	}
270
-	if pr.start == pr.end {
271
-		return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value())
272
-	}
273
-	return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value())
274
-}
275
-
276
-// MarshalText implements [encoding.TextMarshaler] interface.
277
-func (pr PortRange) MarshalText() ([]byte, error) {
278
-	return pr.AppendText(nil)
279
-}
280
-
281
-// UnmarshalText implements [encoding.TextUnmarshaler] interface.
282
-func (pr *PortRange) UnmarshalText(text []byte) error {
283
-	if len(text) == 0 {
284
-		*pr = PortRange{}
285
-		return nil
286
-	}
287
-
288
-	portRange, err := ParsePortRange(string(text))
289
-	if err != nil {
290
-		return err
291
-	}
292
-	*pr = portRange
293
-	return nil
294
-}
295
-
296
-// Range returns pr.
297
-func (pr PortRange) Range() PortRange {
298
-	return pr
299
-}
300
-
301
-// All returns an iterator over all the individual ports in the range.
302
-//
303
-// For example:
304
-//
305
-//	for port := range pr.All() {
306
-//	    // ...
307
-//	}
308
-func (pr PortRange) All() iter.Seq[Port] {
309
-	return func(yield func(Port) bool) {
310
-		for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
311
-			if !yield(Port{num: uint16(i), proto: pr.proto}) {
312
-				return
313
-			}
314
-		}
315
-	}
316
-}
317
-
318
-// parsePortNumber parses rawPort into an int, unwrapping strconv errors
319
-// and returning a single "out of range" error for any value outside 0–65535.
320
-func parsePortNumber(rawPort string) (uint16, error) {
321
-	if rawPort == "" {
322
-		return 0, errors.New("value is empty")
323
-	}
324
-	port, err := strconv.ParseUint(rawPort, 10, 16)
325
-	if err != nil {
326
-		var numErr *strconv.NumError
327
-		if errors.As(err, &numErr) {
328
-			err = numErr.Err
329
-		}
330
-		return 0, err
331
-	}
332
-
333
-	return uint16(port), nil
334
-}
335
-
336
-// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent.
337
-// If proto is not specified, it defaults to "tcp".
338
-func normalizePortProto(proto string) unique.Handle[NetworkProtocol] {
339
-	if proto == "" {
340
-		return unique.Make(TCP)
341
-	}
342
-
343
-	proto = strings.ToLower(proto)
344
-
345
-	return unique.Make(NetworkProtocol(proto))
346
-}
... ...
@@ -6,10 +6,13 @@ import (
6 6
 
7 7
 // NetworkSettings exposes the network settings in the api
8 8
 type NetworkSettings struct {
9
-	SandboxID  string  // SandboxID uniquely represents a container's network stack
10
-	SandboxKey string  // SandboxKey identifies the sandbox
11
-	Ports      PortMap // Ports is a collection of PortBinding indexed by Port
12
-	Networks   map[string]*network.EndpointSettings
9
+	SandboxID  string // SandboxID uniquely represents a container's network stack
10
+	SandboxKey string // SandboxKey identifies the sandbox
11
+
12
+	// Ports is a collection of [network.PortBinding] indexed by [network.Port]
13
+	Ports network.PortMap
14
+
15
+	Networks map[string]*network.EndpointSettings
13 16
 }
14 17
 
15 18
 // NetworkSettingsSummary provides a summary of container's networks
16 19
new file mode 100644
... ...
@@ -0,0 +1,346 @@
0
+package network
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"iter"
6
+	"net/netip"
7
+	"strconv"
8
+	"strings"
9
+	"unique"
10
+)
11
+
12
+// IPProtocol represents a network protocol for a port.
13
+type IPProtocol string
14
+
15
+const (
16
+	TCP  IPProtocol = "tcp"
17
+	UDP  IPProtocol = "udp"
18
+	SCTP IPProtocol = "sctp"
19
+)
20
+
21
+// Sentinel port proto value for zero Port and PortRange values.
22
+var protoZero unique.Handle[IPProtocol]
23
+
24
+// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
25
+//
26
+// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value.
27
+type Port struct {
28
+	num   uint16
29
+	proto unique.Handle[IPProtocol]
30
+}
31
+
32
+// ParsePort parses s as a [Port].
33
+//
34
+// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent.
35
+// If a port number is provided, but no protocol, the default ("tcp") protocol is returned.
36
+func ParsePort(s string) (Port, error) {
37
+	if s == "" {
38
+		return Port{}, errors.New("invalid port: value is empty")
39
+	}
40
+
41
+	port, proto, _ := strings.Cut(s, "/")
42
+
43
+	portNum, err := parsePortNumber(port)
44
+	if err != nil {
45
+		return Port{}, fmt.Errorf("invalid port '%s': %w", port, err)
46
+	}
47
+
48
+	normalizedPortProto := normalizePortProto(proto)
49
+	return Port{num: portNum, proto: normalizedPortProto}, nil
50
+}
51
+
52
+// MustParsePort calls [ParsePort](s) and panics on error.
53
+//
54
+// It is intended for use in tests with hard-coded strings.
55
+func MustParsePort(s string) Port {
56
+	p, err := ParsePort(s)
57
+	if err != nil {
58
+		panic(err)
59
+	}
60
+	return p
61
+}
62
+
63
+// PortFrom returns a [Port] with the given number and protocol.
64
+//
65
+// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
66
+func PortFrom(num uint16, proto IPProtocol) (p Port, ok bool) {
67
+	if proto == "" {
68
+		return Port{}, false
69
+	}
70
+	normalized := normalizePortProto(string(proto))
71
+	return Port{num: num, proto: normalized}, true
72
+}
73
+
74
+// Num returns p's port number.
75
+func (p Port) Num() uint16 {
76
+	return p.num
77
+}
78
+
79
+// Proto returns p's network protocol.
80
+func (p Port) Proto() IPProtocol {
81
+	return p.proto.Value()
82
+}
83
+
84
+// IsZero reports whether p is the zero value.
85
+func (p Port) IsZero() bool {
86
+	return p.proto == protoZero
87
+}
88
+
89
+// IsValid reports whether p is an initialized valid port (not the zero value).
90
+func (p Port) IsValid() bool {
91
+	return p.proto != protoZero
92
+}
93
+
94
+// String returns a string representation of the port in the format "<portnum>/<proto>".
95
+// If the port is the zero value, it returns "invalid port".
96
+func (p Port) String() string {
97
+	switch p.proto {
98
+	case protoZero:
99
+		return "invalid port"
100
+	default:
101
+		return string(p.AppendTo(nil))
102
+	}
103
+}
104
+
105
+// AppendText implements [encoding.TextAppender] interface.
106
+// It is the same as [Port.AppendTo] but returns an error to satisfy the interface.
107
+func (p Port) AppendText(b []byte) ([]byte, error) {
108
+	return p.AppendTo(b), nil
109
+}
110
+
111
+// AppendTo appends a text encoding of p to b and returns the extended buffer.
112
+func (p Port) AppendTo(b []byte) []byte {
113
+	if p.IsZero() {
114
+		return b
115
+	}
116
+	return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value())
117
+}
118
+
119
+// MarshalText implements [encoding.TextMarshaler] interface.
120
+func (p Port) MarshalText() ([]byte, error) {
121
+	return p.AppendText(nil)
122
+}
123
+
124
+// UnmarshalText implements [encoding.TextUnmarshaler] interface.
125
+func (p *Port) UnmarshalText(text []byte) error {
126
+	if len(text) == 0 {
127
+		*p = Port{}
128
+		return nil
129
+	}
130
+
131
+	port, err := ParsePort(string(text))
132
+	if err != nil {
133
+		return err
134
+	}
135
+
136
+	*p = port
137
+	return nil
138
+}
139
+
140
+// Range returns a [PortRange] representing the single port.
141
+func (p Port) Range() PortRange {
142
+	return PortRange{start: p.num, end: p.num, proto: p.proto}
143
+}
144
+
145
+// PortSet is a collection of structs indexed by [Port].
146
+type PortSet = map[Port]struct{}
147
+
148
+// PortBinding represents a binding between a Host IP address and a Host Port.
149
+type PortBinding struct {
150
+	// HostIP is the host IP Address
151
+	HostIP netip.Addr `json:"HostIp"`
152
+	// HostPort is the host port number
153
+	HostPort string `json:"HostPort"`
154
+}
155
+
156
+// PortMap is a collection of [PortBinding] indexed by [Port].
157
+type PortMap = map[Port][]PortBinding
158
+
159
+// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp".
160
+//
161
+// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value.
162
+type PortRange struct {
163
+	start uint16
164
+	end   uint16
165
+	proto unique.Handle[IPProtocol]
166
+}
167
+
168
+// ParsePortRange parses s as a [PortRange].
169
+//
170
+// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent.
171
+// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned.
172
+func ParsePortRange(s string) (PortRange, error) {
173
+	if s == "" {
174
+		return PortRange{}, errors.New("invalid port range: value is empty")
175
+	}
176
+
177
+	portRange, proto, _ := strings.Cut(s, "/")
178
+
179
+	start, end, ok := strings.Cut(portRange, "-")
180
+	startVal, err := parsePortNumber(start)
181
+	if err != nil {
182
+		return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err)
183
+	}
184
+
185
+	portProto := normalizePortProto(proto)
186
+
187
+	if !ok || start == end {
188
+		return PortRange{start: startVal, end: startVal, proto: portProto}, nil
189
+	}
190
+
191
+	endVal, err := parsePortNumber(end)
192
+	if err != nil {
193
+		return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err)
194
+	}
195
+	if endVal < startVal {
196
+		return PortRange{}, errors.New("invalid port range: " + s)
197
+	}
198
+	return PortRange{start: startVal, end: endVal, proto: portProto}, nil
199
+}
200
+
201
+// MustParsePortRange calls [ParsePortRange](s) and panics on error.
202
+// It is intended for use in tests with hard-coded strings.
203
+func MustParsePortRange(s string) PortRange {
204
+	pr, err := ParsePortRange(s)
205
+	if err != nil {
206
+		panic(err)
207
+	}
208
+	return pr
209
+}
210
+
211
+// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
212
+//
213
+// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
214
+func PortRangeFrom(start, end uint16, proto IPProtocol) (pr PortRange, ok bool) {
215
+	if end < start || proto == "" {
216
+		return PortRange{}, false
217
+	}
218
+	normalized := normalizePortProto(string(proto))
219
+	return PortRange{start: start, end: end, proto: normalized}, true
220
+}
221
+
222
+// Start returns pr's start port number.
223
+func (pr PortRange) Start() uint16 {
224
+	return pr.start
225
+}
226
+
227
+// End returns pr's end port number.
228
+func (pr PortRange) End() uint16 {
229
+	return pr.end
230
+}
231
+
232
+// Proto returns pr's network protocol.
233
+func (pr PortRange) Proto() IPProtocol {
234
+	return pr.proto.Value()
235
+}
236
+
237
+// IsZero reports whether pr is the zero value.
238
+func (pr PortRange) IsZero() bool {
239
+	return pr.proto == protoZero
240
+}
241
+
242
+// IsValid reports whether pr is an initialized valid port range (not the zero value).
243
+func (pr PortRange) IsValid() bool {
244
+	return pr.proto != protoZero
245
+}
246
+
247
+// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
248
+// If the port range is the zero value, it returns "invalid port range".
249
+func (pr PortRange) String() string {
250
+	switch pr.proto {
251
+	case protoZero:
252
+		return "invalid port range"
253
+	default:
254
+		return string(pr.AppendTo(nil))
255
+	}
256
+}
257
+
258
+// AppendText implements [encoding.TextAppender] interface.
259
+// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface.
260
+func (pr PortRange) AppendText(b []byte) ([]byte, error) {
261
+	return pr.AppendTo(b), nil
262
+}
263
+
264
+// AppendTo appends a text encoding of pr to b and returns the extended buffer.
265
+func (pr PortRange) AppendTo(b []byte) []byte {
266
+	if pr.IsZero() {
267
+		return b
268
+	}
269
+	if pr.start == pr.end {
270
+		return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value())
271
+	}
272
+	return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value())
273
+}
274
+
275
+// MarshalText implements [encoding.TextMarshaler] interface.
276
+func (pr PortRange) MarshalText() ([]byte, error) {
277
+	return pr.AppendText(nil)
278
+}
279
+
280
+// UnmarshalText implements [encoding.TextUnmarshaler] interface.
281
+func (pr *PortRange) UnmarshalText(text []byte) error {
282
+	if len(text) == 0 {
283
+		*pr = PortRange{}
284
+		return nil
285
+	}
286
+
287
+	portRange, err := ParsePortRange(string(text))
288
+	if err != nil {
289
+		return err
290
+	}
291
+	*pr = portRange
292
+	return nil
293
+}
294
+
295
+// Range returns pr.
296
+func (pr PortRange) Range() PortRange {
297
+	return pr
298
+}
299
+
300
+// All returns an iterator over all the individual ports in the range.
301
+//
302
+// For example:
303
+//
304
+//	for port := range pr.All() {
305
+//	    // ...
306
+//	}
307
+func (pr PortRange) All() iter.Seq[Port] {
308
+	return func(yield func(Port) bool) {
309
+		for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
310
+			if !yield(Port{num: uint16(i), proto: pr.proto}) {
311
+				return
312
+			}
313
+		}
314
+	}
315
+}
316
+
317
+// parsePortNumber parses rawPort into an int, unwrapping strconv errors
318
+// and returning a single "out of range" error for any value outside 0–65535.
319
+func parsePortNumber(rawPort string) (uint16, error) {
320
+	if rawPort == "" {
321
+		return 0, errors.New("value is empty")
322
+	}
323
+	port, err := strconv.ParseUint(rawPort, 10, 16)
324
+	if err != nil {
325
+		var numErr *strconv.NumError
326
+		if errors.As(err, &numErr) {
327
+			err = numErr.Err
328
+		}
329
+		return 0, err
330
+	}
331
+
332
+	return uint16(port), nil
333
+}
334
+
335
+// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent.
336
+// If proto is not specified, it defaults to "tcp".
337
+func normalizePortProto(proto string) unique.Handle[IPProtocol] {
338
+	if proto == "" {
339
+		return unique.Make(TCP)
340
+	}
341
+
342
+	proto = strings.ToLower(proto)
343
+
344
+	return unique.Make(IPProtocol(proto))
345
+}