Skip to content

Commit d75647e

Browse files
committed
cni: add ipv6/dual-stack support for bridge, calico cni
1 parent a1f0157 commit d75647e

File tree

9 files changed

+469
-150
lines changed

9 files changed

+469
-150
lines changed

pkg/drivers/kic/kic.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ func (d *Driver) Create() error {
9595
if params.Memory != "0" {
9696
params.Memory += "mb"
9797
}
98-
9998
networkName := d.NodeConfig.Network
10099
if networkName == "" {
101100
networkName = d.NodeConfig.ClusterName
@@ -106,21 +105,28 @@ func (d *Driver) Create() error {
106105
d.OCIBinary,
107106
networkName,
108107
d.NodeConfig.Subnet,
109-
d.NodeConfig.Subnetv6, // NEW
108+
d.NodeConfig.Subnetv6,
110109
staticIP,
111-
d.NodeConfig.StaticIPv6, // NEW
112-
params.IPFamily, // NEW
110+
d.NodeConfig.StaticIPv6,
111+
params.IPFamily,
113112
)
114113
if err != nil {
115114
msg := "Unable to create dedicated network, this might result in cluster IP change after restart: {{.error}}"
116115
args := out.V{"error": err}
117116
if staticIP != "" {
117+
// With a user-requested static IP we can’t safely fall back
118+
// to the default bridge network.
118119
exit.Message(reason.IfDedicatedNetwork, msg, args)
119120
}
120121
out.WarningT(msg, args)
122+
// NOTE: Do NOT set params.Network here – we’ll fall back to Docker’s
123+
// default bridge network.
124+
} else {
125+
// Only attach to the dedicated network when creation succeeded.
126+
// For IPv6-only clusters the gateway may be nil, but the network
127+
// still exists and can be used.
128+
params.Network = networkName
121129
}
122-
// Always attach to the created user network (even if gateway is nil in IPv6-only)
123-
params.Network = networkName
124130

125131
// Now decide static IPs per family
126132
switch params.IPFamily {
@@ -461,7 +467,11 @@ func (d *Driver) Remove() error {
461467
return fmt.Errorf("expected no container ID be found for %q after delete. but got %q", d.MachineName, id)
462468
}
463469

464-
if err := oci.RemoveNetwork(d.OCIBinary, d.NodeConfig.ClusterName); err != nil {
470+
networkName := d.NodeConfig.ClusterName
471+
if d.NodeConfig.Network != "" {
472+
networkName = d.NodeConfig.Network
473+
}
474+
if err := oci.RemoveNetwork(d.OCIBinary, networkName); err != nil {
465475
klog.Warningf("failed to remove network (which might be okay) %s: %v", d.NodeConfig.ClusterName, err)
466476
}
467477
return nil

pkg/drivers/kic/oci/network_create.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ import (
3636
// defaultFirstSubnetAddr is a first subnet to be used on first kic cluster
3737
// it is one octet more than the one used by KVM to avoid possible conflict
3838
const defaultFirstSubnetAddr = "192.168.49.0"
39-
const defaultFirstSubnetAddrv6 = "fd00::/64"
39+
40+
// defaultFirstSubnetAddrv6 is the first IPv6 subnet used for kic networks.
41+
// Avoid fd00::/64 because Docker's IPv6 pools (e.g. fixed-cidr-v6) often sit
42+
// under fd00::/xx and will trigger "Pool overlaps with other one on this
43+
// address space". Use a different ULA /64 instead.
44+
const defaultFirstSubnetAddrv6 = "fd01::/64"
4045

4146
// name of the default bridge network, used to lookup the MTU (see #9528)
4247
const dockerDefaultBridge = "bridge"
@@ -94,6 +99,7 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
9499
if berr != nil {
95100
klog.Warningf("failed to get mtu information from the %s's default network %q: %v", ociBin, bridgeName, berr)
96101
}
102+
97103
// ----- IPv6/dual flow -----
98104
if ipFamily == "ipv6" || ipFamily == "dual" {
99105
// Decide v6 subnet
@@ -112,16 +118,19 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
112118
// Build args; enable IPv6 always in this branch
113119
args := []string{"network", "create", "--driver=bridge", "--ipv6"}
114120

115-
// For dual, also choose a free IPv4 subnet similar to the v4-only flow
116-
if ipFamily == "dual" {
121+
// For dual-stack with an explicit static IPv4, we must also control v4.
122+
// In the common case (no static IPv4), let Docker choose a free IPv4
123+
// subnet and only pin the IPv6 range. This avoids
124+
// "Pool overlaps with other one on this address space" when 192.168.0.0/16
125+
// is already taken by other Docker networks.
126+
if ipFamily == "dual" && staticIP != "" {
117127
tries := 20
118128
start := firstSubnetAddr(subnet)
119129
if staticIP != "" {
120130
tries = 1
121131
start = staticIP
122132
}
123133

124-
// Try up to 5 candidate /24s starting at start, stepping by 9 (as before)
125134
var lastErr error
126135
for attempts, subnetAddr := 0, start; attempts < 5; attempts++ {
127136
var p *network.Parameters
@@ -150,7 +159,6 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
150159
}
151160

152161
out := rr.Output()
153-
// Respect same retry conditions as v4-only
154162
if strings.Contains(out, "Pool overlaps") ||
155163
(strings.Contains(out, "failed to allocate gateway") && strings.Contains(out, "Address already in use")) ||
156164
strings.Contains(out, "is being used by a network interface") ||
@@ -159,14 +167,16 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
159167
subnetAddr = p.IP
160168
continue
161169
}
162-
// Non-retryable
170+
163171
klog.Errorf("error creating dual-stack network %s: %v", networkName, err)
164172
return nil, fmt.Errorf("un-retryable: %w", err)
165173
}
166174
return nil, fmt.Errorf("failed to create %s network %s (dual): %v", ociBin, networkName, lastErr)
167175
}
168176

169-
// ipv6-only (no IPv4)
177+
// ipv6-only / “IPv6 + auto IPv4” branch:
178+
// - ipFamily == "ipv6"
179+
// - OR ipFamily == "dual" && staticIP == "" (Docker picks IPv4 range)
170180
args = append(args, "--subnet", subnetv6)
171181
if ociBin == Docker && bridgeInfo.mtu > 0 {
172182
args = append(args, "-o", fmt.Sprintf("com.docker.network.driver.mtu=%d", bridgeInfo.mtu))
@@ -178,7 +188,7 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
178188
)
179189

180190
if _, err := runCmd(exec.Command(ociBin, args...)); err != nil {
181-
klog.Warningf("failed to create %s network %q (ipv6-only): %v", ociBin, networkName, err)
191+
klog.Warningf("failed to create %s network %q (ipv6/dual): %v", ociBin, networkName, err)
182192
return nil, fmt.Errorf("create %s network %q: %w", ociBin, networkName, err)
183193
}
184194
ni, _ := containerNetworkInspect(ociBin, networkName)

pkg/minikube/cni/bridge.go

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ limitations under the License.
1717
package cni
1818

1919
import (
20-
"bytes"
20+
"encoding/json"
2121
"fmt"
2222
"os/exec"
23-
"text/template"
23+
"strings"
2424

2525
"github.com/pkg/errors"
2626
"k8s.io/minikube/pkg/minikube/assets"
@@ -32,38 +32,73 @@ import (
3232
// ref: https://www.cni.dev/plugins/current/meta/portmap/
3333
// ref: https://www.cni.dev/plugins/current/meta/firewall/
3434

35-
// note: "cannot set hairpin mode and promiscuous mode at the same time"
36-
// ref: https://github.com/containernetworking/plugins/blob/7e9ada51e751740541969e1ea5a803cbf45adcf3/plugins/main/bridge/bridge.go#L424
37-
var bridgeConf = template.Must(template.New("bridge").Parse(`
38-
{
39-
"cniVersion": "0.4.0",
40-
"name": "bridge",
41-
"plugins": [
42-
{
43-
"type": "bridge",
44-
"bridge": "bridge",
45-
"addIf": "true",
46-
"isDefaultGateway": true,
47-
"forceAddress": false,
48-
"ipMasq": true,
49-
"hairpinMode": true,
50-
"ipam": {
51-
"type": "host-local",
52-
"subnet": "{{.PodCIDR}}"
53-
}
54-
},
55-
{
56-
"type": "portmap",
57-
"capabilities": {
58-
"portMappings": true
59-
}
60-
},
61-
{
62-
"type": "firewall"
63-
}
64-
]
35+
// renderBridgeConflist builds a bridge CNI config that supports IPv4-only, IPv6-only, or dual-stack.
36+
func renderBridgeConflist(k8s config.KubernetesConfig) ([]byte, error) {
37+
// minimal structs for JSON marshal
38+
type rng struct {
39+
Subnet string `json:"subnet"`
40+
}
41+
type ipam struct {
42+
Type string `json:"type"`
43+
Subnet string `json:"subnet,omitempty"` // single-stack (v4 or v6)
44+
Ranges [][]rng `json:"ranges,omitempty"` // dual-stack
45+
}
46+
type bridge struct {
47+
Type string `json:"type"`
48+
Bridge string `json:"bridge"`
49+
IsDefaultGateway bool `json:"isDefaultGateway"`
50+
HairpinMode bool `json:"hairpinMode"`
51+
IPMasq bool `json:"ipMasq"`
52+
IPAM ipam `json:"ipam"`
53+
}
54+
type plugin struct {
55+
Type string `json:"type"`
56+
Capabilities map[string]bool `json:"capabilities,omitempty"`
57+
}
58+
type conflist struct {
59+
CNIVersion string `json:"cniVersion"`
60+
Name string `json:"name"`
61+
Plugins []interface{} `json:"plugins"`
62+
}
63+
64+
v4 := k8s.PodCIDR != ""
65+
v6 := k8s.PodCIDRv6 != ""
66+
67+
cfgIPAM := ipam{Type: "host-local"}
68+
switch {
69+
case v4 && v6:
70+
cfgIPAM.Ranges = [][]rng{{{Subnet: k8s.PodCIDR}}, {{Subnet: k8s.PodCIDRv6}}}
71+
case v6:
72+
cfgIPAM.Subnet = k8s.PodCIDRv6
73+
default:
74+
// fall back to previous default if unset upstream
75+
cidr := k8s.PodCIDR
76+
if cidr == "" {
77+
cidr = DefaultPodCIDR
78+
}
79+
cfgIPAM.Subnet = cidr
80+
}
81+
82+
// NAT generally not desired for IPv6; keep masquerade only for v4
83+
ipMasq := v4 && !v6
84+
85+
br := bridge{
86+
Type: "bridge", Bridge: "cni0",
87+
IsDefaultGateway: true,
88+
HairpinMode: true,
89+
IPMasq: ipMasq,
90+
IPAM: cfgIPAM,
91+
}
92+
portmap := plugin{Type: "portmap", Capabilities: map[string]bool{"portMappings": true}}
93+
firewall := plugin{Type: "firewall"}
94+
95+
out := conflist{
96+
CNIVersion: "1.0.0",
97+
Name: "k8s-pod-network",
98+
Plugins: []interface{}{br, portmap, firewall},
99+
}
100+
return json.MarshalIndent(out, "", " ")
65101
}
66-
`))
67102

68103
// Bridge is a simple CNI manager for single-node usage
69104
type Bridge struct {
@@ -76,14 +111,11 @@ func (c Bridge) String() string {
76111
}
77112

78113
func (c Bridge) netconf() (assets.CopyableFile, error) {
79-
input := &tmplInput{PodCIDR: DefaultPodCIDR}
80-
81-
b := bytes.Buffer{}
82-
if err := bridgeConf.Execute(&b, input); err != nil {
114+
cfgBytes, err := renderBridgeConflist(c.cc.KubernetesConfig)
115+
if err != nil {
83116
return nil, err
84117
}
85-
86-
return assets.NewMemoryAssetTarget(b.Bytes(), "/etc/cni/net.d/1-k8s.conflist", "0644"), nil
118+
return assets.NewMemoryAssetTarget(cfgBytes, "/etc/cni/net.d/1-k8s.conflist", "0644"), nil
87119
}
88120

89121
// Apply enables the CNI
@@ -110,5 +142,15 @@ func (c Bridge) Apply(r Runner) error {
110142

111143
// CIDR returns the default CIDR used by this CNI
112144
func (c Bridge) CIDR() string {
145+
146+
// Prefer explicitly-set CIDRs from the cluster config.
147+
k := c.cc.KubernetesConfig
148+
if k.PodCIDRv6 != "" && (strings.ToLower(k.IPFamily) == "ipv6" || k.PodCIDR == "") {
149+
return k.PodCIDRv6
150+
}
151+
152+
if k.PodCIDR != "" {
153+
return k.PodCIDR
154+
}
113155
return DefaultPodCIDR
114156
}

0 commit comments

Comments
 (0)