Skip to content

Commit 5ad8940

Browse files
committed
fix: allow binding containers on different IPs to the same port
Fixes #4786 Signed-off-by: Kay Yan <kay.yan@daocloud.io>
1 parent d5bc0f0 commit 5ad8940

File tree

4 files changed

+123
-30
lines changed

4 files changed

+123
-30
lines changed

pkg/portutil/iptable/iptables.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,46 @@ import (
2222
"strings"
2323
)
2424

25-
// ParseIPTableRules takes a slice of iptables rules as input and returns a slice of
26-
// uint64 containing the parsed destination port numbers from the rules.
27-
func ParseIPTableRules(rules []string) []uint64 {
28-
ports := []uint64{}
25+
type PortRule struct {
26+
IP string
27+
Port uint64
28+
}
29+
30+
// ParseIPTableRules takes a slice of iptables rules as input and returns a
31+
// slice of PortRule containing the parsed destination IP and port from the
32+
// rules. When a rule has no -d flag, IP is empty (meaning the rule applies to
33+
// all addresses).
34+
func ParseIPTableRules(rules []string) []PortRule {
35+
portRules := []PortRule{}
2936

30-
// Regex to match the '--dports' option followed by the port number
31-
dportRegex := regexp.MustCompile(`--dports ((,?\d+)+)`)
37+
dportsRegex := regexp.MustCompile(`--dports ((,?\d+)+)`)
38+
dportRegex := regexp.MustCompile(`--dport (\d+)`)
39+
destRegex := regexp.MustCompile(`-d (\S+?)(?:/\d+)?\s`)
3240

3341
for _, rule := range rules {
34-
matches := dportRegex.FindStringSubmatch(rule)
35-
if len(matches) > 1 {
36-
for _, _match := range strings.Split(matches[1], ",") {
37-
port64, err := strconv.ParseUint(_match, 10, 16)
38-
if err != nil {
39-
continue
40-
}
41-
ports = append(ports, port64)
42+
var ports []string
43+
44+
if matches := dportsRegex.FindStringSubmatch(rule); len(matches) > 1 {
45+
ports = strings.Split(matches[1], ",")
46+
} else if matches := dportRegex.FindStringSubmatch(rule); len(matches) > 1 {
47+
ports = []string{matches[1]}
48+
} else {
49+
continue
50+
}
51+
52+
var ip string
53+
if destMatches := destRegex.FindStringSubmatch(rule); len(destMatches) > 1 {
54+
ip = destMatches[1]
55+
}
56+
57+
for _, portStr := range ports {
58+
port64, err := strconv.ParseUint(portStr, 10, 16)
59+
if err != nil {
60+
continue
4261
}
62+
portRules = append(portRules, PortRule{IP: ip, Port: port64})
4363
}
4464
}
4565

46-
return ports
66+
return portRules
4767
}

pkg/portutil/iptable/iptables_linux.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,55 @@
1717
package iptable
1818

1919
import (
20+
"strings"
21+
2022
"github.com/coreos/go-iptables/iptables"
2123
)
2224

2325
// Chain used for port forwarding rules: https://www.cni.dev/plugins/current/meta/portmap/#dnat
2426
const cniDnatChain = "CNI-HOSTPORT-DNAT"
2527

28+
// cniDNChainPrefix is the prefix for per-container DNAT sub-chains created by
29+
// the CNI portmap plugin. These sub-chains contain the actual DNAT rules with
30+
// destination IP filtering (e.g. -d 192.168.1.141/32 --dport 80 -j DNAT).
31+
const cniDNChainPrefix = "CNI-DN-"
32+
2633
func ReadIPTables(table string) ([]string, error) {
2734
ipt, err := iptables.New()
2835
if err != nil {
2936
return nil, err
3037
}
3138

3239
var rules []string
33-
chainExists, _ := ipt.ChainExists(table, cniDnatChain)
34-
if chainExists {
35-
rules, err = ipt.List(table, cniDnatChain)
36-
if err != nil {
37-
return nil, err
40+
41+
// Read per-container DNAT sub-chains (CNI-DN-*) which contain the actual
42+
// DNAT rules with both destination IP and port information.
43+
// The parent chain (CNI-HOSTPORT-DNAT) only dispatches by port to these
44+
// sub-chains and does not include destination IP filtering.
45+
chains, err := ipt.ListChains(table)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
for _, chain := range chains {
51+
if strings.HasPrefix(chain, cniDNChainPrefix) {
52+
subRules, err := ipt.List(table, chain)
53+
if err != nil {
54+
continue
55+
}
56+
rules = append(rules, subRules...)
57+
}
58+
}
59+
60+
// Fall back to reading the parent chain if no CNI-DN-* sub-chains were read.
61+
if len(rules) == 0 {
62+
chainExists, _ := ipt.ChainExists(table, cniDnatChain)
63+
if chainExists {
64+
parentRules, err := ipt.List(table, cniDnatChain)
65+
if err != nil {
66+
return nil, err
67+
}
68+
rules = append(rules, parentRules...)
3869
}
3970
}
4071

pkg/portutil/iptable/iptables_test.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,65 @@ func TestParseIPTableRules(t *testing.T) {
2424
testCases := []struct {
2525
name string
2626
rules []string
27-
want []uint64
27+
want []PortRule
2828
}{
2929
{
3030
name: "Empty input",
3131
rules: []string{},
32-
want: []uint64{},
32+
want: []PortRule{},
3333
},
3434
{
3535
name: "Single rule with single port",
3636
rules: []string{
3737
"-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 8080 -j CNI-DN-some-hash",
3838
},
39-
want: []uint64{8080},
39+
want: []PortRule{{IP: "", Port: 8080}},
4040
},
4141
{
4242
name: "Multiple rules with multiple ports",
4343
rules: []string{
4444
"-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 8080 -j CNI-DN-some-hash",
4545
"-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 9090 -j CNI-DN-some-hash",
4646
},
47-
want: []uint64{8080, 9090},
47+
want: []PortRule{
48+
{IP: "", Port: 8080},
49+
{IP: "", Port: 9090},
50+
},
51+
},
52+
{
53+
name: "Single rule with comma-separated ports",
54+
rules: []string{
55+
"-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 8080,9090 -j CNI-DN-some-hash",
56+
},
57+
want: []PortRule{
58+
{IP: "", Port: 8080},
59+
{IP: "", Port: 9090},
60+
},
61+
},
62+
{
63+
name: "Sub-chain DNAT rule with destination IP",
64+
rules: []string{
65+
"-A CNI-DN-some-hash -d 192.168.1.141/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.4.0.2:80",
66+
},
67+
want: []PortRule{{IP: "192.168.1.141", Port: 80}},
68+
},
69+
{
70+
name: "Multiple sub-chain rules with different IPs same port",
71+
rules: []string{
72+
"-A CNI-DN-hash1 -d 192.168.1.141/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.4.0.2:80",
73+
"-A CNI-DN-hash2 -d 192.168.1.142/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.4.0.3:80",
74+
},
75+
want: []PortRule{
76+
{IP: "192.168.1.141", Port: 80},
77+
{IP: "192.168.1.142", Port: 80},
78+
},
79+
},
80+
{
81+
name: "Sub-chain rule without CIDR suffix",
82+
rules: []string{
83+
"-A CNI-DN-hash1 -d 10.0.0.1 -p tcp -m tcp --dport 443 -j DNAT --to-destination 10.4.0.2:443",
84+
},
85+
want: []PortRule{{IP: "10.0.0.1", Port: 443}},
4886
},
4987
}
5088

@@ -58,12 +96,12 @@ func TestParseIPTableRules(t *testing.T) {
5896
}
5997
}
6098

61-
func equal(a, b []uint64) bool {
99+
func equal(a, b []PortRule) bool {
62100
if len(a) != len(b) {
63101
return false
64102
}
65-
for i, v := range a {
66-
if v != b[i] {
103+
for i := range a {
104+
if a[i] != b[i] {
67105
return false
68106
}
69107
}

pkg/portutil/port_allocate_linux.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,14 @@ func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) {
123123
if err != nil {
124124
return nil, err
125125
}
126-
destinationPorts := iptable.ParseIPTableRules(ipTableItems)
126+
portRules := iptable.ParseIPTableRules(ipTableItems)
127127

128-
for _, port := range destinationPorts {
129-
usedPort[port] = true
128+
requestedIsWildcard := ip == "" || ip == "0.0.0.0" || ip == "::"
129+
for _, rule := range portRules {
130+
ruleIsWildcard := rule.IP == "" || rule.IP == "0.0.0.0" || rule.IP == "::"
131+
if requestedIsWildcard || ruleIsWildcard || rule.IP == ip {
132+
usedPort[rule.Port] = true
133+
}
130134
}
131135

132136
return usedPort, nil

0 commit comments

Comments
 (0)