Skip to content

api: add container network port types#50710

Merged
austinvazquez merged 1 commit intomoby:masterfrom
austinvazquez:define-network-port-types
Oct 3, 2025
Merged

api: add container network port types#50710
austinvazquez merged 1 commit intomoby:masterfrom
austinvazquez:define-network-port-types

Conversation

@austinvazquez
Copy link
Contributor

@austinvazquez austinvazquez commented Aug 13, 2025

- What I did

Moves the types defines in github.com/docker/go-connections/nat package into api/types/container package and reworks the daemon usage of github.com/docker/go-connections/nat to internal utility code only.

Reworks previous PortProto and PortRangeProto into new distinct types api/types/container.Port and api/types/container.PortRange which are serializable and comparable.

- How I did it

- How to verify it

- Human readable description for the release notes

api: redefine container network port types

- A picture of a cute animal (not mandatory but encouraged)

@austinvazquez
Copy link
Contributor Author

Related to docker/go-connections#148

@thaJeztah
Copy link
Member

Ah, this requires the code to be updated to use PortProto instead of PortRangeProto;

24.95 # github.com/moby/moby/v2/daemon/links
24.95 daemon/links/links.go:23:20: undefined: container.PortRangeProto

Let me push a quick fix to alias it; we'll probably also need a (temporary) "replace" rule for the go-connections package as used in the client and elsewhere from a branch that aliases nat.Port => api/types/container.PortProto. That allows us to verify the aliasing work, but also that (without any other changes) we don't have a circular import problem.

@thaJeztah thaJeztah force-pushed the define-network-port-types branch 2 times, most recently from bcf6d14 to c43534b Compare August 13, 2025 12:02
@thaJeztah
Copy link
Member

Pushed a couple of commits to see if we can make it build (but possibly remove / replace some code in the daemon). This failure looks like it could be related though; could be just presentation, or an issue to look into;

=== RUN   TestAllPortMappingsAreReturned
    bridge_linux_test.go:557: assertion failed: 
        --- inspect.NetworkSettings.Ports
        +++ →
          map[container.PortProto][]container.PortBinding{
          	"80/tcp": {{HostIP: "0.0.0.0", HostPort: "8000"}, {HostIP: "::", HostPort: "8000"}},
        + 	"81/tcp": nil,
          }

@thaJeztah thaJeztah force-pushed the define-network-port-types branch from c43534b to c17606c Compare August 13, 2025 12:17
@thaJeztah
Copy link
Member

Yeah, clearly more to fix 😂 (looks like possibly same underlying issue as the other failure though)

=== Failed
=== FAIL: amd64.integration-cli TestDockerCLIPortSuite/TestPortExposeHostBinding (0.47s)
    docker_cli_port_test.go:336: assertion failed: 
        Command:  /usr/local/cli-integration/docker inspect --format {{index .NetworkSettings.Ports "80/tcp" 0 "HostPort" }} 698eaf49fb9749212c4e2afda45ec7f6c55e8c6f9eaddd366ec6351e0f9cf069
        ExitCode: 1
        Error:    exit status 1
        Stdout:   
        
        Stderr:   Template parsing error: template: :1:2: executing "" at <index .NetworkSettin...>: error calling index: index of nil pointer
        
        
        Failures:
        ExitCode was 1 expected 0
        Expected no error

@thaJeztah thaJeztah force-pushed the define-network-port-types branch from c17606c to f4e13e8 Compare August 13, 2025 15:30
@austinvazquez
Copy link
Contributor Author

=== FAIL: arm64.docker.docker.integration.network.bridge TestAllPortMappingsAreReturned (1.84s)
bridge_linux_test.go:557: assertion failed:
--- inspect.NetworkSettings.Ports
+++ →
map[container.PortProto][]container.PortBinding{
"80/tcp": {{HostIP: "0.0.0.0", HostPort: "8000"}, {HostIP: "::", HostPort: "8000"}},
+ "81/tcp": nil,
}

bindings[nat.Port(p)] = tmpB
}

ports := slices.Collect(maps.Keys(ctr.Config.ExposedPorts))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the cause of this CI failure:

=== FAIL: arm64.docker.docker.integration.network.bridge TestAllPortMappingsAreReturned (1.84s)
bridge_linux_test.go:557: assertion failed:
--- inspect.NetworkSettings.Ports
+++ →
map[container.PortProto][]container.PortBinding{
"80/tcp": {{HostIP: "0.0.0.0", HostPort: "8000"}, {HostIP: "::", HostPort: "8000"}},
+ "81/tcp": nil,
}

inspect.NetworkSettings.Ports is supposed to contain both currently-mapped ports, and exposed but unmapped ports. Some people / software are depending on this, so we can't change that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW here's how that test can be reproduced with plain docker commands:

$ DOCKER_USERLANDPROXY=false ./hack/make.sh binary install-binary run
$ docker network create --ipv4=true --ipv6=false testnetv4
$ docker network create --ipv4=false --ipv6=true testnetv6
$ docker run --rm -d -p 8000:80/tcp --expose 81/tcp --network testnetv4 --network testnetv6 busybox httpd -f
$ docker ps
CONTAINER ID   IMAGE                           COMMAND                  CREATED              STATUS              PORTS                                     NAMES
02fbc1e14bb4   busybox                         "httpd -f"               About a minute ago   Up About a minute   0.0.0.0:8000->80/tcp, [::]:8000->80/tcp   pedantic_lalande
$ docker inspect 02fbc1e14bb4

In inspect output, there should be two keys under NetworkSettings.Ports (80/tcp and 81/tcp) but there's only one when running these commands against this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @akerouanton that saved me lots of debugging time. Trying out a change to restore the behavior for exposed ports but unmapped ports in network settings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes I think this was one of my quick hacks to make it work; possibly we no longer need it if we have a version of go-connections that aliases things (and/or has docker/go-connections#146)

For some of these we need to look if we REALLY need the utilities from go-connections or if we should have utilities more specific to the use at hand; I think there's various places where the daemon code has to do multiple conversions that are not always needed, but just because it was the only way to do it with the tools in go-connections.

Copy link
Member

@akerouanton akerouanton Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only interesting bit from nat.SortPortMap() is that it creates 'dummy' entries (i.e. nil slices) for un-mapped but exposed ports. People / software are depending on that it seems, so we can't break it.

Otherwise, the sorting itself is not needed. I guess at some point it was influencing the sorting of NetworkSettings.Ports but: 1. if that was the original intent, it should have been done in (*Daemon).ContainerInspect(), not here; 2. since commit 043db3b (released in v27.0), the bridge driver has its own sorting logic to fit its needs.

bindingReqs := n.sortAndNormPBs(ctx, ep, cfg, defHostIP, pbmReq)

@austinvazquez austinvazquez force-pushed the define-network-port-types branch 2 times, most recently from a53fc88 to ee6625f Compare August 13, 2025 20:08
@austinvazquez austinvazquez force-pushed the define-network-port-types branch 2 times, most recently from 5623a8f to abf22c2 Compare August 15, 2025 19:20
@austinvazquez austinvazquez changed the title [DNM] Add container network port types to api module api: add container network port types Aug 15, 2025
@austinvazquez austinvazquez force-pushed the define-network-port-types branch 2 times, most recently from a01218f to 3cfc199 Compare August 15, 2025 21:45
@austinvazquez austinvazquez marked this pull request as ready for review August 15, 2025 21:45
@austinvazquez austinvazquez force-pushed the define-network-port-types branch from d4a2414 to 7a80300 Compare August 16, 2025 00:50
Comment on lines +119 to +127
for p := range ctr.Config.ExposedPorts {
if _, ok := portBindings[p]; !ok {
// Create nil entries for exposed but un-mapped ports.
portBindings[p] = nil
} else if len(portBindings[p]) == 0 {
// Create default port binding.
portBindings[p] = []containertypes.PortBinding{{}}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akerouanton, @thaJeztah, wanted to get your thoughts here. Through test/debugging I found the issue with migrating away from nat.SortPortMap is it would create a default PortBinding for any ports with no specified port binding.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would create a default PortBinding for any ports with no specified port binding.

You mean for any Exposed ports with no specified port binding? I believe this is the current behavior 🤔

If I'm misunderstanding your comment, could you please include a diff of before / after removing nat.SortPortMap?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akerouanton, below is some before/afters from debug code and running network/bridge.TestPortMappingRestore.

This is the bindings map before these changes before/after sorting.

bindings before sorting: map[80/tcp:[]]
bindings after sorting: map[80/tcp:[{ }]]

This is the portBindings slice with my changes before/after the new exposed port handling logic.

portBindings before exposed ports: map[80/tcp:[]]
portBindings after exposed ports: map[80/tcp:[{HostIP: HostPort:}]]

The piece to note is the empty struct after sorting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me summurize my findings here so that we can easily link to it in the fix.

nat.SortPortMap is backfilling an empty PortBinding into the PortBindings slice if it's empty. That sort function is called during ContainerStart. So, containers created with empty PortBindings would have something like that stored on-disk:

containertypes.PortMap{"80/tcp": {}}

But it'd become something like that after a ContainerStart:

containertypes.PortMap{"80/tcp": {{HostIP: "", HostPort: 0}}}

This doesn't make much sense, and it's not clear whether this was intentional or just a bug that slipped through.

From an API standpoint, it'd be less surprising if an empty PortBindings slice was the equivalent of no containertypes.PortMap{} entry.

We can't drop this behavior unceremoniously for older API clients that (unknowingly) rely on that behavior, but we can move that backfilling to the API layer, return a warning for API 1.52, and drop it in 1.53. We also need to take care of correctly backfilling containers stored on-disk as they might have been created with older Engine versions.

Once done, we'll be able to remove the backfilling done here.

Comment on lines +119 to +127
for p := range ctr.Config.ExposedPorts {
if _, ok := portBindings[p]; !ok {
// Create nil entries for exposed but un-mapped ports.
portBindings[p] = nil
} else if len(portBindings[p]) == 0 {
// Create default port binding.
portBindings[p] = []containertypes.PortBinding{{}}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would create a default PortBinding for any ports with no specified port binding.

You mean for any Exposed ports with no specified port binding? I believe this is the current behavior 🤔

If I'm misunderstanding your comment, could you please include a diff of before / after removing nat.SortPortMap?

Comment on lines +124 to +125
// Create default port binding.
portBindings[p] = []containertypes.PortBinding{{}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this? An empty PortBinding is treated as a port binding requesting an ephemeral port to be bound on unspecified addrs (i.e. 0.0.0.0 and ::). So, I think we should not add an empty PortBinding here, but maybe I'm missing something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@austinvazquez This still needs to be fixed 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about that. I slightly misunderstood #50874 to mean we needed to keep this logic for one more release. Now I understand we can remove it and the issue will be handled (and removed) higher in the call stack.

@austinvazquez
Copy link
Contributor Author

Pretty sure these two commits need to be squashed but first wanted to rebase on top of @akerouanton 's changes to get my bearings again on this work.

Comment on lines +293 to +297
if strings.ContainsFunc(proto, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}) {
return PortProto(""), fmt.Errorf("invalid protocol '%s'", proto)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corhere , I know we discussed keeping the protocol validation on the server side to minimize the interop issues with older clients and newer servers, but I assume alphanumeric check here is somewhat future-proof. Mainly I was trying to handle the test case ParsePort("0/tcp/").

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not mind if ParsePort("0/tcp/") == Port{proto: unique.Make("tcp/")}. And I don't think it would be wise to restrict ourselves to alphanumerics anyway. We might have a good reason to encode more than just an L4 protocol name into the proto part of the Port. Some hypothetical examples: "443/tcp+tls", "8000/udp+ipsec", "8080/tcp:ipv6only".

Also, consider the PortFrom function. If only alphanumeric protocol names are permitted, the constructor will need to validate the proto parameter and report an error if invalid. It's just simpler all around to allow any nonempty string as the protocol and let the daemon reject protocols it does not recognize.

Comment on lines +293 to +297
if strings.ContainsFunc(proto, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}) {
return PortProto(""), fmt.Errorf("invalid protocol '%s'", proto)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not mind if ParsePort("0/tcp/") == Port{proto: unique.Make("tcp/")}. And I don't think it would be wise to restrict ourselves to alphanumerics anyway. We might have a good reason to encode more than just an L4 protocol name into the proto part of the Port. Some hypothetical examples: "443/tcp+tls", "8000/udp+ipsec", "8080/tcp:ipv6only".

Also, consider the PortFrom function. If only alphanumeric protocol names are permitted, the constructor will need to validate the proto parameter and report an error if invalid. It's just simpler all around to allow any nonempty string as the protocol and let the daemon reject protocols it does not recognize.

@austinvazquez austinvazquez force-pushed the define-network-port-types branch from 95baa65 to f9266a1 Compare September 26, 2025 12:49
}
if proto == "" {
return PortRange{}, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now second-guessing how these cases are handled. I can't think of a good justification to explain why some edge case inputs result in a zero-value PortRange while others result in an error. It also violates the Go convention where err == nil (or ok == true) signals that the other result parameters are meaningful.

One possibility would be to implicitly normalize the port numbers: if end < start { start, end = end, start }. That would allow us to make PortRangeFrom consistent with PortFrom, returning the zero value iff the protocol is empty, and eliminating the need for an error result parameter. But that would be inconsistent with ParsePortRange, and I am having a hard time justifying why PortRangeFrom(2, 1, TCP) is acceptable but ParsePortRange("2-1/tcp") is not.

Do we simply drop the error result, returning PortRange{} when end < start || proto == ""? That doesn't seem right either, as that would marshal to the empty string.

Perhaps we're just overthinking things. What if we split the difference and return ok bool, like netip.AddrFromSlice?

// PortFrom returns a Port with the given number and protocol.
//
// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
func PortFrom(num uint16, proto PortProto) (p port, ok bool)

// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
//
// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
func PortRangeFrom(start, end uint16, proto PortProto) (r PortRange, ok bool)

It's not like the error string is particularly informative apart from its nilness. Returning multiple values forces the caller to acknowledge that the constructor is not guaranteed to return a valid value for all inputs, and to handle (or explicitly ignore) the unhappy path.

It is notable that netip.PrefixFrom has a single result parameter despite not being guaranteed to return a valid value for all inputs either. It returns an invalid but nonzero value when the bits parameter is out of range. I do not think that such behaviour would make sense for either of our PortFrom or PortRangeFrom constructors. A netip.Prefix wraps a netip.Addr; even if the prefix length is bogus the IP address still could be meaningful. There is value in encoding and passing around such invalid Prefixes. In contrast, when end < start in a PortRangeFrom call the only possibly well-formed piece of information is the protocol, which unlike the Addr of an invalid Prefix, is not meaningful out of context. Since the caller of PortFrom or PortRangeFrom is expected to get a well-formed PortRange as the result, the zero value is not a valid result for the function. Therefore returning a single result that may be zero-valued would be considered in-band signalling, which is not best practice.

In summary, my recommendation would be to have both PortFrom and PortRangeFrom return an ok bool result parameter.

Copy link
Contributor Author

@austinvazquez austinvazquez Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay yeah I think I can get behind that. Even when authoring the error solution, it didn't feel as nice as the PortFrom implementation. +1 to the argument and reference to netip.AddrFromSlice. At least we can give a consistent experience across Port(Range)From functions while forcing the user to acknowledge the potential invalid value. Still brings most of the value of a compile time check.

@austinvazquez austinvazquez force-pushed the define-network-port-types branch from f9266a1 to f944445 Compare September 30, 2025 13:45
Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! I just have a handful of nitpicks and suggestions for the daemon code.

daemon/list.go Outdated
Comment on lines 410 to 412
for portNum := portRange.Start(); portNum <= portRange.End(); portNum++ {
filter[fmt.Sprintf("%d/%s", portNum, portRange.Proto())] = true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really slick if we provided a convenience method to iterate over the ports of a range.

Suggested change
for portNum := portRange.Start(); portNum <= portRange.End(); portNum++ {
filter[fmt.Sprintf("%d/%s", portNum, portRange.Proto())] = true
}
for p := range portRange.All() {
filter[p.String()] = true
}
func (pr PortRange) All() iter.Seq[Port] {
	return func(yield func(Port) bool) {
		for i := pr.Start(); i <= pr.End(); i++ {
			if !yield(Port{num: i, proto: pr.proto}) {
				return
			}
		}
	}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted a bit offline but doc'ing here as well. While unit testing I found it is easy to make the mistake where the iter variable is typed to uint16 and the increment can cause an infinite loop when start or end port is max value. More over proving the need for All() function.

@austinvazquez austinvazquez force-pushed the define-network-port-types branch 3 times, most recently from 906526c to 80c5d25 Compare September 30, 2025 19:33
Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🎉

@austinvazquez austinvazquez force-pushed the define-network-port-types branch from 80c5d25 to ca570c6 Compare September 30, 2025 20:35
Copy link
Member

@akerouanton akerouanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 small nits, and this comment needs to be addressed: https://github.com/moby/moby/pull/50710/files#r2285063245

Otherwise LGTM.

)

// PortProto represents a network protocol for a port.
type PortProto string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PortProto sounds like a weird name to me. Every time I see it I instinctively think that it's a union of a port number and a proto, whereas it's just a proto. Maybe we can rename that type into Proto 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akerouanton , It's a little challenging because the type is actually part of the container package so container.Protocol is a little strange as well. How about NetworkProtocol?


portProto := normalizePortProto(proto)

return Port{num: portNum, proto: unique.Make(portProto)}, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time normalizePortProto is called, its output is passed to unique.Make. Maybe we can internalize this into normalizePortProto?

@austinvazquez austinvazquez force-pushed the define-network-port-types branch 2 times, most recently from acc0a57 to cfcaa46 Compare October 2, 2025 13:58
Copy link
Contributor

@robmry robmry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

As a bonus, I think this fixes #48274.

return pr
}

func (pr PortRange) All() iter.Seq[Port] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could add a doc string

Comment on lines +22 to +30
if !p.IsZero() {
t.Errorf("Port{}.IsZero() = false, want true")
}
if p.IsValid() {
t.Errorf("Port{}.IsValid() = true, want false")
}
if p.String() != "invalid port" {
t.Errorf("Port{}.String() = %q, want %q", p.String(), "invalid port")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious - as the API module already requires "gotest.tools/v3", could these tests use it? For example ...

		assert.Check(t, p.IsZero())
		assert.Check(t, !p.IsValid())
		assert.Check(t, is.Equal(p.String(), "invalid port"))

Comment on lines +118 to +127
var c int
if ip.Proto() == container.TCP {
c--
}
if strings.EqualFold(jp.Proto(), "tcp") {
return false
if jp.Proto() == container.TCP {
c++
}

return strings.ToLower(ip.Proto()) < strings.ToLower(jp.Proto())
if c != 0 {
return c
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the protos are different, so at-most one can be TCP - so I don't think c is needed? ...

	if ip.Proto() == container.TCP {
		return -1
	}
	if jp.Proto() == container.TCP {
		return 1
	}

@austinvazquez austinvazquez force-pushed the define-network-port-types branch from cfcaa46 to 21371d0 Compare October 2, 2025 16:58
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was jotting down some comments, but jumping between things and I'm late to the party, so maybe some of this was already discussed.

// Sentinel port proto value for zero Port and PortRange values.
var protoZero unique.Handle[NetworkProtocol]

// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-nit; the / is optional, but required when specifying a protocol; probably <portnum>[/<proto>] (or even <portnum>[</proto>] is more accurate. That last one is probably confusing though as it looks like a closing "html" tag 😂

return Port{}, errors.New("value is empty")
}

port, proto, _ := strings.Cut(s, "/")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silly question; do we want <port>/ (trailing slash, but no proto) to be considered valid, or (also see above) make it only accepted if used together with a proto?

Comment on lines +12 to +19
// NetworkProtocol represents a network protocol for a port.
type NetworkProtocol string

const (
TCP NetworkProtocol = "tcp"
UDP NetworkProtocol = "udp"
SCTP NetworkProtocol = "sctp"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly fine for a follow-up, but I noticed we define a type in types/swarm as well, so in som places we need to convert one to the other; wondering if we need to alias one to the other (or have an internal type that's used as alias for both);

// PortConfigProtocol represents the protocol of a port.
type PortConfigProtocol string
const (
// TODO(stevvooe): These should be used generally, not just for PortConfig.
// PortConfigProtocolTCP TCP
PortConfigProtocolTCP PortConfigProtocol = "tcp"
// PortConfigProtocolUDP UDP
PortConfigProtocolUDP PortConfigProtocol = "udp"
// PortConfigProtocolSCTP SCTP
PortConfigProtocolSCTP PortConfigProtocol = "sctp"
)

Comment on lines +183 to +187
in: "1234/",
port: portFrom(1234, TCP),
str: "1234/tcp",
portRange: portRangeFrom(1234, 1234, TCP),
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this answers my question whether we consider a trailing / to be OK; was this needed for backward compatibility, or something we want to endorse as a whole? (is this something we could get out of somehow?)

(thinking along the lines of fixing up before we parse for old API versions, to allow us to be more strict in future 🤔 - but not sure if we have that opportunity)

Comment on lines +189 to +192
in: "1234/tcp:ipv6only",
port: portFrom(1234, "tcp:ipv6only"),
str: "1234/tcp:ipv6only",
portRange: portRangeFrom(1234, 1234, "tcp:ipv6only"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, wait; was this one new? (the :ipv6only suffix)? Or was this already there?

Mostly looking at the use of : for separator here, because ... we have a lot of colons already in our formats (consider [<hostip>[<port or range>]]:[<port or range>][/<proto>][:ipv6only], where <hostip> may be IPv6.

If itt's new, would tcp6, udp6 (e.g.) be an option?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an invented hypothetical to test that the parser accepts protocols with non-alphabetic characters.

// PortFrom returns a [Port] with the given number and protocol.
//
// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I'm eyeing is the use of uint16 in some places; while "correct" (as in it's he range accepted for ports), they also tend to be slightly awkward to use at times (e.g. a strconv.Itoa won't work).

But of course, it depends on how it's used; for this one, as it's constructing the Port, it means the caller already must've done' half the work (make sure the port-number is of the right type and range); I haven't looked yet how / where it's used and if that currently requires additional handling in callers 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. ParsePort is available for users who don't have a good reason to do half the work themselves.

Comment on lines +74 to +75
// Num returns p's port number.
func (p Port) Num() uint16 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly related to the above (also depend on how it's used).

(Also still hate that net.JoinHostPort requires a string for the port 😠 😂 )

@thompson-shaun thompson-shaun moved this from Complete to Discussing in 🔦 Maintainer spotlight Oct 2, 2025
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Co-authored-by: Cory Snider <csnider@mirantis.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
@austinvazquez austinvazquez force-pushed the define-network-port-types branch from 21371d0 to cb3abac Compare October 2, 2025 18:59
@github-project-automation github-project-automation bot moved this from Discussing to Complete in 🔦 Maintainer spotlight Oct 2, 2025
@austinvazquez austinvazquez reopened this Oct 2, 2025
@austinvazquez
Copy link
Contributor Author

austinvazquez commented Oct 3, 2025

Discussion from the maintainers call:

  • the API validation for protocol create backwards compatibility problems with older daemons; this implementation is to be future proof and have the daemon perform validation for the protocols it supports
  • the port types can also be used for swarm (let's do this in follow-up to unblock netip.Addr work)
  • let's merge this as -is and more follow-up to come.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api API impact/api kind/refactor PR's that refactor, or clean-up code module/api release-blocker PRs we want to block a release on status/2-code-review

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

6 participants