Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Change Conntrack to delete by Both Port And IP#52423

Merged
thaJeztah merged 1 commit into
moby:mastermoby/moby:masterfrom
CarsonLenze:masterCarsonLenze/moby:masterCopy head branch name to clipboard
May 5, 2026
Merged

Change Conntrack to delete by Both Port And IP#52423
thaJeztah merged 1 commit into
moby:mastermoby/moby:masterfrom
CarsonLenze:masterCarsonLenze/moby:masterCopy head branch name to clipboard

Conversation

@CarsonLenze

@CarsonLenze CarsonLenze commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

- What I did

Changed DeleteConntrackEntriesByPort to delete by both dest IP And dest Port.
I was running into issues where I was running two diffrent containers with the same port but diffrent IPs. When restarting one container it would delete all the conntrack entires for both containers since it was only deleting by port.

Im not sure if there are any issues open about this, or if this is even the best way to fix this, but It was causing issues for us and seemed like a fairly major issue.

- How I did it

Passed the HostIP from portMapping into DeleteConntrackEntriesByPort and added the IPs as a filter by ConntrackOrigDstIP

- How to verify it

Start up a udp listener in one container on one IP, then start up another udp listener in another container on the same port but with a diffrent IP. Restarting one container shouldn't cause the conntrack entires from the other container to get deleted.

- Human readable description for the release notes

Fix conntrack entries being incorrectly deleted for UDP containers sharing the same port on different IPs when one container is restarted.

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

Here is one of my pet turtles :)

IMG_9689

@github-actions github-actions Bot added the area/daemon Core Engine label Apr 21, 2026
@thaJeztah thaJeztah added the area/networking Networking label Apr 21, 2026
@thaJeztah

Copy link
Copy Markdown
Member

I marked the DCO check to pass, because it was complaining about the name not matching the metadata from your commit;

Commit sha: 5deb7f3, Author: CarsonLenze, Committer: CarsonLenze; Expected "CarsonLenze <carsonlenze@gmail.com>", but got "Carson Lenze <carsonlenze@gmail.com>".

If you want Git to automatically set the correct name for your sign-offs, you can update your Git config with the right name;

git config --global user.name "Carson Lenze"

@thaJeztah
thaJeztah requested a review from robmry April 21, 2026 07:33

@thaJeztah thaJeztah left a comment

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.

Left some quick comments on the implementation, but would love @robmry to have a look for the intended change as a whole.

Comment thread daemon/libnetwork/iptables/conntrack.go Outdated
log.G(context.TODO()).Warnf("Failed to delete conntrack state for %s port %d: %v", proto.String(), port, err)
continue
}
if err := filter.AddIP(netlink.ConntrackOrigDstIP, hostIPs[i]); err != nil {

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.

Having 2 separate slices (one for ports and one for ip-addresses) looks brittle; it now becomes a contract to make sure they're the same length.

Comment on lines +1676 to +1679
for _, pb := range ep.portMapping {
if pb.Proto == types.UDP {
udpPorts = append(udpPorts, pb.HostPort)
udpHostIPs = append(udpHostIPs, pb.HostIP)

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.

(related to my other comment) and it looks like those slices are created by deconstructing the list of port-mappings.

I wonder if we should make DeleteConntrackEntriesByPort accept a list of port-bindings, so that all combination needed to perform the delete is combined.

// PortBinding represents a port binding between the container and the host
type PortBinding struct {
Proto Protocol
IP net.IP
Port uint16
HostIP net.IP
HostPort uint16
HostPortEnd uint16
}

At a quick glance, this is the only location the function is used, so we can adjust it for this purpose.

And; it looks like the ep.portMapping slice contains a list of portmapperapi.PortBinding, which already contains that type;

type PortBinding struct {
// PortBinding contains the port binding information reported through the
// Engine API.
types.PortBinding

So, we can collect that info;

var udpPorts []types.PortBinding
for _, pb := range ep.portMapping {
		if pb.Proto == types.UDP {
			udpPorts = append(udpPorts, pb.PortBinding)
		}
}

Then pass it to DeleteConntrackEntriesByPort as-is;

iptables.DeleteConntrackEntriesByPort(nlh, types.UDP, udpPorts)

I even wonder if we then still need the proto argument; it looks like it's only used for a log-message outside of the loop inside that function, but we can keep that for a follow-up cleanup;

log.G(context.TODO()).Debugf("DeleteConntrackEntriesByPort for %s ports purged ipv4:%d, ipv6:%d", proto.String(), totalIPv4FlowPurged, totalIPv6FlowPurged)

@robmry

robmry commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Sounds good to me - thank you.

The original code came from #44742 ... there's no sign the current behaviour was intentional, and I can't think why it would have been. But cc @akerouanton, @corhere.

Passing in the port binding rather than two separate lists sounds good - it'd also be great to have a regression test that demonstrates the issue and the fix.

@CarsonLenze

Copy link
Copy Markdown
Contributor Author

Hey @thaJeztah, I made the changes as requested. I also tried to update my DCO and its still not wanting to add a space in my name, sorry about that.

@robmry I'm not too familiar with regression tests. I went ahead and tested it locally by just checking to see what Conntrack entires get deleted on containers stopping, and all looks good. If you could help me or point me in the right direction I can attempt to create those tests.

@CarsonLenze
CarsonLenze requested a review from thaJeztah April 22, 2026 03:59
@thaJeztah

Copy link
Copy Markdown
Member

Oh! Looks like one of your commits is missing a Signed-off-by: line (it has your name and e-mail, but misses the prefix); looks like I won't be able to "skip" that one, so if you can fix?

While doing so, could you also squash the two commits? (Let me know if you need help doing so)

Signed-off-by: CarsonLenze <carsonlenze@gmail.com>

Change to use PortBinding

Signed-off-by: CarsonLenze <carsonlenze@gmail.com>
@CarsonLenze

Copy link
Copy Markdown
Contributor Author

Got those done, Let me know if you need me to do anything else!

@thaJeztah thaJeztah added this to the 29.5.0 milestone May 4, 2026

@thaJeztah thaJeztah left a comment

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.

LGTM, thx!

@thaJeztah
thaJeztah merged commit 0fef616 into moby:master May 5, 2026
258 of 261 checks passed
Comment on lines +76 to +77
if err := filter.AddIP(netlink.ConntrackOrigDstIP, port.HostIP); err != nil {
log.G(context.TODO()).Warnf("Failed to delete conntrack state for %s port %d: %v", proto.String(), port.Port, err)

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.

@AkihiroSuda I just noticed there's also a ChildHostIP field on the wrapper portmapperapi.PortBinding struct - is that relevant in this context?

// ChildHostIP is the host IP address, as seen from the daemon. This
// is normally the same as PortBinding.HostIP but, in rootless mode, it
// will be an address in the rootless network namespace. RootlessKit
// binds the port on the real (parent) host address and maps it to the
// same port number on the address dockerd sees in the child namespace.
// So, for example, docker-proxy and DNAT rules need to use the child
// namespace's host address. (PortBinding.HostIP isn't replaced by the
// child address, because it's stored as user-config and the child
// address may change if RootlessKit is configured differently.)
ChildHostIP net.IP `json:"-"`

(and an accessor);

// ChildPortBinding is pb.PortBinding, with the host address the daemon
// will see - which, in rootless mode, will be an address in the RootlessKit's
// child namespace (see PortBinding.ChildHostIP).
func (pb PortBinding) ChildPortBinding() types.PortBinding {
res := pb.PortBinding
res.HostIP = pb.ChildHostIP
return res
}

@vincentbernat

Copy link
Copy Markdown
Contributor

This change does not work if you don't bind to a specific IP address as the IP would be 0.0.0.0 or ::.

@vincentbernat

Copy link
Copy Markdown
Contributor

Proposed fix here: #52640

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.