Skip to content

Commit 9f4158b

Browse files
committed
cni: add ipv6/dual-stack support for bridge, calico cni
1 parent 11e82ed commit 9f4158b

File tree

9 files changed

+456
-143
lines changed

9 files changed

+456
-143
lines changed

pkg/drivers/kic/kic.go

Lines changed: 13 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
@@ -115,15 +114,18 @@ func (d *Driver) Create() error {
115114
if err != nil {
116115
msg := "Unable to create dedicated network, this might result in cluster IP change after restart: {{.error}}"
117116
args := out.V{"error": err}
118-
if staticIP != "" || d.NodeConfig.StaticIPv6 != "" {
119-
// If the user requested a static IP on either family, failing
120-
// to create the dedicated network should be fatal.
117+
if staticIP != "" {
118+
// With a user-requested static IP we can’t safely fall back
119+
// to the default bridge network.
121120
exit.Message(reason.IfDedicatedNetwork, msg, args)
122121
}
123122
out.WarningT(msg, args)
123+
// NOTE: Do NOT set params.Network here – we’ll fall back to Docker’s
124+
// default bridge network.
124125
} else {
125-
// Only attach to the user-defined network when creation/reuse
126-
// succeeded. For IPv6-only networks, gateway may legitimately be nil.
126+
// Only attach to the dedicated network when creation succeeded.
127+
// For IPv6-only clusters the gateway may be nil, but the network
128+
// still exists and can be used.
127129
params.Network = networkName
128130
}
129131

@@ -469,7 +471,11 @@ func (d *Driver) Remove() error {
469471
return fmt.Errorf("expected no container ID be found for %q after delete. but got %q", d.MachineName, id)
470472
}
471473

472-
if err := oci.RemoveNetwork(d.OCIBinary, d.NodeConfig.ClusterName); err != nil {
474+
networkName := d.NodeConfig.ClusterName
475+
if d.NodeConfig.Network != "" {
476+
networkName = d.NodeConfig.Network
477+
}
478+
if err := oci.RemoveNetwork(d.OCIBinary, networkName); err != nil {
473479
klog.Warningf("failed to remove network (which might be okay) %s: %v", d.NodeConfig.ClusterName, err)
474480
}
475481
return nil

pkg/drivers/kic/oci/network_create.go

Lines changed: 11 additions & 3 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"
@@ -174,11 +179,14 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
174179
return nil, fmt.Errorf("failed to create %s network %s (dual): %w", ociBin, networkName, lastErr)
175180
}
176181

177-
// ----- IPv6-only (no IPv4 subnet) -----
178182
args := append([]string{}, baseArgs...)
179183
if subnetv6 != "" {
180184
args = append(args, "--subnet", subnetv6)
181185
}
186+
// ipv6-only / “IPv6 + auto IPv4” branch:
187+
// - ipFamily == "ipv6"
188+
// - OR ipFamily == "dual" && staticIP == "" (Docker picks IPv4 range)
189+
args = append(args, "--subnet", subnetv6)
182190
if ociBin == Docker && bridgeInfo.mtu > 0 {
183191
args = append(args, "-o", fmt.Sprintf("com.docker.network.driver.mtu=%d", bridgeInfo.mtu))
184192
}
@@ -189,7 +197,7 @@ func CreateNetworkWithIPFamily(ociBin, networkName, subnet, subnetv6, staticIP,
189197
)
190198

191199
if _, err := runCmd(exec.Command(ociBin, args...)); err != nil {
192-
klog.Warningf("failed to create %s network %q (ipv6-only): %v", ociBin, networkName, err)
200+
klog.Warningf("failed to create %s network %q (ipv6/dual): %v", ociBin, networkName, err)
193201
return nil, fmt.Errorf("create %s network %q: %w", ociBin, networkName, err)
194202
}
195203

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)