Skip to content

fix: allow binding containers on different IPs to the same port#4800

Open
yankay wants to merge 1 commit intocontainerd:mainfrom
yankay:fix-port-different-ip
Open

fix: allow binding containers on different IPs to the same port#4800
yankay wants to merge 1 commit intocontainerd:mainfrom
yankay:fix-port-different-ip

Conversation

@yankay
Copy link
Contributor

@yankay yankay commented Mar 19, 2026

Summary

Fixes #4786

The port conflict check introduced in PR #4097 (commit 04f836d) incorrectly reports "port is already allocated" when binding containers to different host IPs but the same port number. For example:

services:
  nginx-primary:
    ports:
      - "192.168.1.141:80:80"
      - "192.168.1.142:80:80"
  nginx-sec:
    ports:
      - "192.168.1.143:80:80"   # fails: port is already allocated

Root cause

  1. ReadIPTables only read the CNI-HOSTPORT-DNAT parent chain, which dispatches by port but does not include -d <ip> destination IP filtering. The actual IP filtering is in CNI-DN-* sub-chains.
  2. ParseIPTableRules only extracted port numbers from --dports and completely ignored destination IPs.

This made every iptables-managed port appear globally allocated regardless of which host IP it was bound to.

Fix

  • ReadIPTables: Read CNI-DN-* sub-chains (which contain DNAT rules with both -d <ip> and --dport), falling back to the parent chain if no sub-chains exist.
  • ParseIPTableRules: Return PortRule{IP, Port} instead of just port numbers. Parse both --dport (sub-chains) and --dports (parent chain), and extract -d <ip> destination.
  • getUsedPorts: Filter iptables ports by IP overlap — wildcard addresses ("", 0.0.0.0, ::) conflict with everything; specific IPs only conflict with the same IP or wildcards.

Test plan

  • Unit tests pass (go test ./pkg/portutil/...)
  • Manual test: three containers on 127.0.0.1/127.0.0.2/127.0.0.3:8880 all start successfully
  • Manual test: duplicate IP+port correctly rejected
  • Manual test: 0.0.0.0 wildcard correctly conflicts with existing bindings
  • Manual test: all containers accessible via curl (HTTP 200)

Copilot AI review requested due to automatic review settings March 19, 2026 13:51
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a regression in nerdctl’s Linux port-conflict detection so containers can bind the same host port on different host IPs without being incorrectly rejected as “already allocated” when CNI portmap iptables rules are present.

Changes:

  • Read CNI portmap per-container DNAT sub-chains (CNI-DN-*) from iptables to capture destination-IP-specific bindings (with fallback to the parent chain when sub-chains don’t exist).
  • Extend iptables rule parsing to return {IP, Port} pairs (supporting both --dports and --dport, and extracting -d <ip>).
  • Update used-port filtering to only treat a port as conflicting when IPs overlap (wildcards conflict with everything; specific IPs only with same IP or wildcard).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
pkg/portutil/port_allocate_linux.go Filters iptables-derived used ports by requested IP vs rule IP (wildcard-aware).
pkg/portutil/iptable/iptables.go Changes parser output from ports-only to {IP, Port} and parses -d + --dport/--dports.
pkg/portutil/iptable/iptables_test.go Updates unit tests for the new {IP, Port} parsing behavior and adds sub-chain cases.
pkg/portutil/iptable/iptables_linux.go Reads CNI-DN-* sub-chains (and falls back to CNI-HOSTPORT-DNAT when absent).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// sub-chains and does not include destination IP filtering.
chains, err := ipt.ListChains(table)
if err != nil {
return nil, nil
Comment on lines +53 to +57
hasDNChains = true
subRules, err := ipt.List(table, chain)
if err != nil {
continue
}
Comment on lines +128 to +133
requestedIsWildcard := ip == "" || ip == "0.0.0.0" || ip == "::"
for _, rule := range portRules {
ruleIsWildcard := rule.IP == "" || rule.IP == "0.0.0.0" || rule.IP == "::"
if requestedIsWildcard || ruleIsWildcard || rule.IP == ip {
usedPort[rule.Port] = true
}
@yankay yankay force-pushed the fix-port-different-ip branch 3 times, most recently from 10ba730 to 41db171 Compare March 19, 2026 14:04
Fixes containerd#4786

Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Made-with: Cursor
@yankay yankay force-pushed the fix-port-different-ip branch from 41db171 to 68bdc22 Compare March 19, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression, in v2.1.3 unable to bind containers on different IPs to same ports

2 participants