Skip to content

libnetwork/portallocator: assorted cleanups#48373

Merged
thaJeztah merged 8 commits intomoby:masterfrom
thaJeztah:cleanup_portallocator
Aug 29, 2024
Merged

libnetwork/portallocator: assorted cleanups#48373
thaJeztah merged 8 commits intomoby:masterfrom
thaJeztah:cleanup_portallocator

Conversation

@thaJeztah
Copy link
Copy Markdown
Member

@thaJeztah thaJeztah commented Aug 26, 2024

portallocator: RequestPortInRange: fix doc-link in godoc

The doc-link was not formatted correctly and didn't work. While updating
also slightly touch-up the description to explain "defaultIP".

portallocator: ReleaseAll: remove unused error-return

portallocator: use new instance in tests

Test the functionality in isolation instead of using the singleton that's
returned by the GET function; this makes sure tests don't affect each other,
and doesn't require resetting the singleton in between tests, potentially
allowing these tests to eb run in parallel.

portallocator: use net.IPv4zero for defaultIP, and make it a property

Use the variable that's provided by the net package, and make the defaultIP
a property of the allocator instead of a package variable.

portallocator: RequestPort: skip RequestPortInRange as intermediate

RequestPortInRange is a wrapper for RequestPortsInRange; skip it as
intermediate function.

portallocator: make newPortMap a regular constructor

It was a method on PortAllocator, but not really related, other than
reading the default values.

portallocator: RequestPortsInRange: validate range once

RequestPortsInRange calls portMap.getPortRange() in a loop, but the given
port-range is always the same. Perform validation once instead of for each
iteration.

portallocator: un-export PortAllocator.Begin, PortAllocator.End

These values are configured when instantiating the allocator, and not
intended to be mutated externally. They're only used internally with
the exception of a test in the bridge driver that uses it to pick a
port that can be used for testing.

This patch:

  • un-exports the Begin and End fields
  • introduces a GetPortRange() utility to allow the bridge driver
    to get the port, but marking it as a function for internal use.

- Description for the changelog

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

@thaJeztah thaJeztah added status/2-code-review area/networking Networking kind/refactor PR's that refactor, or clean-up code area/networking/portmapping Networking labels Aug 26, 2024
@thaJeztah thaJeztah added this to the 28.0.0 milestone Aug 26, 2024
@thaJeztah thaJeztah self-assigned this Aug 26, 2024
@thaJeztah thaJeztah force-pushed the cleanup_portallocator branch 2 times, most recently from 20c88d4 to 12f54ea Compare August 26, 2024 15:06
@thaJeztah thaJeztah marked this pull request as ready for review August 26, 2024 19:32
Copy link
Copy Markdown
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

Begin: start,
End: end,
ipMap: ipMapping{},
defaultIP: net.IPv4zero,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see why it'd be better to turn defaultIP into a PortAllocator property. However, I agree that tests shouldn't rely on this var, and I think we can just ditch that defaultIP altogether ⬇️

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not really tied to this; I initially kept it as a package variable, but decided to move it internal to the PortAllocator, mostly because that's already a singleton and to make it more clear it's internal to that instance, but also (slightly) considering if this would be something configurable (i.e. when instancing the allocator).

@@ -114,7 +115,7 @@ func (p *PortAllocator) RequestPort(ip net.IP, proto string, port int) (int, err
// default IP (0.0.0.0).
func (p *PortAllocator) RequestPortInRange(ip net.IP, proto string, portStart, portEnd int) (int, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Excluding tests, RequestPortInRange is only called by RequestPort. RequestPort is only called from allocateDaemonPort in a loop that already prevents ip to be nil.

I think we can just drop that defaulting behavior from this method, but it's probably worth taking a defensive stance by returning an error if ip is nil.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think I had a patch stashed that removed that loop, which I left out of this PR for now, but, yes, also agreed that we could look at reducing the signature, and remove what we don't need.


if ip == nil {
ip = defaultIP
ip = p.defaultIP // FIXME(thaJeztah): consider making this a required argument and producing an error instead, or set default when constructing.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For this one, there's a bit more code involved but I think we're all good too (ie. we can remove this defaulting).

ReleasePorts is called from two places:

  1. attemptBindHostPorts, in a defer while iterating on the addrs slice, which is built from portBindingReq.childHostIP. This field is set in setChildHostIP. This method either re-use PBs HostIP as-is (they already get a default value applied), or uses what the portDriverClient.ChildHostIP() returns (which is a netip.Addr).

if len(bnd.HostIP) == 0 {
if defHostIP.To4() == nil {
// The default binding address is IPv6.
return portBindingReq{}, false
}
bnd.HostIP = defHostIP
}
// Unmap the addresses if they're IPv4-mapped IPv6.
bnd.HostIP = bnd.HostIP.To4()
bnd.IP = containerIPv4.To4()
return setChildHostIP(pdc, portBindingReq{
PortBinding: bnd,
disableNAT: disableNAT,
}), true

// If there's no host address, use the default.
if len(bnd.HostIP) == 0 {
if defHostIP.Equal(net.IPv4zero) {
if !netutils.IsV6Listenable() {
// No implicit binding if the host has no IPv6 support.
return portBindingReq{}, false
}
// Implicit binding to "::", no explicit HostIP and the default is 0.0.0.0
bnd.HostIP = net.IPv6zero
} else if defHostIP.To4() == nil {
// The default binding IP is an IPv6 address, use it.
bnd.HostIP = defHostIP
} else {
// The default binding IP is an IPv4 address, nothing to do here.
return portBindingReq{}, false
}
}
bnd.IP = containerIP
return setChildHostIP(pdc, portBindingReq{
PortBinding: bnd,
disableNAT: disableNAT,
}), true

func setChildHostIP(pdc portDriverClient, req portBindingReq) portBindingReq {
if pdc == nil {
req.childHostIP = req.HostIP
return req
}
hip, _ := netip.AddrFromSlice(req.HostIP)
req.childHostIP = pdc.ChildHostIP(hip).AsSlice()
return req
}

  1. releasePortBindings, which is called:
    a. in a defer in addPortMappings with the result of bindHostPorts() -> attemptBindHostPorts().
    b. from releasePorts with the list of port mappings that were effectively created.

Maybe it's worth changing the signature of ReleasePorts to make sure no nil net.IP get passed.

I think in the future we should also consider replacing net.IP with netip.Addr.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup; I'd be +1 for sure to make it a hard failure. I was on the fence changing it already, but went looking where the value came from, and there were quite some places where it could be set, so I decided to (for now) leave a TODO here and to leave it for a follow-up.

(Also because I didn't want to shove such a change in a general "cleanup").

Comment thread libnetwork/portallocator/portallocator.go
@akerouanton
Copy link
Copy Markdown
Member

With the recent changes made by @robmry to our portmapper, I really wonder how useful this whole portallocator is. We're now using bind syscalls to reserve 'OS-level' ports (and thus confirm that they're free). Before that change, there was a risk of port clashes being detected after inserting iptables rules, but not anymore.

Maybe we should just ditch this package altogether? WDYT @robmry?

@robmry
Copy link
Copy Markdown
Contributor

robmry commented Aug 27, 2024

Maybe we should just ditch this package altogether? WDYT @robmry?

I think it's still useful to track where to start the search for the next free port (to avoid reallocating the same port for short-lived mappings) - but it could be an internal bridge driver package, and possibly simplified.

The doc-link was not formatted correctly and didn't work. While updating
also slightly touch-up the description to explain "defaultIP".

Signed-off-by: Sebastiaan van Stijn <[email protected]>
Test the functionality in isolation instead of using the singleton that's
returned by the `GET` function; this makes sure tests don't affect each other,
and doesn't require resetting the singleton in between tests, potentially
allowing these tests to eb run in parallel.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
Use the variable that's provided by the net package, and make the defaultIP
a property of the allocator instead of a package variable.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
RequestPortInRange is a wrapper for RequestPortsInRange; skip it as
intermediate function.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
It was a method on PortAllocator, but not really related, other than
reading the default values.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
RequestPortsInRange calls portMap.getPortRange() in a loop, but the given
port-range is always the same. Perform validation once instead of for each
iteration.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
These values are configured when instantiating the allocator, and not
intended to be mutated externally. They're only used internally with
the exception of a test in the bridge driver that uses it to pick a
port that can be used for testing.

This patch:

- un-exports the Begin and End fields
- introduces a GetPortRange() utility to allow the bridge driver
  to get the port, but marking it as a function for internal use.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
@thaJeztah thaJeztah force-pushed the cleanup_portallocator branch from 12f54ea to 8e580ef Compare August 27, 2024 15:15
@thaJeztah thaJeztah merged commit 623e717 into moby:master Aug 29, 2024
@thaJeztah thaJeztah deleted the cleanup_portallocator branch August 29, 2024 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking/portmapping Networking area/networking Networking kind/refactor PR's that refactor, or clean-up code status/2-code-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants